Compare commits

...

37 Commits

Author SHA1 Message Date
Jenthe Noordsij ec80d1145c Increase timeout for Docker image sync job to 30 minutes 2026-06-12 17:12:06 +02:00
qwerty8811 2391520b50 Add optional X-Forwarded-Scheme and X-Scheme headers in forwarded headers middleware 2026-06-12 11:16:07 +02:00
qwerty8811 6cc3dd8d40 Add reportNodeInternalIPs option to report node internal IPs in Ingress status 2026-06-12 10:26:07 +02:00
Anatole Lucet bcf768ee09 Update Gateway API statuses once routing config is built
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
2026-06-11 10:10:07 +02:00
kevinpollet 51b9a37615 Merge branch v3.7 into master 2026-06-10 17:05:29 +02:00
Kevin Pollet 26c96a3935 Prepare release v3.7.5 2026-06-10 16:46:07 +02:00
kevinpollet cb9e8ab510 Merge branch v3.6 into v3.7 2026-06-10 16:16:05 +02:00
Kevin Pollet b46e795f41 Prepare release v3.6.21 2026-06-10 16:02:11 +02:00
kevinpollet e53a37b869 Merge branch v2.11 into v3.6 2026-06-10 15:34:13 +02:00
Kevin Pollet ad1c1fc2f2 Prepare release v2.11.50 2026-06-10 15:28:05 +02:00
Romain 0209f984eb Fix snicheck for routers with no hosts
Co-authored-by: Gina A. <70909035+gndz07@users.noreply.github.com>
2026-06-10 15:16:06 +02:00
Kevin Pollet e043982244 Support BackendTLSPolicy for TLSRoute 2026-06-10 12:10:05 +02:00
Tim Schumacher 149e62d6db Bump to github.com/pires/go-proxyproto v0.12.0 2026-06-09 17:24:05 +02:00
Julien Salleyron 4ef4c09300 Fix routers with same host, different tlsoptions on different entryPoint
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
2026-06-09 17:08:07 +02:00
Learloj d5ad3eb63b Pass endpointslice fencing on ingress-nginx provider 2026-06-09 16:28:05 +02:00
Baptiste Mayelle 8447bfc71e Reject cross-provider references with backendRefs.namespace 2026-06-09 16:24:05 +02:00
KirylJazzSax dc4b6fe2c6 Support Backend TLS policy for gRPC backends 2026-06-09 16:22:05 +02:00
Gina A. 15ecff2bbd Skip ingress when auth-secret resolution fails 2026-06-08 14:08:05 +02:00
kevinpollet 8773d7ead4 Merge branch v3.7 into master 2026-06-05 16:01:41 +02:00
Romain 74b6408475 Prepare release v3.7.4 2026-06-05 15:56:04 +02:00
kevinpollet 708aa38f36 Merge branch v3.6 into v3.7 2026-06-05 15:26:12 +02:00
Romain 1137f1f807 Prepare release v3.6.20 2026-06-05 15:20:05 +02:00
kevinpollet 15c47f9cb4 Merge branch v2.11 into v3.6 2026-06-05 14:51:59 +02:00
Romain ba8830fdef Prepare release v2.11.49 2026-06-05 14:48:04 +02:00
Julien Salleyron b6bb80f8ff Fix snicheck with keepalive 2026-06-05 14:36:05 +02:00
Anatole Lucet 6ddda32184 Fix BackendTLSPolicy status update
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
2026-06-05 12:16:08 +02:00
Gina A. 5404f6fb25 Bump axios to v1.17.0 2026-06-05 11:22:05 +02:00
Gina A. 5d123f52e1 Bump react-router and jsdom 2026-06-05 11:20:05 +02:00
bzyy1024 d653ee3d73 Fix redis write timeout option configuration 2026-06-04 16:22:06 +02:00
Romain f32f05f811 Bump github.com/quic-go/quic-go to v0.59.1 2026-06-04 16:12:05 +02:00
romain 29406d4289 Merge current branch v3.7 into master 2026-05-27 14:51:50 +02:00
faukah eec68dce06 flake.nix: cleanup, refactor 2026-05-20 15:44:06 +02:00
Sheddy edd7d2eb33 Service-level Middleware Documentation 2026-05-04 13:56:05 +02:00
mmatur f7c0fdea57 Merge branch v3.7 into master 2026-04-30 16:47:39 +02:00
mmatur 9893e89628 Merge branch v3.7 into master 2026-04-22 14:40:14 +02:00
romain 786f7192e1 Merge branch v3.7 into master 2026-04-09 11:46:50 +02:00
kevinpollet 174e5d8111 Merge branch v3.7 into master 2026-03-26 14:05:54 +01:00
55 changed files with 3202 additions and 634 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ on:
jobs:
sync:
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 30
permissions:
packages: write
contents: read
+3
View File
@@ -348,6 +348,9 @@ linters:
text: 'appendAssign: append result not assigned to the same slice'
linters:
- gocritic
- path: pkg/server/conncontext.go
linters:
- fatcontext
paths:
- pkg/provider/kubernetes/crd/generated/
+57
View File
@@ -1,3 +1,60 @@
## [v3.7.5](https://github.com/traefik/traefik/tree/v3.7.5) (2026-06-10)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.4...v3.7.5)
**Bug fixes:**
- **[k8s/ingress-nginx]** Skip ingress when auth-secret resolution fails ([#13323](https://github.com/traefik/traefik/pull/13323) @gndz07)
- **[k8s/ingress-nginx]** Pass endpointslice fencing on ingress-nginx provider ([#13290](https://github.com/traefik/traefik/pull/13290) @Learloj)
- **[k8s/gatewayapi]** Reject cross-provider references with backendRefs.namespace ([#13322](https://github.com/traefik/traefik/pull/13322) @youkoulayley)
- **[server]** Bump to github.com/pires/go-proxyproto v0.12.0 ([#13313](https://github.com/traefik/traefik/pull/13313) @timschumi)
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
## [v3.6.21](https://github.com/traefik/traefik/tree/v3.6.21) (2026-06-10)
[All Commits](https://github.com/traefik/traefik/compare/v3.6.20...v3.6.21)
**Bug fixes:**
- **[k8s/gatewayapi]** Reject cross-provider references with backendRefs.namespace ([#13322](https://github.com/traefik/traefik/pull/13322) @youkoulayley)
- **[server]** Bump to github.com/pires/go-proxyproto v0.12.0 ([#13313](https://github.com/traefik/traefik/pull/13313) @timschumi)
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
## [v2.11.50](https://github.com/traefik/traefik/tree/v2.11.50) (2026-06-10)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.49...v2.11.50)
**Bug fixes:**
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
## [v3.7.4](https://github.com/traefik/traefik/tree/v3.7.4) (2026-06-05)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.3...v3.7.4)
**Bug fixes:**
- **[middleware]** Fix redis write timeout option configuration ([#13273](https://github.com/traefik/traefik/pull/13273) @bzyy1024)
- **[webui]** Bump react-router and jsdom ([#13301](https://github.com/traefik/traefik/pull/13301) @gndz07)
- **[k8s/gatewayapi]** Fix BackendTLSPolicy status update ([#13306](https://github.com/traefik/traefik/pull/13306) @AnatoleLucet)
- **[http3]** Bump github.com/quic-go/quic-go to v0.59.1 ([#13300](https://github.com/traefik/traefik/pull/13300) @rtribotte)
- **[webui]** Bump axios to v1.17.0 ([#13299](https://github.com/traefik/traefik/pull/13299) @gndz07)
- **[tls]** Fix snicheck with keepalive ([#13305](https://github.com/traefik/traefik/pull/13305) @juliens)
## [v3.6.20](https://github.com/traefik/traefik/tree/v3.6.20) (2026-06-05)
[All Commits](https://github.com/traefik/traefik/compare/v3.6.19...v3.6.20)
**Bug fixes:**
- **[middleware]** Fix redis write timeout option configuration ([#13273](https://github.com/traefik/traefik/pull/13273) @bzyy1024)
- **[webui]** Bump react-router and jsdom ([#13301](https://github.com/traefik/traefik/pull/13301) @gndz07)
- **[k8s/gatewayapi]** Fix BackendTLSPolicy status update ([#13306](https://github.com/traefik/traefik/pull/13306) @AnatoleLucet)
- **[http3]** Bump github.com/quic-go/quic-go to v0.59.1 ([#13300](https://github.com/traefik/traefik/pull/13300) @rtribotte)
- **[webui]** Bump axios to v1.17.0 ([#13299](https://github.com/traefik/traefik/pull/13299) @gndz07)
- **[tls]** Fix snicheck with keepalive ([#13305](https://github.com/traefik/traefik/pull/13305) @juliens)
## [v2.11.49](https://github.com/traefik/traefik/tree/v2.11.49) (2026-06-05)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.48...v2.11.49)
**Bug fixes:**
- **[http3]** Bump github.com/quic-go/quic-go to v0.59.1 ([#13300](https://github.com/traefik/traefik/pull/13300) @rtribotte)
- **[webui]** Bump axios to v1.17.0 ([#13299](https://github.com/traefik/traefik/pull/13299) @gndz07)
- **[tls]** Fix snicheck with keepalive ([#13305](https://github.com/traefik/traefik/pull/13305) @juliens)
## [v3.7.3](https://github.com/traefik/traefik/tree/v3.7.3) (2026-06-04)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.1...v3.7.3)
+6
View File
@@ -93,6 +93,12 @@ For a complete list of supported annotations and behavioral differences, see the
The Kubernetes Ingress NGINX provider requires **Traefik v3.6.2 or later**.
!!! info "Legacy Scheme Headers"
If your applications still depend on ingress-nginx's legacy `X-Forwarded-Scheme` or `X-Scheme` headers,
enable `entryPoints.<name>.forwardedHeaders.addXForwardedSchemeHeaders=true` on the entrypoints that receive this traffic.
This keeps `X-Forwarded-Proto` unchanged and restores the compatibility headers at the entrypoint level for every provider.
---
## Prerequisites
@@ -85,6 +85,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-entrypoints-name-address" href="#opt-entrypoints-name-address" title="#opt-entrypoints-name-address">entrypoints._name_.address</a> | Entry point address. | |
| <a id="opt-entrypoints-name-allowacmebypass" href="#opt-entrypoints-name-allowacmebypass" title="#opt-entrypoints-name-allowacmebypass">entrypoints._name_.allowacmebypass</a> | Enables handling of ACME TLS and HTTP challenges with custom routers. | false |
| <a id="opt-entrypoints-name-asdefault" href="#opt-entrypoints-name-asdefault" title="#opt-entrypoints-name-asdefault">entrypoints._name_.asdefault</a> | Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. | false |
| <a id="opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders" href="#opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders" title="#opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders">entrypoints._name_.forwardedheaders.addxforwardedschemeheaders</a> | Add the X-Forwarded-Scheme and X-Scheme headers. | false |
| <a id="opt-entrypoints-name-forwardedheaders-connection" href="#opt-entrypoints-name-forwardedheaders-connection" title="#opt-entrypoints-name-forwardedheaders-connection">entrypoints._name_.forwardedheaders.connection</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | |
| <a id="opt-entrypoints-name-forwardedheaders-insecure" href="#opt-entrypoints-name-forwardedheaders-insecure" title="#opt-entrypoints-name-forwardedheaders-insecure">entrypoints._name_.forwardedheaders.insecure</a> | Trust all forwarded headers. | false |
| <a id="opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" href="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" title="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor">entrypoints._name_.forwardedheaders.notappendxforwardedfor</a> | Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled). | false |
@@ -395,6 +396,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetesingress-labelselector" href="#opt-providers-kubernetesingress-labelselector" title="#opt-providers-kubernetesingress-labelselector">providers.kubernetesingress.labelselector</a> | Kubernetes Ingress label selector to use. | |
| <a id="opt-providers-kubernetesingress-namespaces" href="#opt-providers-kubernetesingress-namespaces" title="#opt-providers-kubernetesingress-namespaces">providers.kubernetesingress.namespaces</a> | Kubernetes namespaces. | |
| <a id="opt-providers-kubernetesingress-nativelbbydefault" href="#opt-providers-kubernetesingress-nativelbbydefault" title="#opt-providers-kubernetesingress-nativelbbydefault">providers.kubernetesingress.nativelbbydefault</a> | Defines whether to use Native Kubernetes load-balancing mode by default. | false |
| <a id="opt-providers-kubernetesingress-reportnodeinternalips" href="#opt-providers-kubernetesingress-reportnodeinternalips" title="#opt-providers-kubernetesingress-reportnodeinternalips">providers.kubernetesingress.reportnodeinternalips</a> | Report node internal IPs in Ingress status. | false |
| <a id="opt-providers-kubernetesingress-strictprefixmatching" href="#opt-providers-kubernetesingress-strictprefixmatching" title="#opt-providers-kubernetesingress-strictprefixmatching">providers.kubernetesingress.strictprefixmatching</a> | Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). | false |
| <a id="opt-providers-kubernetesingress-throttleduration" href="#opt-providers-kubernetesingress-throttleduration" title="#opt-providers-kubernetesingress-throttleduration">providers.kubernetesingress.throttleduration</a> | Ingress refresh throttle duration | 0 |
| <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. | |
@@ -89,6 +89,7 @@ additionalArguments:
| <a id="opt-asDefault" href="#opt-asDefault" title="#opt-asDefault">`asDefault`</a> | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
| <a id="opt-allowACMEByPass" href="#opt-allowACMEByPass" title="#opt-allowACMEByPass">`allowACMEByPass`</a> | Enables handling of ACME TLS and HTTP challenges with custom routers instead of the internal ACME router. | false | No |
| <a id="opt-forwardedHeaders-connection" href="#opt-forwardedHeaders-connection" title="#opt-forwardedHeaders-connection">`forwardedHeaders.`<br />`connection`</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | false | No |
| <a id="opt-forwardedHeaders-addXForwardedSchemeHeaders" href="#opt-forwardedHeaders-addXForwardedSchemeHeaders" title="#opt-forwardedHeaders-addXForwardedSchemeHeaders">`forwardedHeaders.`<br />`addXForwardedSchemeHeaders`</a> | Add the compatibility headers `X-Forwarded-Scheme` and `X-Scheme`. | false | No |
| <a id="opt-forwardedHeaders-insecure" href="#opt-forwardedHeaders-insecure" title="#opt-forwardedHeaders-insecure">`forwardedHeaders.`<br />`insecure`</a> | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
| <a id="opt-forwardedHeaders-trustedIPs" href="#opt-forwardedHeaders-trustedIPs" title="#opt-forwardedHeaders-trustedIPs">`forwardedHeaders.`<br />`trustedIPs`</a> | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
| <a id="opt-forwardedHeaders-notAppendXForwardedFor" href="#opt-forwardedHeaders-notAppendXForwardedFor" title="#opt-forwardedHeaders-notAppendXForwardedFor">`forwardedHeaders.`<br />`notAppendXForwardedFor`</a> | When set to `true`, Traefik will not append the client's `RemoteAddr` to the `X-Forwarded-For` header. The existing header is preserved as-is. If no `X-Forwarded-For` header exists, none will be added. | false | No |
@@ -392,6 +393,37 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward
--entryPoints.web.forwardedHeaders.connection=foobar
```
??? info "`forwardedHeaders.addXForwardedSchemeHeaders`"
Add the compatibility headers `X-Forwarded-Scheme` and `X-Scheme` next to `X-Forwarded-Proto`.
This is primarily useful when migrating from ingress-nginx and your applications still rely on these legacy headers.
When enabled, these compatibility headers follow the same value as `X-Forwarded-Proto`.
```yaml tab="File (YAML)"
## Static configuration
entryPoints:
websecure:
address: ":443"
forwardedHeaders:
addXForwardedSchemeHeaders: true
```
```toml tab="File (TOML)"
## Static configuration
[entryPoints]
[entryPoints.websecure]
address = ":443"
[entryPoints.websecure.forwardedHeaders]
addXForwardedSchemeHeaders = true
```
```bash tab="CLI"
## Static configuration
--entryPoints.websecure.address=:443
--entryPoints.websecure.forwardedHeaders.addXForwardedSchemeHeaders=true
```
### HTTP3
As HTTP/3 actually uses UDP, when Traefik is configured with a TCP `entryPoint`
@@ -58,12 +58,13 @@ which in turn creates the resulting routers, services, handlers, etc.
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-hostname" href="#opt-providers-kubernetesIngress-ingressEndpoint-hostname" title="#opt-providers-kubernetesIngress-ingressEndpoint-hostname">`providers.kubernetesIngress.`<br />`ingressEndpoint.hostname`</a> | Hostname used for Kubernetes Ingress endpoints. | "" | No |
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-ip" href="#opt-providers-kubernetesIngress-ingressEndpoint-ip" title="#opt-providers-kubernetesIngress-ingressEndpoint-ip">`providers.kubernetesIngress.`<br />`ingressEndpoint.ip`</a> | This IP will get copied to the Ingress `status.loadbalancer.ip`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-publishedService" href="#opt-providers-kubernetesIngress-ingressEndpoint-publishedService" title="#opt-providers-kubernetesIngress-ingressEndpoint-publishedService">`providers.kubernetesIngress.`<br />`ingressEndpoint.publishedService`</a> | The Kubernetes service to copy status from.<br />More information [here](#ingressendpointpublishedservice). | "" | No |
| <a id="opt-providers-kubernetesIngress-reportNodeInternalIPs" href="#opt-providers-kubernetesIngress-reportNodeInternalIPs" title="#opt-providers-kubernetesIngress-reportNodeInternalIPs">`providers.kubernetesIngress.reportNodeInternalIPs`</a> | Report node internal IPs in Ingress status.<br />Incompatible with `ingressEndpoint` and `disableClusterScopeResources`.<br />More information [here](#reportnodeinternalips). | false | No |
| <a id="opt-providers-kubernetesIngress-throttleDuration" href="#opt-providers-kubernetesIngress-throttleDuration" title="#opt-providers-kubernetesIngress-throttleDuration">`providers.kubernetesIngress.throttleDuration`</a> | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.<br />This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.<br />If empty, every event is caught. | 0s | No |
| <a id="opt-providers-kubernetesIngress-allowEmptyServices" href="#opt-providers-kubernetesIngress-allowEmptyServices" title="#opt-providers-kubernetesIngress-allowEmptyServices">`providers.kubernetesIngress.allowEmptyServices`</a> | Allows creating a route to reach a service that has no endpoint available.<br />It allows Traefik to handle the requests and responses targeting this service (applying middleware or observability operations) before returning a `503` HTTP Status. | false | No |
| <a id="opt-providers-kubernetesIngress-allowExternalNameServices" href="#opt-providers-kubernetesIngress-allowExternalNameServices" title="#opt-providers-kubernetesIngress-allowExternalNameServices">`providers.kubernetesIngress.allowExternalNameServices`</a> | Allows the `Ingress` to reference ExternalName services. | false | No |
| <a id="opt-providers-kubernetesIngress-crossProviderNamespaces" href="#opt-providers-kubernetesIngress-crossProviderNamespaces" title="#opt-providers-kubernetesIngress-crossProviderNamespaces">`providers.kubernetesIngress.crossProviderNamespaces`</a> | List of namespaces from which Ingresses or Services are allowed to use `traefik.ingress.kubernetes.io/router.middlewares`, `traefik.ingress.kubernetes.io/router.tls.options`, or `traefik.ingress.kubernetes.io/service.serverstransport` annotations.<br />When unset, all namespaces are allowed. When set to `[]`, every cross-provider reference is rejected. | [] | No |
| <a id="opt-providers-kubernetesIngress-nativeLBByDefault" href="#opt-providers-kubernetesIngress-nativeLBByDefault" title="#opt-providers-kubernetesIngress-nativeLBByDefault">`providers.kubernetesIngress.nativeLBByDefault`</a> | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik for every `Ingress` by default.<br />It can be overridden in the [`Service`](../../../../reference/routing-configuration/kubernetes/crd/http/service.md#opt-nativeLB) | false | No |
| <a id="opt-providers-kubernetesIngress-disableClusterScopeResources" href="#opt-providers-kubernetesIngress-disableClusterScopeResources" title="#opt-providers-kubernetesIngress-disableClusterScopeResources">`providers.kubernetesIngress.disableClusterScopeResources`</a> | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).<br />By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.<br />Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).<br />This will also prevent from using the `NodePortLB` options on services. | false | No |
| <a id="opt-providers-kubernetesIngress-disableClusterScopeResources" href="#opt-providers-kubernetesIngress-disableClusterScopeResources" title="#opt-providers-kubernetesIngress-disableClusterScopeResources">`providers.kubernetesIngress.disableClusterScopeResources`</a> | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).<br />By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.<br />Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).<br />This will also prevent from using the `NodePortLB` options on services and is incompatible with `reportNodeInternalIPs`. | false | No |
| <a id="opt-providers-kubernetesIngress-strictPrefixMatching" href="#opt-providers-kubernetesIngress-strictPrefixMatching" title="#opt-providers-kubernetesIngress-strictPrefixMatching">`providers.kubernetesIngress.strictPrefixMatching`</a> | Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). For example, a PathPrefix of `/foo` will match `/foo`, `/foo/`, and `/foo/bar` but not `/foobar`. | false | No |
<!-- markdownlint-enable MD013 -->
@@ -138,6 +139,31 @@ providers:
--providers.kubernetesingress.ingressendpoint.publishedservice=namespace/foo-service
```
### `reportNodeInternalIPs`
When set to `true`, Traefik reports the internal IPs of all nodes in the cluster into the `status.loadBalancer.ingress` field of each managed Ingress resource.
This is the equivalent of ingress-nginx's `--report-node-internal-ip-address` flag and is the recommended approach for bare-metal Kubernetes deployments where Traefik runs as a DaemonSet without a cloud LoadBalancer or MetalLB.
This option requires cluster-scope access to Node resources and is mutually exclusive with `ingressEndpoint` and `disableClusterScopeResources`.
```yaml tab="File (YAML)"
providers:
kubernetesIngress:
reportNodeInternalIPs: true
# ...
```
```toml tab="File (TOML)"
[providers.kubernetesIngress]
reportNodeInternalIPs = true
# ...
```
```bash tab="CLI"
--providers.kubernetesingress.reportnodeinternalips=true
```
## Routing Configuration
See the dedicated section in [routing](../../../../reference/routing-configuration/kubernetes/ingress.md).
@@ -41,6 +41,7 @@ creating the corresponding routers, services, middlewares, and other components
Important differences in default behaviors:
- **Request buffering**: NGINX enables `proxy-request-buffering` by default, while Traefik requires explicit opt-in via the provider's `proxyRequestBuffering` option.
- **Legacy scheme headers**: If your applications depend on `X-Forwarded-Scheme` or `X-Scheme`, enable `entryPoints.<name>.forwardedHeaders.addXForwardedSchemeHeaders=true` on the relevant entrypoints.
To ensure consistent behavior during migration,
review and configure Traefik's provider-level options to match your current NGINX ConfigMap settings.
@@ -50,6 +50,7 @@
insecure = true
trustedIPs = ["foobar", "foobar"]
connection = ["foobar", "foobar"]
addXForwardedSchemeHeaders = true
[entryPoints.EntryPoint0.http]
middlewares = ["foobar", "foobar"]
encodeQuerySemicolons = true
@@ -61,6 +61,7 @@ entryPoints:
connection:
- foobar
- foobar
addXForwardedSchemeHeaders: true
http:
redirections:
entryPoint:
Generated
+7 -44
View File
@@ -1,37 +1,16 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1772963539,
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
"type": "github"
"lastModified": 1778036283,
"narHash": "sha256-GW2cEd/cLcVbbCes8iQuoY2qGIeCA7UiaD351hpkXfI=",
"rev": "ed67bc86e84e51d4a88e73c7fd36006dc876476f",
"type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre993032.ed67bc86e84e/nixexprs.tar.xz"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
"type": "tarball",
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
}
},
"nixpkgs-golangci": {
@@ -68,26 +47,10 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"nixpkgs-golangci": "nixpkgs-golangci",
"nixpkgs-kct": "nixpkgs-kct"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+26 -25
View File
@@ -3,7 +3,7 @@
inputs = {
# Main nixpkgs (used for gnused)
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
# Pinned nixpkgs for kubernetes-controller-tools
# Search: https://www.nixhub.io/packages/kubernetes-controller-tools
@@ -12,33 +12,34 @@
# Pinned nixpkgs for golangci-lint
# Search: https://www.nixhub.io/packages/golangci-lint
nixpkgs-golangci.url = "github:NixOS/nixpkgs/80d901ec0377e19ac3f7bb8c035201e2e098cc97";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, nixpkgs-kct, nixpkgs-golangci, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
};
outputs =
{
nixpkgs,
nixpkgs-kct,
nixpkgs-golangci,
...
}:
let
inherit (nixpkgs.lib) genAttrs;
forEachSystem = genAttrs nixpkgs.lib.systems.flakeExposed;
pkgs-kct = import nixpkgs-kct {
inherit system;
};
pkgs-golangci = import nixpkgs-golangci {
inherit system;
};
in
{
devShells.default = pkgs.mkShell {
pkgsForEach = nixpkgs.legacyPackages;
pkgsKctForEach = nixpkgs-kct.legacyPackages;
pkgsGolangCiForEach = nixpkgs-golangci.legacyPackages;
in
{
devShells = forEachSystem (system: {
default = pkgsForEach.${system}.mkShell {
packages = [
pkgs-kct.kubernetes-controller-tools
pkgs.gnused
pkgs-golangci.golangci-lint
pkgsForEach.${system}.gnused
pkgsKctForEach.${system}.kubernetes-controller-tools
pkgsGolangCiForEach.${system}.golangci-lint
];
};
}
);
}
});
formatter = forEachSystem (system: pkgsForEach.${system}.nixfmt);
};
}
+2 -2
View File
@@ -53,11 +53,11 @@ require (
github.com/moby/moby/api v1.54.1
github.com/moby/moby/client v0.4.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pires/go-proxyproto v0.8.1
github.com/pires/go-proxyproto v0.12.0
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo.
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/quic-go/quic-go v0.59.0
github.com/quic-go/quic-go v0.59.1
github.com/redis/go-redis/v9 v9.8.0
github.com/rs/zerolog v1.33.0
github.com/sirupsen/logrus v1.9.4
+4 -4
View File
@@ -1748,8 +1748,8 @@ github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -1811,8 +1811,8 @@ github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05Zp
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
@@ -0,0 +1,101 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
[entryPoints.websecure]
address = ":4443"
[entryPoints.websecure2]
address = ":4444"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
# --- Same host, same options, same entryPoint: no conflict, the options are applied. ---
[http.routers.same-1]
rule = "Host(`same.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.same-1.tls]
options = "tls12"
[http.routers.same-2]
rule = "Host(`same.www.snitest.com`) && PathPrefix(`/same`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.same-2.tls]
options = "tls12"
# --- Same host, different options, same entryPoint: conflict, fallback to default options. ---
[http.routers.conflict-1]
rule = "Host(`conflict.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.conflict-1.tls]
options = "tls12"
[http.routers.conflict-2]
rule = "Host(`conflict.www.snitest.com`) && PathPrefix(`/conflict`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.conflict-2.tls]
options = "tls13"
# --- Same host, different options, different entryPoints: no conflict, each entryPoint keeps its own options. ---
[http.routers.cross-ep1]
rule = "Host(`cross.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.cross-ep1.tls]
options = "tls12"
[http.routers.cross-ep2]
rule = "Host(`cross.www.snitest.com`)"
entryPoints = ["websecure2"]
service = "service1"
[http.routers.cross-ep2.tls]
options = "tls13"
# --- Domain fronting (Host header != SNI): same options follow the header, different options are rejected. ---
[http.routers.df-a]
rule = "Host(`df-a.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.df-a.tls]
options = "tls12"
[http.routers.df-b]
rule = "Host(`df-b.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.df-b.tls]
options = "tls12"
[http.routers.df-c]
rule = "Host(`df-c.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.df-c.tls]
options = "tls13"
[http.services.service1]
[[http.services.service1.loadBalancer.servers]]
url = "http://127.0.0.1:9010"
[[tls.certificates]]
certFile = "fixtures/https/wildcard.www.snitest.com.cert"
keyFile = "fixtures/https/wildcard.www.snitest.com.key"
[tls.options]
[tls.options.tls12]
maxVersion = "VersionTLS12"
[tls.options.tls13]
minVersion = "VersionTLS13"
+148 -1
View File
@@ -415,7 +415,7 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions() {
assert.ErrorContains(s.T(), err, "tls: no supported versions satisfy MinVersion and MaxVersion")
// with unknown tls option
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("found different TLS options for routers on the same host, so using the default TLS options instead"))
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"))
require.NoError(s.T(), err)
}
@@ -1262,6 +1262,153 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
}
}
// TestWithTLSOptionsConflict checks how TLS options are resolved when several routers
// target the same host (SNI), across the different conflict situations:
// - same options on the same entryPoint: no conflict, the options are applied;
// - different options on the same entryPoint: conflict, fallback to the default options;
// - different options on different entryPoints: no conflict, each entryPoint keeps its
// own options (they are selected independently on each listener);
// - domain fronting (Host header != SNI): allowed when both resolve to the same options,
// rejected with a 421 otherwise.
//
// The effective TLS options are probed through the negotiated TLS version: the "tls12"
// options cap the version to TLS 1.2, while the "tls13" options require at least TLS 1.3.
func (s *HTTPSSuite) TestWithTLSOptionsConflict() {
backend := startTestServer("9010", http.StatusOK, "server1")
defer backend.Close()
file := s.adaptFile("fixtures/https/https_tls_options_conflict.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
// wait for Traefik
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`cross.www.snitest.com`)"))
require.NoError(s.T(), err)
testCases := []struct {
desc string
addr string // entryPoint address to reach
hostHeader string
serverName string // SNI
minVersion uint16 // 0 means the crypto/tls library default
maxVersion uint16 // 0 means the crypto/tls library default
// expectHandshakeError is set when the TLS handshake itself is expected to fail
// (i.e. the probed options reject the client's TLS version). Otherwise
// expectedStatusCode is asserted on the HTTP response.
expectHandshakeError bool
expectedStatusCode int
}{
// Same host, same options, same entryPoint: no conflict, the "tls12" options are applied.
{
desc: "same options / same entryPoint: TLS 1.2 client is accepted",
addr: "127.0.0.1:4443",
hostHeader: "same.www.snitest.com",
serverName: "same.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
{
desc: "same options / same entryPoint: TLS 1.3 client is rejected (maxVersion TLS1.2 enforced)",
addr: "127.0.0.1:4443",
hostHeader: "same.www.snitest.com",
serverName: "same.www.snitest.com",
minVersion: tls.VersionTLS13,
expectHandshakeError: true,
},
// Same host, different options, same entryPoint: conflict, both routers fall back to the default options.
{
desc: "conflicting options / same entryPoint: TLS 1.3 client is accepted (default options used)",
addr: "127.0.0.1:4443",
hostHeader: "conflict.www.snitest.com",
serverName: "conflict.www.snitest.com",
minVersion: tls.VersionTLS13,
expectedStatusCode: http.StatusOK,
},
{
desc: "conflicting options / same entryPoint: TLS 1.2 client is accepted (default options used)",
addr: "127.0.0.1:4443",
hostHeader: "conflict.www.snitest.com",
serverName: "conflict.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
// Same host, different options, different entryPoints: no conflict, each entryPoint keeps its own options.
{
desc: "different entryPoints: websecure keeps tls12, TLS 1.2 client is accepted",
addr: "127.0.0.1:4443",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
{
desc: "different entryPoints: websecure keeps tls12, TLS 1.3 client is rejected",
addr: "127.0.0.1:4443",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
minVersion: tls.VersionTLS13,
expectHandshakeError: true,
},
{
desc: "different entryPoints: websecure2 keeps tls13, TLS 1.3 client is accepted",
addr: "127.0.0.1:4444",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
minVersion: tls.VersionTLS13,
expectedStatusCode: http.StatusOK,
},
{
desc: "different entryPoints: websecure2 keeps tls13, TLS 1.2 client is rejected",
addr: "127.0.0.1:4444",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectHandshakeError: true,
},
// Domain fronting (Host header != SNI) on the same entryPoint.
{
desc: "domain fronting / same options: request follows the Host header (200)",
addr: "127.0.0.1:4443",
hostHeader: "df-a.www.snitest.com",
serverName: "df-b.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
{
desc: "domain fronting / different options: request is misdirected (421)",
addr: "127.0.0.1:4443",
hostHeader: "df-a.www.snitest.com",
serverName: "df-c.www.snitest.com",
minVersion: tls.VersionTLS13,
expectedStatusCode: http.StatusMisdirectedRequest,
},
}
for _, test := range testCases {
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: test.serverName,
MinVersion: test.minVersion,
MaxVersion: test.maxVersion,
}
req, err := http.NewRequest(http.MethodGet, "https://"+test.addr+"/", nil)
require.NoError(s.T(), err)
req.Host = test.hostHeader
if test.expectHandshakeError {
_, err = (&http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}).Do(req)
assert.ErrorContains(s.T(), err, "tls:", "test %q should fail the TLS handshake", test.desc)
continue
}
err = try.RequestWithTransport(req, 2*time.Second, &http.Transport{TLSClientConfig: tlsConfig}, try.StatusCodeIs(test.expectedStatusCode))
assert.NoError(s.T(), err, "test %q failed with: %v", test.desc, err)
}
}
// TestWithInvalidTLSOption verifies the behavior when using an invalid tlsOption configuration.
func (s *HTTPSSuite) TestWithInvalidTLSOption() {
backend := startTestServer("9010", http.StatusOK, "server1")
+3 -3
View File
@@ -949,7 +949,7 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
s.traefikCmd(withConfigFile(file))
// All errors
err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host, so using the default TLS options instead"]`))
err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"]`))
require.NoError(s.T(), err)
// router3 has an error because it uses an unknown entrypoint
@@ -957,11 +957,11 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
require.NoError(s.T(), err)
// router4 is enabled, but in warning state because its tls options conf was messed up
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router4@file", 1000*time.Millisecond, try.BodyContains(`"status":"warning"`))
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/websecure-conflicted-router4@file", 1000*time.Millisecond, try.BodyContains(`"status":"warning"`))
require.NoError(s.T(), err)
// router5 is disabled because its middleware conf is broken
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router5@file", 1000*time.Millisecond, try.BodyContains())
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/websecure-conflicted-router5@file", 1000*time.Millisecond, try.BodyContains())
require.NoError(s.T(), err)
}
+5 -4
View File
@@ -150,10 +150,11 @@ type TLSConfig struct {
// ForwardedHeaders Trust client forwarding headers.
type ForwardedHeaders struct {
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
AddXForwardedSchemeHeaders bool `description:"Add the X-Forwarded-Scheme and X-Scheme headers." json:"addXForwardedSchemeHeaders,omitempty" toml:"addXForwardedSchemeHeaders,omitempty" yaml:"addXForwardedSchemeHeaders,omitempty" export:"true"`
}
// ProxyProtocol contains Proxy-Protocol configuration.
@@ -18,12 +18,14 @@ const (
XForwardedFor = "X-Forwarded-For"
XForwardedHost = "X-Forwarded-Host"
XForwardedPort = "X-Forwarded-Port"
xForwardedScheme = "X-Forwarded-Scheme"
xForwardedServer = "X-Forwarded-Server"
XForwardedURI = "X-Forwarded-Uri"
XForwardedMethod = "X-Forwarded-Method"
XForwardedPrefix = "X-Forwarded-Prefix"
xForwardedTLSClientCert = "X-Forwarded-Tls-Client-Cert"
xForwardedTLSClientCertInfo = "X-Forwarded-Tls-Client-Cert-Info"
xScheme = "X-Scheme"
xRealIP = "X-Real-Ip"
connection = "Connection"
upgrade = "Upgrade"
@@ -34,6 +36,7 @@ const (
// that Go's HTTP server preserves (e.g. X_Forwarded_Proto).
var XHeadersSet = map[string]struct{}{
XForwardedProto: {},
xForwardedScheme: {},
XForwardedFor: {},
XForwardedHost: {},
XForwardedPort: {},
@@ -43,6 +46,7 @@ var XHeadersSet = map[string]struct{}{
XForwardedPrefix: {},
xForwardedTLSClientCert: {},
xForwardedTLSClientCertInfo: {},
xScheme: {},
xRealIP: {},
}
@@ -70,17 +74,18 @@ func isManagedXHeader(key string) bool {
// Unless insecure is set,
// it first removes all the existing values for those headers if the remote address is not one of the trusted ones.
type XForwarded struct {
insecure bool
trustedIPs []string
connectionHeaders []string
notAppendXForwardedFor bool
ipChecker *ip.Checker
next http.Handler
hostname string
insecure bool
trustedIPs []string
connectionHeaders []string
notAppendXForwardedFor bool
addXForwardedSchemeHeaders bool
ipChecker *ip.Checker
next http.Handler
hostname string
}
// NewXForwarded creates a new XForwarded.
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, next http.Handler) (*XForwarded, error) {
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, addXForwardedSchemeHeaders bool, next http.Handler) (*XForwarded, error) {
var ipChecker *ip.Checker
if len(trustedIPs) > 0 {
var err error
@@ -101,13 +106,14 @@ func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []strin
}
return &XForwarded{
insecure: insecure,
trustedIPs: trustedIPs,
connectionHeaders: canonicalConnectionHeaders,
notAppendXForwardedFor: notAppendXForwardedFor,
ipChecker: ipChecker,
next: next,
hostname: hostname,
insecure: insecure,
trustedIPs: trustedIPs,
connectionHeaders: canonicalConnectionHeaders,
notAppendXForwardedFor: notAppendXForwardedFor,
addXForwardedSchemeHeaders: addXForwardedSchemeHeaders,
ipChecker: ipChecker,
next: next,
hostname: hostname,
}, nil
}
@@ -168,6 +174,12 @@ func (x *XForwarded) rewrite(outreq *http.Request) {
unsafeHeader(outreq.Header).Set(XForwardedPort, forwardedPort(outreq))
}
if x.addXForwardedSchemeHeaders {
scheme := unsafeHeader(outreq.Header).Get(XForwardedProto)
unsafeHeader(outreq.Header).Set(xForwardedScheme, scheme)
unsafeHeader(outreq.Header).Set(xScheme, scheme)
}
if xfHost := unsafeHeader(outreq.Header).Get(XForwardedHost); xfHost == "" && outreq.Host != "" {
unsafeHeader(outreq.Header).Set(XForwardedHost, outreq.Host)
}
@@ -17,12 +17,14 @@ func TestServeHTTP(t *testing.T) {
insecure bool
trustedIps []string
connectionHeaders []string
addSchemeHeaders bool
incomingHeaders map[string][]string
remoteAddr string
expectedHeaders map[string]string
tls bool
websocket bool
host string
absentHeaders []string
}{
{
desc: "all Empty",
@@ -230,6 +232,24 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "https",
},
},
{
desc: "xForwardedScheme headers with tls",
tls: true,
addSchemeHeaders: true,
expectedHeaders: map[string]string{
XForwardedProto: "https",
xForwardedScheme: "https",
xScheme: "https",
},
},
{
desc: "xForwardedScheme headers disabled keeps legacy headers absent",
tls: true,
expectedHeaders: map[string]string{
XForwardedProto: "https",
},
absentHeaders: []string{xForwardedScheme, xScheme},
},
{
desc: "xForwardedProto with websocket",
tls: false,
@@ -238,6 +258,16 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "ws",
},
},
{
desc: "xForwardedScheme headers with websocket",
websocket: true,
addSchemeHeaders: true,
expectedHeaders: map[string]string{
XForwardedProto: "ws",
xForwardedScheme: "ws",
xScheme: "ws",
},
},
{
desc: "xForwardedProto with websocket and tls",
tls: true,
@@ -246,6 +276,17 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "wss",
},
},
{
desc: "xForwardedScheme headers with websocket and tls",
tls: true,
websocket: true,
addSchemeHeaders: true,
expectedHeaders: map[string]string{
XForwardedProto: "wss",
xForwardedScheme: "wss",
xScheme: "wss",
},
},
{
desc: "xForwardedProto with websocket and tls and already x-forwarded-proto with wss",
tls: true,
@@ -257,6 +298,21 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "wss",
},
},
{
desc: "xForwardedScheme headers overwrite in insecure mode",
insecure: true,
addSchemeHeaders: true,
incomingHeaders: map[string][]string{
XForwardedProto: {"https"},
xForwardedScheme: {"external-https"},
xScheme: {"external-https"},
},
expectedHeaders: map[string]string{
XForwardedProto: "https",
xForwardedScheme: "https",
xScheme: "https",
},
},
{
desc: "xForwardedPort with explicit port",
host: "foo.com:8080",
@@ -643,7 +699,7 @@ func TestServeHTTP(t *testing.T) {
}
}
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false,
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false, test.addSchemeHeaders,
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
require.NoError(t, err)
@@ -656,6 +712,10 @@ func TestServeHTTP(t *testing.T) {
for k, v := range test.expectedHeaders {
assert.Equal(t, v, req.Header.Get(k))
}
for _, header := range test.absentHeaders {
assert.NotContains(t, req.Header, http.CanonicalHeaderKey(header))
}
})
}
}
@@ -782,7 +842,7 @@ func TestConnection(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, nil)
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, false, nil)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "https://localhost", nil)
+1 -1
View File
@@ -49,7 +49,7 @@ func newRedisLimiter(ctx context.Context, rate rate.Limit, burst int64, maxDelay
}
if config.Redis.WriteTimeout != nil {
if *config.Redis.ReadTimeout > 0 {
if *config.Redis.WriteTimeout > 0 {
options.WriteTimeout = time.Duration(*config.Redis.WriteTimeout)
} else {
options.WriteTimeout = -1
+2 -3
View File
@@ -721,9 +721,8 @@ func (c *clientWrapper) UpdateBackendTLSPolicyStatus(ctx context.Context, policy
ancestorStatuses := make([]gatev1.PolicyAncestorStatus, len(status.Ancestors))
copy(ancestorStatuses, status.Ancestors)
// keep statuses added by other gateway controllers,
// and statuses for Traefik gateway controller but not for the same Gateway as the one in parameter (AncestorRef).
for _, ancestorStatus := range currentPolicy.Status.Ancestors {
// Keep statuses added by other gateway controllers.
if ancestorStatus.ControllerName != controllerName {
ancestorStatuses = append(ancestorStatuses, ancestorStatus)
continue
@@ -734,7 +733,7 @@ func (c *clientWrapper) UpdateBackendTLSPolicyStatus(ctx context.Context, policy
return fmt.Errorf("failed to update BackendTLSPolicy %s/%s status: PolicyAncestor statuses count exceeds 16", policy.Namespace, policy.Name)
}
// do not update status when nothing has changed.
// Do not update status when nothing has changed.
if policyAncestorStatusesEqual(currentPolicy.Status.Ancestors, ancestorStatuses) {
return nil
}
@@ -0,0 +1,108 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: web
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: GRPCRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: GRPCRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: grpc-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- foo.com
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: backend-tls-policy
namespace: default
spec:
targetRefs:
- group: ""
kind: Service
name: whoami
validation:
hostname: whoami
caCertificateRefs:
- group: ""
kind: ConfigMap
name: ca-file
- group: core
kind: ConfigMap
name: ca-file-2
- group: ""
kind: Secret
name: ca-file
- group: core
kind: Secret
name: ca-file-2
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file
namespace: default
data:
ca.crt: "CA1"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: "CA2"
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file
namespace: default
data:
ca.crt: Q0ExLXNlY3JldA==
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: Q0EyLXNlY3JldA==
@@ -0,0 +1,60 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: GRPCRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: GRPCRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: grpc-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- foo.com
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: backend-tls-policy
namespace: default
spec:
targetRefs:
- group: core
kind: Service
name: whoami
validation:
hostname: whoami
wellKnownCACertificates: System
@@ -0,0 +1,55 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- matches:
- path:
type: Exact
value: /bar
backendRefs:
- weight: 1
group: traefik.io
kind: TraefikService
name: service@file
namespace: bar
port: 80
- name: whoami
port: 80
weight: 1
group: ""
kind: Service
@@ -0,0 +1,51 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tcp
protocol: TCP
port: 9000
allowedRoutes:
kinds:
- kind: TCPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: tcp-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- weight: 1
group: traefik.io
kind: TraefikService
name: service@file
namespace: bar
port: 9000
- name: whoamitcp
port: 9000
weight: 1
group: ""
kind: Service
@@ -0,0 +1,67 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9000
tls:
certificateRefs:
- kind: Secret
name: supersecret
group: ""
allowedRoutes:
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- weight: 1
group: traefik.io
kind: TraefikService
name: service@file
namespace: bar
port: 9000
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""
@@ -0,0 +1,124 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9001
hostname: foo.com
tls:
mode: Terminate # Default mode
certificateRefs:
- kind: Secret
name: supersecret
group: ""
allowedRoutes:
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: policy-1
namespace: default
spec:
targetRefs:
- group: ""
kind: Service
name: whoami
validation:
hostname: whoami
caCertificateRefs:
- group: ""
kind: ConfigMap
name: ca-file
- group: core
kind: ConfigMap
name: ca-file-2
- group: ""
kind: Secret
name: ca-file
- group: core
kind: Secret
name: ca-file-2
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file
namespace: default
data:
ca.crt: "CA1"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: "CA2"
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file
namespace: default
data:
ca.crt: Q0ExLXNlY3JldA==
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: Q0EyLXNlY3JldA==
@@ -0,0 +1,78 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9001
hostname: foo.com
tls:
mode: Terminate # Default mode
certificateRefs:
- kind: Secret
name: supersecret
group: ""
allowedRoutes:
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: policy-1
namespace: default
spec:
targetRefs:
- group: core
kind: Service
name: whoami
validation:
hostname: whoami
wellKnownCACertificates: System
+135 -38
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"net"
"regexp"
"slices"
"strconv"
"strings"
@@ -21,7 +22,7 @@ import (
)
// TODO: as described in the specification https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.GRPCRoute, we should check for hostname conflicts between HTTP and GRPC routes.
func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
routes, err := p.client.ListGRPCRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list GRPCRoutes")
@@ -39,9 +40,8 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
continue
}
var parentStatuses []gatev1.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := &gatev1.RouteParentStatus{
parentStatus := gatev1.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -77,7 +77,7 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
}
}
routeConf, resolveRefCondition := p.loadGRPCRoute(logger.WithContext(ctx), listener, route, hostnames)
routeConf, resolveRefCondition := p.loadGRPCRoute(logger.WithContext(ctx), listener, route, hostnames, statusReport)
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
}
@@ -85,23 +85,12 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
parentStatuses = append(parentStatuses, *parentStatus)
}
status := gatev1.GRPCRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateGRPCRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update GRPCRoute status")
statusReport.RecordGRPCRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
}
}
}
func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
@@ -168,7 +157,7 @@ func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener,
default:
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadGRPCService(conf, routerName, routeRule, route)
router.Service, serviceCondition = p.loadGRPCService(listener, conf, routerName, routeRule, route, statusReport)
if serviceCondition != nil {
condition = *serviceCondition
}
@@ -181,7 +170,7 @@ func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener,
return conf, condition
}
func (p *Provider) loadGRPCService(conf *dynamic.Configuration, routeKey string, routeRule gatev1.GRPCRouteRule, route *gatev1.GRPCRoute) (string, *metav1.Condition) {
func (p *Provider) loadGRPCService(listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.GRPCRouteRule, route *gatev1.GRPCRoute, statusReport *statusReport) (string, *metav1.Condition) {
name := routeKey + "-wrr"
if _, ok := conf.HTTP.Services[name]; ok {
return name, nil
@@ -190,7 +179,7 @@ func (p *Provider) loadGRPCService(conf *dynamic.Configuration, routeKey string,
var wrr dynamic.WeightedRoundRobin
var condition *metav1.Condition
for _, backendRef := range routeRule.BackendRefs {
svcName, svc, errCondition := p.loadGRPCBackendRef(route, backendRef)
svcName, svc, errCondition := p.loadGRPCBackendRef(listener, conf, route, backendRef, statusReport)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
condition = errCondition
@@ -219,7 +208,7 @@ func (p *Provider) loadGRPCService(conf *dynamic.Configuration, routeKey string,
return name, condition
}
func (p *Provider) loadGRPCBackendRef(route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef) (string, *dynamic.Service, *metav1.Condition) {
func (p *Provider) loadGRPCBackendRef(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef, statusReport *statusReport) (string, *dynamic.Service, *metav1.Condition) {
kind := ptr.Deref(backendRef.Kind, kindService)
group := groupCore
@@ -271,11 +260,16 @@ func (p *Provider) loadGRPCBackendRef(route *gatev1.GRPCRoute, backendRef gatev1
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr + "-grpc")
lb, errCondition := p.loadGRPCServers(namespace, route, backendRef)
lb, st, errCondition := p.loadGRPCServers(namespace, route, backendRef, listener, statusReport)
if errCondition != nil {
return serviceName, nil, errCondition
}
if st != nil {
lb.ServersTransport = serviceName
conf.HTTP.ServersTransports[serviceName] = st
}
return serviceName, &dynamic.Service{LoadBalancer: lb}, nil
}
@@ -325,10 +319,10 @@ func (p *Provider) loadGRPCMiddlewares(conf *dynamic.Configuration, namespace, r
return middlewareNames, nil
}
func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef) (*dynamic.ServersLoadBalancer, *metav1.Condition) {
func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef.BackendRef)
if err != nil {
return nil, &metav1.Condition{
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
@@ -338,26 +332,128 @@ func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, ba
}
}
if svcPort.Protocol != corev1.ProtocolTCP {
return nil, &metav1.Condition{
backendTLSPolicies, err := p.client.ListBackendTLSPoliciesForService(namespace, string(backendRef.Name))
if err != nil {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: only TCP protocol is supported", namespace, backendRef.Name),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot list BackendTLSPolicies for Service %s/%s: %s", namespace, string(backendRef.Name), err),
}
}
protocol, err := getGRPCServiceProtocol(svcPort)
if err != nil {
return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: only \"kubernetes.io/h2c\" and \"https\" appProtocol is supported", namespace, backendRef.Name),
// Sort BackendTLSPolicies by creation timestamp, then by name to match the BackendTLSPolicy requirements.
slices.SortStableFunc(backendTLSPolicies, func(a, b *gatev1.BackendTLSPolicy) int {
cmpTime := a.CreationTimestamp.Time.Compare(b.CreationTimestamp.Time)
if cmpTime == 0 {
return strings.Compare(a.Name, b.Name)
}
return cmpTime
})
var serversTransport *dynamic.ServersTransport
for _, policy := range backendTLSPolicies {
for _, targetRef := range policy.Spec.TargetRefs {
// Skip targetRefs that doesn't match the backendRef,
// since a BackendTLSPolicy can select multiple services.
if targetRef.Name != backendRef.Name {
continue
}
// Skip the targetRef if the sectionName doesn't match the backendRef port.
if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) {
continue
}
policyAncestorStatus := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{
Group: ptr.To(gatev1.Group(groupGateway)),
Kind: ptr.To(gatev1.Kind(kindGateway)),
Namespace: ptr.To(gatev1.Namespace(namespace)),
Name: gatev1.ObjectName(listener.GWName),
SectionName: ptr.To(gatev1.SectionName(listener.Name)),
},
ControllerName: controllerName,
}
// Multiple BackendTLSPolicies can match the same service port, meaning that there is a conflict.
if serversTransport != nil {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions,
metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
},
metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonConflicted),
},
)
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
continue
}
var resolvedRefCondition metav1.Condition
serversTransport, resolvedRefCondition = p.loadServersTransport(namespace, policy)
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, resolvedRefCondition)
if resolvedRefCondition.Status == metav1.ConditionFalse {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonNoValidCACertificate),
})
} else {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonAccepted),
})
}
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
// When something went wrong during the loading of a ServersTransport,
// we stop here and return a route condition error.
if resolvedRefCondition.Status == metav1.ConditionFalse {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot apply BackendTLSPolicy for Service %s/%s: %s", namespace, string(backendRef.Name), resolvedRefCondition.Message),
}
}
}
}
// If a ServersTransport is set, it means a BackendTLSPolicy matched the service port, and we can safely assume the protocol is HTTPS.
// When no ServersTransport is set, we need to determine the protocol based on the service port.
protocol := "https"
if serversTransport == nil {
protocol, err = getGRPCServiceProtocol(svcPort)
if err != nil {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: %s", namespace, backendRef.Name, err),
}
}
}
@@ -369,7 +465,8 @@ func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, ba
URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(ba.IP, strconv.Itoa(int(ba.Port)))),
})
}
return lb, nil
return lb, serversTransport, nil
}
func buildGRPCMatchRule(hostnames []gatev1.Hostname, match gatev1.GRPCRouteMatch) (string, int) {
+33 -38
View File
@@ -23,7 +23,7 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
routes, err := p.client.ListHTTPRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list HTTPRoutes")
@@ -41,9 +41,8 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
continue
}
var parentStatuses []gatev1.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := &gatev1.RouteParentStatus{
parentStatus := gatev1.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -79,7 +78,7 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
}
}
routeConf, resolveRefCondition := p.loadHTTPRoute(logger.WithContext(ctx), listener, route, hostnames)
routeConf, resolveRefCondition := p.loadHTTPRoute(logger.WithContext(ctx), listener, route, hostnames, statusReport)
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
}
@@ -87,23 +86,12 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
parentStatuses = append(parentStatuses, *parentStatus)
}
status := gatev1.HTTPRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateHTTPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update HTTPRoute status")
statusReport.RecordHTTPRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
}
}
}
func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
@@ -176,7 +164,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener,
default:
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadWRRService(ctx, listener, conf, routerName, routeRule, route, match.Path)
router.Service, serviceCondition = p.loadWRRService(ctx, listener, conf, routerName, routeRule, route, match.Path, statusReport)
if serviceCondition != nil {
condition = *serviceCondition
}
@@ -191,7 +179,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener,
return conf, condition
}
func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute, pathMatch *gatev1.HTTPPathMatch) (string, *metav1.Condition) {
func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute, pathMatch *gatev1.HTTPPathMatch, statusReport *statusReport) (string, *metav1.Condition) {
name := routeKey + "-wrr"
if _, ok := conf.HTTP.Services[name]; ok {
return name, nil
@@ -202,7 +190,7 @@ func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener,
for _, backendRef := range routeRule.BackendRefs {
// TODO in loadService we need to always return a non-nil serviceName even when there is an error which is not the
// usual defacto.
svcName, errCondition := p.loadService(ctx, listener, conf, route, backendRef, pathMatch)
svcName, errCondition := p.loadService(listener, conf, route, backendRef, pathMatch, statusReport)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
log.Ctx(ctx).Error().
@@ -229,7 +217,7 @@ func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener,
// loadService returns a dynamic.Service config corresponding to the given gatev1.HTTPBackendRef.
// Note that the returned dynamic.Service config can be nil (for cross-provider, internal services, and backendFunc).
func (p *Provider) loadService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, pathMatch *gatev1.HTTPPathMatch) (string, *metav1.Condition) {
func (p *Provider) loadService(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, pathMatch *gatev1.HTTPPathMatch, statusReport *statusReport) (string, *metav1.Condition) {
kind := ptr.Deref(backendRef.Kind, kindService)
group := groupCore
@@ -240,6 +228,17 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co
namespace := route.Namespace
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http"), &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
}
}
}
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http")
@@ -304,7 +303,7 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, st, errCondition := p.loadHTTPServers(ctx, namespace, route, backendRef, listener)
lb, st, errCondition := p.loadHTTPServers(namespace, route, backendRef, listener, statusReport)
if errCondition != nil {
return serviceName, errCondition
}
@@ -431,7 +430,7 @@ func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRe
return filterFunc(string(extensionRef.Name), namespace)
}
func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, listener gatewayListener) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
func (p *Provider) loadHTTPServers(namespace string, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef.BackendRef)
if err != nil {
return nil, nil, &metav1.Condition{
@@ -468,6 +467,12 @@ func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route
var serversTransport *dynamic.ServersTransport
for _, policy := range backendTLSPolicies {
for _, targetRef := range policy.Spec.TargetRefs {
// Skip targetRefs that doesn't match the backendRef,
// since a BackendTLSPolicy can select multiple services.
if targetRef.Name != backendRef.Name {
continue
}
// Skip the targetRef if the sectionName doesn't match the backendRef port.
if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) {
continue
}
@@ -502,12 +507,7 @@ func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route
},
)
status := gatev1.PolicyStatus{
Ancestors: []gatev1.PolicyAncestorStatus{policyAncestorStatus},
}
if err := p.client.UpdateBackendTLSPolicyStatus(ctx, ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, status); err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("Unable to update conflicting BackendTLSPolicy status")
}
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
continue
}
@@ -534,12 +534,7 @@ func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route
})
}
status := gatev1.PolicyStatus{
Ancestors: []gatev1.PolicyAncestorStatus{policyAncestorStatus},
}
if err := p.client.UpdateBackendTLSPolicyStatus(ctx, ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, status); err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("Unable to update BackendTLSPolicy status")
}
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
// When something wen wrong during the loading of a ServersTransport,
// we stop here and return a route condition error.
@@ -600,7 +595,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
}
for _, caCertRef := range policy.Spec.Validation.CACertificateRefs {
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != "ConfigMap" && caCertRef.Kind != "Secret") {
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != kindConfigMap && caCertRef.Kind != kindSecret) {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
@@ -613,7 +608,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
var caCRT string
switch caCertRef.Kind {
case "ConfigMap":
case kindConfigMap:
configmap, err := p.client.GetConfigMap(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
@@ -626,7 +621,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
}
}
caCRT = configmap.Data["ca.crt"]
case "Secret":
case kindSecret:
secret, err := p.client.GetSecret(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
+37 -37
View File
@@ -49,6 +49,8 @@ const (
kindTCPRoute = "TCPRoute"
kindTLSRoute = "TLSRoute"
kindService = "Service"
kindConfigMap = "ConfigMap"
kindSecret = "Secret"
appProtocolHTTP = "http"
appProtocolHTTPS = "https"
@@ -222,20 +224,29 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
// Note that event is the *first* event that came in during this throttling interval -- if we're hitting our throttle, we may have dropped events.
// This is fine, because we don't treat different event types differently.
// But if we do in the future, we'll need to track more information about the dropped events.
conf := p.loadConfigurationFromGateways(ctxLog)
confHash, err := hashstructure.Hash(conf, nil)
switch {
case err != nil:
logger.Error().Msg("Unable to hash the configuration")
case p.lastConfiguration.Get() == confHash:
logger.Debug().Msgf("Skipping Kubernetes event kind %T", event)
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: ProviderName,
Configuration: conf,
conf, statusReport, err := p.loadConfigurationFromGateways(ctxLog)
if err != nil {
logger.Error().Err(err).Msg("Unable to load configuration from Gateways")
} else {
confHash, err := hashstructure.Hash(conf, nil)
switch {
case err != nil:
logger.Error().Msg("Unable to hash the configuration")
case p.lastConfiguration.Get() == confHash:
logger.Debug().Msgf("Skipping Kubernetes event kind %T", event)
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: ProviderName,
Configuration: conf,
}
}
// Flush regardless of whether the dynamic configuration changed: the
// statusReport is independent of confHash and may carry writes even
// when the data plane has nothing new to consume (e.g. a GatewayClass
// that's now Accepted but has no Gateway pointing at it yet).
statusReport.Flush(ctxLog, p.client)
}
// If we're throttling,
@@ -302,7 +313,8 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
}
// TODO Handle errors and update resources statuses (gatewayClass, gateway).
func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.Configuration {
func (p *Provider) loadConfigurationFromGateways(ctx context.Context) (*dynamic.Configuration, *statusReport, error) {
statusReport := newStatusReport()
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
@@ -325,14 +337,12 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
addresses, err := p.gatewayAddresses()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to get Gateway status addresses")
return nil
return nil, nil, fmt.Errorf("getting gateway addresses: %w", err)
}
gatewayClasses, err := p.client.ListGatewayClasses()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list GatewayClasses")
return nil
return nil, nil, fmt.Errorf("listing gateway classes: %w", err)
}
var supportedFeatures []gatev1.SupportedFeature
@@ -363,13 +373,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
SupportedFeatures: supportedFeatures,
}
if err := p.client.UpdateGatewayClassStatus(ctx, gatewayClass.Name, status); err != nil {
log.Ctx(ctx).
Warn().
Err(err).
Str("gateway_class", gatewayClass.Name).
Msg("Unable to update GatewayClass status")
}
statusReport.RecordGatewayClassStatus(gatewayClass.Name, status)
}
var gateways []*gatev1.Gateway
@@ -390,14 +394,14 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
gatewayListeners = append(gatewayListeners, p.loadGatewayListeners(logger.WithContext(ctx), gateway, conf)...)
}
p.loadHTTPRoutes(ctx, gatewayListeners, conf)
p.loadHTTPRoutes(ctx, gatewayListeners, conf, statusReport)
p.loadGRPCRoutes(ctx, gatewayListeners, conf)
p.loadGRPCRoutes(ctx, gatewayListeners, conf, statusReport)
p.loadTLSRoutes(ctx, gatewayListeners, conf)
p.loadTLSRoutes(ctx, gatewayListeners, conf, statusReport)
if p.ExperimentalChannel {
p.loadTCPRoutes(ctx, gatewayListeners, conf)
p.loadTCPRoutes(ctx, gatewayListeners, conf, statusReport)
}
for _, gateway := range gateways {
@@ -428,14 +432,10 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
Msg("Gateway Not Accepted")
}
if err = p.client.UpdateGatewayStatus(ctx, ktypes.NamespacedName{Name: gateway.Name, Namespace: gateway.Namespace}, gatewayStatus); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update Gateway status")
}
statusReport.RecordGatewayStatus(ktypes.NamespacedName{Name: gateway.Name, Namespace: gateway.Namespace}, gatewayStatus)
}
return conf
return conf, statusReport, nil
}
func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gateway, conf *dynamic.Configuration) []gatewayListener {
@@ -591,7 +591,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
var errCertConditions []metav1.Condition
listenerTLSCerts := make(map[string]*tls.CertAndStores)
for _, certificateRef := range listener.TLS.CertificateRefs {
if certificateRef.Kind == nil || *certificateRef.Kind != "Secret" || certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) {
if certificateRef.Kind == nil || *certificateRef.Kind != kindSecret || certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) {
errCertConditions = append(errCertConditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
@@ -604,7 +604,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
}
certificateNamespace := string(ptr.Deref(certificateRef.Namespace, gatev1.Namespace(gateway.Namespace)))
if err := p.isReferenceGranted(kindGateway, gateway.Namespace, groupCore, "Secret", string(certificateRef.Name), certificateNamespace); err != nil {
if err := p.isReferenceGranted(kindGateway, gateway.Namespace, groupCore, kindSecret, string(certificateRef.Name), certificateNamespace); err != nil {
errCertConditions = append(errCertConditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
@@ -95,7 +95,10 @@ func TestGatewayClassLabelSelector(t *testing.T) {
client: client,
}
_ = p.loadConfigurationFromGateways(t.Context())
_, statusReport, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
statusReport.Flush(t.Context(), p.client)
gw, err := gwClient.GatewayV1().Gateways("default").Get(t.Context(), "traefik-external", metav1.GetOptions{})
require.NoError(t, err)
@@ -2753,7 +2756,9 @@ func TestLoadHTTPRoutes(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -3216,7 +3221,9 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) {
p.RegisterBackendFuncs(group, kind, backendFunc)
}
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -3502,7 +3509,193 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) {
p.RegisterFilterFuncs(group, kind, filterFunc)
}
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
assert.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
}
func TestLoadGRPCRoutes(t *testing.T) {
testCases := []struct {
desc string
paths []string
expected *dynamic.Configuration
entryPoints map[string]Entrypoint
}{
{
desc: "Simple GRPCRoute and BackendTLSPolicy with CA certificate",
paths: []string{"services.yml", "grpcroute/with_backend_tls_policy.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64": {
EntryPoints: []string{"web"},
Service: "grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr",
Rule: `Host("foo.com") && PathPrefix("/")`,
Priority: 22,
RuleSyntax: "default",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-80-grpc",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80-grpc": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "https://10.10.0.1:80",
},
{
URL: "https://10.10.0.2:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
ServersTransport: "default-whoami-80-grpc",
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-whoami-80-grpc": {
ServerName: "whoami",
RootCAs: []types.FileOrContent{
"CA1",
"CA2",
"CA1-secret",
"CA2-secret",
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple GRPCRoute and BackendTLSPolicy with System CA",
paths: []string{"services.yml", "grpcroute/with_backend_tls_policy_system.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64": {
EntryPoints: []string{"web"},
Service: "grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr",
Rule: `Host("foo.com") && PathPrefix("/")`,
Priority: 22,
RuleSyntax: "default",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-80-grpc",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80-grpc": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "https://10.10.0.1:80",
},
{
URL: "https://10.10.0.2:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
ServersTransport: "default-whoami-80-grpc",
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-whoami-80-grpc": {
ServerName: "whoami",
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
if test.expected == nil {
return
}
k8sObjects, gwObjects := readResources(t, test.paths)
kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
client := newClientImpl(kubeClient, gwClient)
eventCh, err := client.WatchAll(nil, make(chan struct{}))
require.NoError(t, err)
if len(k8sObjects) > 0 || len(gwObjects) > 0 {
<-eventCh
}
p := Provider{
EntryPoints: test.entryPoints,
client: client,
}
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -3794,7 +3987,9 @@ func TestLoadGRPCRoutes_filterExtensionRef(t *testing.T) {
p.RegisterFilterFuncs(group, kind, filterFunc)
}
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
assert.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -4715,7 +4910,9 @@ func TestLoadTCPRoutes(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -5976,6 +6173,178 @@ func TestLoadTLSRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple TLSRoute and BackendTLSPolicy with CA certificate",
paths: []string{"services.yml", "tlsroute/with_backend_tls_policy.yml"},
entryPoints: map[string]Entrypoint{"tls": {
Address: ":9001",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"deny-unknown-host": {
Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)",
Priority: 1,
Service: "deny-unknown-host",
TLS: &dynamic.RouterTCPTLSConfig{},
},
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls"},
Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
Rule: `HostSNI("foo.com")`,
Priority: 7,
RuleSyntax: "default",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"deny-unknown-host": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
},
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "default-whoami-80",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.1:80",
},
{
Address: "10.10.0.2:80",
},
},
ServersTransport: "default-whoami-80",
},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{
"default-whoami-80": {
TLS: &dynamic.TLSClientConfig{
ServerName: "whoami",
RootCAs: []types.FileOrContent{
"CA1",
"CA2",
"CA1-secret",
"CA2-secret",
},
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
},
},
},
{
desc: "Simple TLSRoute and BackendTLSPolicy with System CA",
paths: []string{"services.yml", "tlsroute/with_backend_tls_policy_system.yml"},
entryPoints: map[string]Entrypoint{"tls": {
Address: ":9001",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"deny-unknown-host": {
Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)",
Priority: 1,
Service: "deny-unknown-host",
TLS: &dynamic.RouterTCPTLSConfig{},
},
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls"},
Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
Rule: `HostSNI("foo.com")`,
Priority: 7,
RuleSyntax: "default",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"deny-unknown-host": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
},
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "default-whoami-80",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.1:80",
},
{
Address: "10.10.0.2:80",
},
},
ServersTransport: "default-whoami-80",
},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{
"default-whoami-80": {
TLS: &dynamic.TLSClientConfig{
ServerName: "whoami",
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
},
},
},
}
for _, test := range testCases {
@@ -6009,7 +6378,9 @@ func TestLoadTLSRoutes(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -6999,7 +7370,9 @@ func TestLoadMixedRoutes(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -7297,7 +7670,9 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
assert.Equal(t, test.expected, conf)
})
}
@@ -8425,20 +8800,22 @@ func Test_isCrossProviderNamespaceAllowed(t *testing.T) {
func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
testCases := []struct {
desc string
fixture string
crossProviderNamespaces []string
wantError bool
}{
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "httproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "httproute/simple_cross_provider.yml"})
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
@@ -8459,7 +8836,8 @@ func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
router, ok := conf.HTTP.Routers["httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3"]
require.True(t, ok)
@@ -8488,20 +8866,22 @@ func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
testCases := []struct {
desc string
fixture string
crossProviderNamespaces []string
wantError bool
}{
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tcproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tcproute/simple_cross_provider.yml"})
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
@@ -8524,7 +8904,8 @@ func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
ExperimentalChannel: true,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
router, ok := conf.TCP.Routers["tcproute-default-tcp-app-1-gw-default-my-gateway-ep-tcp-0-e3b0c44298fc1c149afb"]
require.True(t, ok)
@@ -8560,20 +8941,22 @@ func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
func TestCrossProviderNamespaces_TLSRoute(t *testing.T) {
testCases := []struct {
desc string
fixture string
crossProviderNamespaces []string
wantError bool
}{
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tlsroute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tlsroute/simple_cross_provider.yml"})
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
@@ -8595,7 +8978,8 @@ func TestCrossProviderNamespaces_TLSRoute(t *testing.T) {
client: client,
}
conf := p.loadConfigurationFromGateways(t.Context())
conf, _, err := p.loadConfigurationFromGateways(t.Context())
assert.NoError(t, err)
fmt.Println(conf.TCP.Routers)
+136
View File
@@ -0,0 +1,136 @@
package gateway
import (
"context"
"reflect"
"github.com/rs/zerolog/log"
ktypes "k8s.io/apimachinery/pkg/types"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
// statusReport collects the status writes produced by a single rebuild so they
// can be flushed to the apiserver after the dynamic configuration has been published.
type statusReport struct {
gatewayClasses map[string]gatev1.GatewayClassStatus
gateways map[ktypes.NamespacedName]gatev1.GatewayStatus
httpRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
grpcRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
tcpRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
tlsRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
backendTLSPolicies map[ktypes.NamespacedName]gatev1.PolicyStatus
}
func newStatusReport() *statusReport {
return &statusReport{
gatewayClasses: map[string]gatev1.GatewayClassStatus{},
gateways: map[ktypes.NamespacedName]gatev1.GatewayStatus{},
httpRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
grpcRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
tcpRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
tlsRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
backendTLSPolicies: map[ktypes.NamespacedName]gatev1.PolicyStatus{},
}
}
// Flush sends every status write collected during the
// routing configuration build to the Kubernetes API server.
func (r *statusReport) Flush(ctx context.Context, client *clientWrapper) {
logger := log.Ctx(ctx)
for name, status := range r.gatewayClasses {
if err := client.UpdateGatewayClassStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("gateway_class", name).Msg("Unable to update GatewayClass status")
}
}
for name, status := range r.gateways {
if err := client.UpdateGatewayStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("gateway", name.Name).Str("namespace", name.Namespace).Msg("Unable to update Gateway status")
}
}
for name, routeStatus := range r.httpRoutes {
status := gatev1.HTTPRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateHTTPRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("http_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update HTTPRoute status")
}
}
for name, routeStatus := range r.grpcRoutes {
status := gatev1.GRPCRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateGRPCRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("grpc_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update GRPCRoute status")
}
}
for name, routeStatus := range r.tcpRoutes {
status := gatev1alpha2.TCPRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateTCPRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("tcp_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update TCPRoute status")
}
}
for name, routeStatus := range r.tlsRoutes {
status := gatev1.TLSRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateTLSRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("tls_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update TLSRoute status")
}
}
for name, policyStatus := range r.backendTLSPolicies {
if err := client.UpdateBackendTLSPolicyStatus(ctx, name, policyStatus); err != nil {
logger.Warn().Err(err).Str("backend_tls_policy", name.Name).Str("namespace", name.Namespace).Msg("Unable to update BackendTLSPolicy status")
}
}
}
func (r *statusReport) RecordGatewayClassStatus(gatewayClassName string, status gatev1.GatewayClassStatus) {
r.gatewayClasses[gatewayClassName] = status
}
func (r *statusReport) RecordGatewayStatus(gateway ktypes.NamespacedName, status gatev1.GatewayStatus) {
r.gateways[gateway] = status
}
func (r *statusReport) RecordHTTPRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.httpRoutes[route] = gatev1.RouteStatus{
Parents: append(r.httpRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordGRPCRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.grpcRoutes[route] = gatev1.RouteStatus{
Parents: append(r.grpcRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordTCPRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.tcpRoutes[route] = gatev1.RouteStatus{
Parents: append(r.tcpRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordTLSRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.tlsRoutes[route] = gatev1.RouteStatus{
Parents: append(r.tlsRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordBackendTLSPolicyStatus(policy ktypes.NamespacedName, status gatev1.PolicyAncestorStatus) {
var ancestors []gatev1.PolicyAncestorStatus
// Keep existing ancestor statuses, except if it matches the status to merge.
for _, existing := range r.backendTLSPolicies[policy].Ancestors {
if reflect.DeepEqual(existing.AncestorRef, status.AncestorRef) {
continue
}
ancestors = append(ancestors, existing)
}
r.backendTLSPolicies[policy] = gatev1.PolicyStatus{
Ancestors: append(ancestors, status), // Add the new status to the existing ancestors statuses.
}
}
@@ -0,0 +1,152 @@
package gateway
import (
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ktypes "k8s.io/apimachinery/pkg/types"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func TestStatusReport_RecordGatewayClassStatus(t *testing.T) {
report := newStatusReport()
accepted := gatev1.GatewayClassStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayClassConditionStatusAccepted)}},
}
report.RecordGatewayClassStatus("traefik", accepted)
assert.Equal(t, accepted, report.gatewayClasses["traefik"])
// A later record for the same GatewayClass overwrites the previous one.
unsupported := gatev1.GatewayClassStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayClassReasonUnsupportedVersion)}},
}
report.RecordGatewayClassStatus("traefik", unsupported)
assert.Equal(t, unsupported, report.gatewayClasses["traefik"])
}
func TestStatusReport_RecordGatewayStatus(t *testing.T) {
report := newStatusReport()
gateway := ktypes.NamespacedName{Namespace: "default", Name: "my-gateway"}
accepted := gatev1.GatewayStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayConditionAccepted)}},
}
report.RecordGatewayStatus(gateway, accepted)
assert.Equal(t, accepted, report.gateways[gateway])
// A later record for the same Gateway overwrites the previous one.
programmed := gatev1.GatewayStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayConditionProgrammed)}},
}
report.RecordGatewayStatus(gateway, programmed)
assert.Equal(t, programmed, report.gateways[gateway])
}
func TestStatusReport_RecordHTTPRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordHTTPRouteStatus(route, gatewayParent)
report.RecordHTTPRouteStatus(route, otherParent)
// Each parentRef accumulates as a distinct parent status.
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.httpRoutes[route].Parents)
}
func TestStatusReport_RecordGRPCRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordGRPCRouteStatus(route, gatewayParent)
report.RecordGRPCRouteStatus(route, otherParent)
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.grpcRoutes[route].Parents)
}
func TestStatusReport_RecordTCPRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordTCPRouteStatus(route, gatewayParent)
report.RecordTCPRouteStatus(route, otherParent)
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.tcpRoutes[route].Parents)
}
func TestStatusReport_RecordTLSRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordTLSRouteStatus(route, gatewayParent)
report.RecordTLSRouteStatus(route, otherParent)
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.tlsRoutes[route].Parents)
}
func TestStatusReport_RecordBackendTLSPolicyStatus(t *testing.T) {
gatewayAncestor := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{Name: "gateway"},
ControllerName: controllerName,
}
otherAncestor := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{Name: "other-gateway"},
ControllerName: controllerName,
}
testCases := []struct {
desc string
records []gatev1.PolicyAncestorStatus
expected []gatev1.PolicyAncestorStatus
}{
{
desc: "distinct ancestor refs accumulate",
records: []gatev1.PolicyAncestorStatus{gatewayAncestor, otherAncestor},
expected: []gatev1.PolicyAncestorStatus{gatewayAncestor, otherAncestor},
},
{
desc: "same ancestor ref is replaced, not duplicated",
records: []gatev1.PolicyAncestorStatus{
gatewayAncestor,
{
AncestorRef: gatev1.ParentReference{Name: "gateway"},
ControllerName: "another.io/controller",
},
},
expected: []gatev1.PolicyAncestorStatus{
{
AncestorRef: gatev1.ParentReference{Name: "gateway"},
ControllerName: "another.io/controller",
},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
report := newStatusReport()
policy := ktypes.NamespacedName{Namespace: "default", Name: "my-policy"}
for _, record := range test.records {
report.RecordBackendTLSPolicyStatus(policy, record)
}
assert.Equal(t, test.expected, report.backendTLSPolicies[policy].Ancestors)
})
}
}
+19 -20
View File
@@ -19,7 +19,7 @@ import (
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
logger := log.Ctx(ctx)
routes, err := p.client.ListTCPRoutes()
if err != nil {
@@ -28,19 +28,13 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gateway
}
for _, route := range routes {
logger := log.Ctx(ctx).With().
Str("tcp_route", route.Name).
Str("namespace", route.Namespace).
Logger()
routeListeners := matchingGatewayListeners(gatewayListeners, route.Namespace, route.Spec.ParentRefs)
if len(routeListeners) == 0 {
continue
}
var parentStatuses []gatev1alpha2.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := &gatev1alpha2.RouteParentStatus{
parentStatus := gatev1alpha2.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -77,18 +71,7 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gateway
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
parentStatuses = append(parentStatuses, *parentStatus)
}
routeStatus := gatev1alpha2.TCPRouteStatus{
RouteStatus: gatev1alpha2.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateTCPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update TCPRoute status")
statusReport.RecordTCPRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
}
}
}
@@ -221,6 +204,17 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev
namespace := route.Namespace
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load TCPRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
}
}
}
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
@@ -347,4 +341,9 @@ func mergeTCPConfiguration(from, to *dynamic.Configuration) {
to.TCP.Services = map[string]*dynamic.TCPService{}
}
maps.Copy(to.TCP.Services, from.TCP.Services)
if to.TCP.ServersTransports == nil {
to.TCP.ServersTransports = map[string]*dynamic.TCPServersTransport{}
}
maps.Copy(to.TCP.ServersTransports, from.TCP.ServersTransports)
}
+222 -30
View File
@@ -5,12 +5,14 @@ import (
"fmt"
"net"
"regexp"
"slices"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider"
"github.com/traefik/traefik/v3/pkg/types"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ktypes "k8s.io/apimachinery/pkg/types"
@@ -18,7 +20,7 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
logger := log.Ctx(ctx)
routes, err := p.client.ListTLSRoutes()
if err != nil {
@@ -27,18 +29,13 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
}
for _, route := range routes {
logger := log.Ctx(ctx).With().
Str("tls_route", route.Name).
Str("namespace", route.Namespace).Logger()
routeListeners := matchingGatewayListeners(gatewayListeners, route.Namespace, route.Spec.ParentRefs)
if len(routeListeners) == 0 {
continue
}
var parentStatuses []gatev1.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := &gatev1.RouteParentStatus{
parentStatus := gatev1.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -73,14 +70,14 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
}
}
routeConf, resolveRefCondition := p.loadTLSRoute(listener, route, hostnames)
routeConf, resolveRefCondition := p.loadTLSRoute(listener, route, hostnames, statusReport)
if accepted && listener.Attached {
mergeTCPConfiguration(routeConf, conf)
}
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
parentStatuses = append(parentStatuses, *parentStatus)
statusReport.RecordTLSRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
}
// When there is at least one TLS listener, we add a default deny-all route to avoid accepting traffic for undefined hosts.
@@ -96,21 +93,10 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
}
}
routeStatus := gatev1.TLSRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateTLSRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update TLSRoute status")
}
}
}
func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
@@ -171,7 +157,7 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute
}
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadTLSWRRService(conf, routerName, routeRule.BackendRefs, route)
router.Service, serviceCondition = p.loadTLSWRRService(listener, conf, routerName, routeRule.BackendRefs, route, statusReport)
if serviceCondition != nil {
condition = *serviceCondition
}
@@ -183,7 +169,7 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute
}
// loadTLSWRRService is generating a WRR service, even when there is only one target.
func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey string, backendRefs []gatev1.BackendRef, route *gatev1.TLSRoute) (string, *metav1.Condition) {
func (p *Provider) loadTLSWRRService(listener gatewayListener, conf *dynamic.Configuration, routeKey string, backendRefs []gatev1.BackendRef, route *gatev1.TLSRoute, statusReport *statusReport) (string, *metav1.Condition) {
name := routeKey + "-wrr"
if _, ok := conf.TCP.Services[name]; ok {
return name, nil
@@ -192,7 +178,7 @@ func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey strin
var wrr dynamic.TCPWeightedRoundRobin
var condition *metav1.Condition
for _, backendRef := range backendRefs {
svcName, svc, errCondition := p.loadTLSService(route, backendRef)
svcName, svc, errCondition := p.loadTLSService(listener, conf, route, backendRef, statusReport)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
@@ -226,7 +212,7 @@ func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey strin
return name, condition
}
func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.BackendRef) (string, *dynamic.TCPService, *metav1.Condition) {
func (p *Provider) loadTLSService(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.TLSRoute, backendRef gatev1.BackendRef, statusReport *statusReport) (string, *dynamic.TCPService, *metav1.Condition) {
kind := ptr.Deref(backendRef.Kind, kindService)
group := groupCore
@@ -237,6 +223,17 @@ func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.Back
namespace := route.Namespace
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load TLSRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
}
}
}
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
@@ -283,18 +280,23 @@ func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.Back
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, errCondition := p.loadTLSServers(namespace, route, backendRef)
lb, st, errCondition := p.loadTLSServers(namespace, route, backendRef, listener, statusReport)
if errCondition != nil {
return serviceName, nil, errCondition
}
if st != nil {
lb.ServersTransport = serviceName
conf.TCP.ServersTransports[serviceName] = st
}
return serviceName, &dynamic.TCPService{LoadBalancer: lb}, nil
}
func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, backendRef gatev1.BackendRef) (*dynamic.TCPServersLoadBalancer, *metav1.Condition) {
func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, backendRef gatev1.BackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.TCPServersLoadBalancer, *dynamic.TCPServersTransport, *metav1.Condition) {
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef)
if err != nil {
return nil, &metav1.Condition{
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.GetGeneration(),
@@ -304,8 +306,116 @@ func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, back
}
}
backendTLSPolicies, err := p.client.ListBackendTLSPoliciesForService(namespace, string(backendRef.Name))
if err != nil {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot list BackendTLSPolicies for Service %s/%s: %s", namespace, string(backendRef.Name), err),
}
}
// Sort BackendTLSPolicies by creation timestamp, then by name to match the BackendTLSPolicy requirements.
slices.SortStableFunc(backendTLSPolicies, func(a, b *gatev1.BackendTLSPolicy) int {
cmpTime := a.CreationTimestamp.Time.Compare(b.CreationTimestamp.Time)
if cmpTime == 0 {
return strings.Compare(a.Name, b.Name)
}
return cmpTime
})
var serversTransport *dynamic.TCPServersTransport
for _, policy := range backendTLSPolicies {
for _, targetRef := range policy.Spec.TargetRefs {
// Skip targetRefs that doesn't match the backendRef,
// since a BackendTLSPolicy can select multiple services.
if targetRef.Name != backendRef.Name {
continue
}
// Skip the targetRef if the sectionName doesn't match the backendRef port.
if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) {
continue
}
policyAncestorStatus := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{
Group: ptr.To(gatev1.Group(groupGateway)),
Kind: ptr.To(gatev1.Kind(kindGateway)),
Namespace: ptr.To(gatev1.Namespace(namespace)),
Name: gatev1.ObjectName(listener.GWName),
SectionName: ptr.To(gatev1.SectionName(listener.Name)),
},
ControllerName: controllerName,
}
// Multiple BackendTLSPolicies can match the same service port, meaning that there is a conflict.
if serversTransport != nil {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions,
metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
},
metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonConflicted),
},
)
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
continue
}
var resolvedRefCondition metav1.Condition
serversTransport, resolvedRefCondition = p.loadTCPServersTransport(namespace, policy)
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, resolvedRefCondition)
if resolvedRefCondition.Status == metav1.ConditionFalse {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonNoValidCACertificate),
})
} else {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonAccepted),
})
}
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
// When something went wrong during the loading of a ServersTransport,
// we stop here and return a route condition error.
if resolvedRefCondition.Status == metav1.ConditionFalse {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot apply BackendTLSPolicy for Service %s/%s: %s", namespace, string(backendRef.Name), resolvedRefCondition.Message),
}
}
}
}
if svcPort.Protocol != corev1.ProtocolTCP {
return nil, &metav1.Condition{
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.GetGeneration(),
@@ -323,7 +433,89 @@ func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, back
Address: net.JoinHostPort(ba.IP, strconv.Itoa(int(ba.Port))),
})
}
return lb, nil
return lb, serversTransport, nil
}
func (p *Provider) loadTCPServersTransport(namespace string, policy *gatev1.BackendTLSPolicy) (*dynamic.TCPServersTransport, metav1.Condition) {
st := &dynamic.TCPServersTransport{
TLS: &dynamic.TLSClientConfig{
ServerName: string(policy.Spec.Validation.Hostname),
},
}
if policy.Spec.Validation.WellKnownCACertificates != nil {
return st, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
}
}
for _, caCertRef := range policy.Spec.Validation.CACertificateRefs {
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != kindConfigMap && caCertRef.Kind != kindSecret) {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidKind),
Message: "Only ConfigMaps and Secrets are supported",
}
}
var caCRT string
switch caCertRef.Kind {
case kindConfigMap:
configmap, err := p.client.GetConfigMap(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
Message: fmt.Sprintf("getting configmap %s/%s: %s", namespace, string(caCertRef.Name), err),
}
}
caCRT = configmap.Data["ca.crt"]
case kindSecret:
secret, err := p.client.GetSecret(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
Message: fmt.Sprintf("getting secret %s/%s: %s", namespace, string(caCertRef.Name), err),
}
}
caCRT = string(secret.Data["ca.crt"])
}
if caCRT == "" {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
Message: fmt.Sprintf("%s %s/%s does not have a ca.crt", caCertRef.Kind, namespace, string(caCertRef.Name)),
}
}
st.TLS.RootCAs = append(st.TLS.RootCAs, types.FileOrContent(caCRT))
}
return st, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
}
}
func hostSNIRule(hostnames []gatev1.Hostname) (string, int) {
@@ -415,11 +415,13 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
logger.Error().
Err(err).
Str("ingress", fmt.Sprintf("%s/%s rule-%d path-%d", ing.Namespace, ing.Name, ri, pi)).
Msg("Cannot resolve auth secret, skipping auth middleware")
} else {
loc.BasicAuth = basic
loc.DigestAuth = digest
Msg("Cannot resolve auth secret, skipping ingress")
// Skipping the ingress entirely when auth secret resolution fails,
// to match ingress-nginx behavior.
continue
}
loc.BasicAuth = basic
loc.DigestAuth = digest
}
// Pre-resolve custom headers ConfigMap.
@@ -0,0 +1,25 @@
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ingress-with-basicauth-secret-missing
namespace: default
annotations:
nginx.ingress.kubernetes.io/auth-type: "basic"
nginx.ingress.kubernetes.io/auth-secret-type: "auth-file"
nginx.ingress.kubernetes.io/auth-secret: "default/missing-basic-auth"
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
spec:
ingressClassName: nginx
rules:
- host: whoami.localhost
http:
paths:
- path: /basicauth
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
@@ -0,0 +1,69 @@
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ingress-with-endpoint-conditions
namespace: default
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: whoami.localhost
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80
---
kind: Service
apiVersion: v1
metadata:
name: whoami
namespace: default
spec:
clusterIP: 10.10.10.1
ports:
- name: web
protocol: TCP
port: 80
targetPort: web
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: whoami-abc
namespace: default
labels:
kubernetes.io/service-name: whoami
addressType: IPv4
ports:
- name: web
port: 80
endpoints:
- addresses:
- 10.10.0.1
conditions:
ready: true
serving: true
terminating: false
- addresses:
- 10.10.0.2
conditions:
ready: false
serving: true
terminating: true
- addresses:
- 10.10.0.3
conditions:
ready: false
serving: false
terminating: true
@@ -1329,6 +1329,37 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Basic Auth with missing secret — ingress is skipped entirely",
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-basicauth-secret-missing.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{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"unavailable-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Forward Auth",
paths: []string{
@@ -15656,6 +15687,102 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Ingress with endpoint conditions",
paths: []string{
"ingressclasses.yml",
"ingresses/ingress-with-endpoint-conditions.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-endpoint-conditions-rule-0-path-0": {
EntryPoints: []string{"http"},
Rule: `Host("whoami.localhost") && PathPrefix("/")`,
RuleSyntax: "default",
Service: "default-ingress-with-endpoint-conditions-whoami-80",
Middlewares: []string{"default-ingress-with-endpoint-conditions-rule-0-path-0-retry"},
Observability: &dynamic.RouterObservabilityConfig{
Metadata: &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: "default",
IngressName: "ingress-with-endpoint-conditions",
ServiceName: "whoami",
ServicePort: "80",
},
},
},
},
"default-ingress-with-endpoint-conditions-rule-0-path-0-tls": {
EntryPoints: []string{"https"},
Rule: `Host("whoami.localhost") && PathPrefix("/")`,
RuleSyntax: "default",
Service: "default-ingress-with-endpoint-conditions-whoami-80",
Middlewares: []string{"default-ingress-with-endpoint-conditions-rule-0-path-0-tls-retry"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
Metadata: &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: "default",
IngressName: "ingress-with-endpoint-conditions",
ServiceName: "whoami",
ServicePort: "80",
},
},
},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-endpoint-conditions-rule-0-path-0-retry": {
Retry: &dynamic.Retry{Attempts: 3},
},
"default-ingress-with-endpoint-conditions-rule-0-path-0-tls-retry": {
Retry: &dynamic.Retry{Attempts: 3},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-endpoint-conditions-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://10.10.0.1:80"},
{URL: "http://10.10.0.2:80", Fenced: true},
},
Strategy: dynamic.BalancerStrategyWRR,
PassHostHeader: ptr.To(true),
ServersTransport: "default-ingress-with-endpoint-conditions",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
"unavailable-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-endpoint-conditions": {
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 secret missing — ingress is skipped entirely",
paths: []string{
@@ -294,7 +294,8 @@ func buildService(backend *backend, serversTransportName string) *dynamic.Servic
svc := &dynamic.Service{LoadBalancer: lb}
for _, ep := range backend.Endpoints {
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{
URL: fmt.Sprintf("http://%s", ep.Address),
URL: fmt.Sprintf("http://%s", ep.Address),
Fenced: ep.Fenced,
})
}
@@ -320,7 +321,8 @@ func buildServiceWithLocConfig(backend *backend, serversTransportName string, lo
svc := &dynamic.Service{LoadBalancer: lb}
for _, ep := range backend.Endpoints {
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{
URL: fmt.Sprintf("%s://%s", scheme, ep.Address),
URL: fmt.Sprintf("%s://%s", scheme, ep.Address),
Fenced: ep.Fenced,
})
}
@@ -0,0 +1,75 @@
---
kind: Node
apiVersion: v1
metadata:
name: node1
status:
addresses:
- type: InternalIP
address: 10.0.0.1
- type: ExternalIP
address: 1.2.3.4
---
kind: Node
apiVersion: v1
metadata:
name: node2
status:
addresses:
- type: InternalIP
address: 10.0.0.2
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: foo
namespace: default
spec:
rules:
- host: "*.foo.com"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service1
port:
number: 80
---
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: default
spec:
ports:
- port: 80
clusterIP: 10.0.0.1
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: service1-abc
namespace: default
labels:
kubernetes.io/service-name: service1
addressType: IPv4
ports:
- port: 8080
name: ""
endpoints:
- addresses:
- 10.10.0.1
conditions:
ready: true
+37 -4
View File
@@ -48,6 +48,7 @@ type Provider struct {
LabelSelector string `description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation or IngressClass name to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"`
ReportNodeInternalIPs bool `description:"Report node internal IPs in Ingress status." json:"reportNodeInternalIPs,omitempty" toml:"reportNodeInternalIPs,omitempty" yaml:"reportNodeInternalIPs,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
@@ -72,6 +73,14 @@ func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) {
// Init the provider.
func (p *Provider) Init() error {
if p.ReportNodeInternalIPs && p.IngressEndpoint != nil {
return errors.New("reportNodeInternalIPs and ingressEndpoint are mutually exclusive")
}
if p.ReportNodeInternalIPs && p.DisableClusterScopeResources {
return errors.New("reportNodeInternalIPs and disableClusterScopeResources are mutually exclusive")
}
return nil
}
@@ -246,6 +255,26 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
ingresses := client.GetIngresses()
var nodeIngressStatus []netv1.IngressLoadBalancerIngress
if p.ReportNodeInternalIPs {
nodes, _, err := client.GetNodes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Error while getting nodes for ingress status")
} else {
for _, node := range nodes {
for _, address := range node.Status.Addresses {
if address.Type == corev1.NodeInternalIP {
nodeIngressStatus = append(nodeIngressStatus, netv1.IngressLoadBalancerIngress{IP: address.Address})
}
}
}
if len(nodeIngressStatus) == 0 {
log.Ctx(ctx).Error().Msg("No nodes with internal IP address found for ingress status")
}
}
}
certConfigs := make(map[string]*tls.CertAndStores)
for _, ingress := range ingresses {
logger := log.Ctx(ctx).With().Str("ingress", ingress.Name).Str("namespace", ingress.Namespace).Logger()
@@ -255,7 +284,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
continue
}
if err := p.updateIngressStatus(ingress, client); err != nil {
if err := p.updateIngressStatus(ingress, client, nodeIngressStatus); err != nil {
logger.Error().Err(err).Msg("Error while updating ingress status")
}
@@ -442,7 +471,11 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
return conf
}
func (p *Provider) updateIngressStatus(ing *netv1.Ingress, k8sClient Client) error {
func (p *Provider) updateIngressStatus(ing *netv1.Ingress, k8sClient Client, nodeIngressStatus []netv1.IngressLoadBalancerIngress) error {
if len(nodeIngressStatus) > 0 {
return k8sClient.UpdateIngressStatus(ing, nodeIngressStatus)
}
// Only process if an EndpointIngress has been configured.
if p.IngressEndpoint == nil {
return nil
@@ -623,12 +656,12 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
return nil, errors.New("nodes lookup is disabled")
}
nodes, nodesExists, nodesErr := client.GetNodes()
nodes, _, nodesErr := client.GetNodes()
if nodesErr != nil {
return nil, nodesErr
}
if !nodesExists || len(nodes) == 0 {
if len(nodes) == 0 {
return nil, fmt.Errorf("nodes not found in namespace %s", namespace)
}
@@ -3094,6 +3094,77 @@ func readResources(t *testing.T, paths []string) []runtime.Object {
return k8sObjects
}
func TestProviderInit(t *testing.T) {
p := Provider{
ReportNodeInternalIPs: true,
IngressEndpoint: &EndpointIngress{IP: "1.2.3.4"},
}
assert.EqualError(t, p.Init(), "reportNodeInternalIPs and ingressEndpoint are mutually exclusive")
p2 := Provider{
ReportNodeInternalIPs: true,
DisableClusterScopeResources: true,
}
assert.EqualError(t, p2.Init(), "reportNodeInternalIPs and disableClusterScopeResources are mutually exclusive")
p3 := Provider{ReportNodeInternalIPs: true}
assert.NoError(t, p3.Init())
}
func TestReportNodeInternalIPs(t *testing.T) {
testCases := []struct {
desc string
client clientMock
expectedEmpty bool
}{
{
desc: "nodes present",
client: newClientMock(generateTestFilename("Node Internal IP")),
},
{
desc: "GetNodes API error",
client: clientMock{apiNodesError: errors.New("api nodes error")},
expectedEmpty: true,
},
{
desc: "no nodes found",
client: clientMock{nodes: []*corev1.Node{}},
expectedEmpty: true,
},
{
desc: "nodes exist but none have an internal IP",
client: clientMock{
nodes: []*corev1.Node{
{
Status: corev1.NodeStatus{
Addresses: []corev1.NodeAddress{
{Type: corev1.NodeExternalIP, Address: "1.2.3.4"},
},
},
},
},
},
expectedEmpty: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := Provider{ReportNodeInternalIPs: true}
conf := p.loadConfigurationFromIngresses(t.Context(), test.client)
if test.expectedEmpty {
assert.Empty(t, conf.HTTP.Routers)
assert.Empty(t, conf.HTTP.Services)
} else {
assert.NotEmpty(t, conf.HTTP.Routers)
assert.NotEmpty(t, conf.HTTP.Services)
}
})
}
}
func TestStrictPrefixMatchingRule(t *testing.T) {
tests := []struct {
path string
+101 -50
View File
@@ -55,7 +55,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
Str(logs.RouterName, routerName).
Strs(logs.EntryPointName, defaultEntryPoints).
Msg("No entryPoint defined for this router, using the default one(s) instead")
router.EntryPoints = defaultEntryPoints
router.EntryPoints = slices.Clone(defaultEntryPoints)
}
// The `ruleSyntax` option is deprecated.
@@ -99,7 +99,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
log.Debug().
Str(logs.RouterName, routerName).
Msgf("No entryPoint defined for this TCP router, using the default one(s) instead: %+v", defaultEntryPoints)
router.EntryPoints = defaultEntryPoints
router.EntryPoints = slices.Clone(defaultEntryPoints)
}
conf.TCP.Routers[provider.MakeQualifiedName(pvd, routerName)] = router
}
@@ -173,80 +173,131 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
return conf
}
func resolveHTTPTLSOptions(cfg dynamic.Configuration) dynamic.Configuration {
if cfg.HTTP == nil || len(cfg.HTTP.Routers) == 0 {
return cfg
// resolveHTTPTLSOptions resolves the TLS options for the given routers, on a per
// entryPoint basis.
//
// TLS options conflicts (i.e. the same host served with different TLS options) can
// only be detected and arbitrated within a single TLS listener, that is to say within
// a single entryPoint. To honor that, routers are grouped per entryPoint and the
// conflict detection is run independently for each entryPoint.
//
// A router keeps its original name, and its resolved TLS options, for the entryPoints
// on which it does not conflict. For each entryPoint on which it conflicts, that
// entryPoint is removed from the router and a dedicated copy is emitted, with its
// TLSOptions reset to the default one, named following the "ep-conflicted-name@provider" pattern.
func resolveHTTPTLSOptions(routers map[string]*dynamic.Router) map[string]*dynamic.Router {
if len(routers) == 0 {
return routers
}
rts := make(map[string]*dynamic.Router)
newRouters := make(map[string]*dynamic.Router)
// Keyed by domain, then by options reference.
// The actual source of truth for what TLS options will actually be used for the connection.
// As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS
// options that occur for a given host name, so that later on we can set relevant
// errors and logging for all the routers concerned (i.e. wrongly configured).
tlsOptionsForHostSNI := map[string]map[string][]string{}
for routerHTTPName, routerHTTPConfig := range cfg.HTTP.Routers {
rts[routerHTTPName] = routerHTTPConfig.DeepCopy()
if routerHTTPConfig.TLS == nil {
// Split every router per entryPoint.
// Routers always have at least one entryPoint at this stage, as they are
// defaulted in mergeConfiguration before applyModel and this resolution run.
routersByEntryPoint := map[string]map[string]*dynamic.Router{}
for name, router := range routers {
if router.TLS == nil {
newRouters[name] = router
continue
}
ctxRouter := provider.AddInContext(context.Background(), routerHTTPName)
logger := log.Ctx(ctxRouter).With().Str(logs.RouterName, routerHTTPName).Logger()
tlsOptionsName := traefiktls.DefaultTLSConfigName
if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
router.TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
if len(router.TLS.Options) > 0 && router.TLS.Options != traefiktls.DefaultTLSConfigName {
router.TLS.ResolvedOptions = provider.GetQualifiedName(provider.AddInContext(context.Background(), name), router.TLS.Options)
}
domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule)
for _, ep := range router.EntryPoints {
if routersByEntryPoint[ep] == nil {
routersByEntryPoint[ep] = map[string]*dynamic.Router{}
}
routersByEntryPoint[ep][name] = router
}
}
// Resolve the TLS options independently for each entryPoint.
conflictingRouters := make(map[string][]string, len(routersByEntryPoint))
for ep, epRouters := range routersByEntryPoint {
conflictingRouters[ep] = findConflictingRouters(ep, epRouters)
}
for name, router := range routers {
router.EntryPoints = slices.DeleteFunc(router.EntryPoints, func(ep string) bool {
deleted := slices.Contains(conflictingRouters[ep], name)
if deleted {
rt := router.DeepCopy()
rt.TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
rt.EntryPoints = []string{ep}
// The new name is not collision free but has very small possibility to collide.
// TODO: rework this naming whenever we'll introduce a resource reference mechanism not based on a string.
newRouters[ep+"-conflicted-"+name] = rt
}
return deleted
})
if len(router.EntryPoints) > 0 {
newRouters[name] = router
}
}
return newRouters
}
// findConflictingRouters returns the names of the routers, among the given
// single-entryPoint routers, that serve a host (SNI) also served by another router
// with a different resolved TLS option. Such routers are arbitrated by falling back
// to the default TLS options.
func findConflictingRouters(ep string, routers map[string]*dynamic.Router) []string {
var conflicting []string
// For each host (SNI, already lower-cased by the domain parsing), the routers
// serving it grouped by their resolved TLS option. A host with more than one
// group is served with conflicting TLS options.
routersByHostAndOption := map[string]map[string][]string{}
for name, router := range routers {
if router.TLS == nil {
continue
}
domains, err := httpmuxer.ParseDomains(router.Rule)
if err != nil {
logger.Error().Err(err).Msgf("Invalid rule %s", routerHTTPConfig.Rule)
continue
}
if len(domains) == 0 {
rts[routerHTTPName].TLS.ResolvedOptions = "default"
logger.Warn().Msgf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule)
// The configured TLSOptions on a router without a domain in its rule cannot be selected when evaluating the SNI,
// so if it is not the default one, it is a conflict.
if len(domains) == 0 && router.TLS.ResolvedOptions != traefiktls.DefaultTLSConfigName {
conflicting = append(conflicting, name)
continue
}
for _, domain := range domains {
// domain is already in lower case thanks to the domain parsing
if tlsOptionsForHostSNI[domain] == nil {
tlsOptionsForHostSNI[domain] = make(map[string][]string)
if routersByHostAndOption[domain] == nil {
routersByHostAndOption[domain] = map[string][]string{}
}
tlsOptionsForHostSNI[domain][tlsOptionsName] = append(tlsOptionsForHostSNI[domain][tlsOptionsName], routerHTTPName)
option := router.TLS.ResolvedOptions
routersByHostAndOption[domain][option] = append(routersByHostAndOption[domain][option], name)
}
}
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
if len(tlsConfigs) == 1 {
for optionsName, v := range tlsConfigs {
log.Debug().Msgf("Adding route for %s with TLS options %s", hostSNI, optionsName)
for _, s := range v {
rts[s].TLS.ResolvedOptions = optionsName
}
}
for domain, routersByOption := range routersByHostAndOption {
if len(routersByOption) == 1 {
continue
}
// multiple tlsConfigs
routers := make([]string, 0, len(tlsConfigs))
for _, v := range tlsConfigs {
for _, s := range v {
rts[s].TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
routers = append(routers, s)
}
var routersInConflict []string
for _, names := range routersByOption {
conflicting = append(conflicting, names...)
routersInConflict = append(routersInConflict, names...)
}
log.Warn().Msgf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
log.Error().Msgf("On EntryPoint %q, Host %q is served by multiple routers with different TLS options, default TLSOptions will be applied for the following routers: %v", ep, domain, routersInConflict)
}
cfg.HTTP.Routers = rts
return cfg
return conflicting
}
func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
+111
View File
@@ -5,6 +5,7 @@ import (
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
otypes "github.com/traefik/traefik/v3/pkg/observability/types"
"github.com/traefik/traefik/v3/pkg/tls"
@@ -1230,3 +1231,113 @@ func Test_applyModel(t *testing.T) {
})
}
}
func Test_resolveHTTPTLSOptions(t *testing.T) {
testCases := []struct {
desc string
routers map[string]*dynamic.Router
expected map[string]string // router name -> ResolvedOptions
unexpectedRouters []string
}{
{
desc: "same host, different options, different entryPoints: no conflict",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
"router-b@file": {EntryPoints: []string{"ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}},
},
expected: map[string]string{
"router-a@file": "optsA@file",
"router-b@file": "optsB@file",
},
},
{
desc: "same host, different options, same entryPoint: conflict falls back to default",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
"router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}},
},
expected: map[string]string{
"ep-a-conflicted-router-a@file": "default",
"ep-a-conflicted-router-b@file": "default",
},
unexpectedRouters: []string{"router-a@file", "router-b@file"},
},
{
desc: "same host, same options, same entryPoint: keeps the configured options",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
"router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`) && PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
},
expected: map[string]string{
"router-a@file": "optsA@file",
"router-b@file": "optsA@file",
},
},
{
desc: "router spanning two entryPoints, conflict on one only: router is duplicated",
routers: map[string]*dynamic.Router{
"shared@file": {EntryPoints: []string{"ep-a", "ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsX"}},
"other@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsY"}},
},
expected: map[string]string{
"ep-a-conflicted-shared@file": "default", // conflicts with other@file on ep-a
"shared@file": "optsX@file", // alone on ep-b
"ep-a-conflicted-other@file": "default",
},
unexpectedRouters: []string{"other@file"},
},
{
desc: "no domain in rule, non-default options: forced to default and renamed",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
},
expected: map[string]string{
"ep-a-conflicted-router-a@file": "default",
},
unexpectedRouters: []string{"router-a@file"},
},
{
desc: "no domain in rule, implicit default options: not conflicting, keeps its name",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{}},
},
expected: map[string]string{
"router-a@file": "default",
},
unexpectedRouters: []string{"ep-a-conflicted-router-a@file"},
},
{
desc: "no domain in rule, explicit default options: not conflicting, keeps its name",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{
Options: "default",
}},
},
expected: map[string]string{
"router-a@file": "default",
},
unexpectedRouters: []string{"ep-a-conflicted-router-a@file"},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
got := resolveHTTPTLSOptions(test.routers)
for name, want := range test.expected {
rt, ok := got[name]
require.True(t, ok, "router %q is missing", name)
require.NotNil(t, rt.TLS, "router %q has no TLS config", name)
assert.Equal(t, want, rt.TLS.ResolvedOptions, "router %q %v", name, rt.EntryPoints)
}
for _, name := range test.unexpectedRouters {
_, ok := got[name]
require.False(t, ok, "router %q is present", name)
}
})
}
}
+3 -1
View File
@@ -176,7 +176,9 @@ func (c *ConfigurationWatcher) applyConfigurations(ctx context.Context) {
conf := mergeConfiguration(newConfigs.DeepCopy(), c.defaultEntryPoints)
conf = applyModel(conf)
conf = resolveHTTPTLSOptions(conf)
if conf.HTTP != nil {
conf.HTTP.Routers = resolveHTTPTLSOptions(conf.HTTP.Routers)
}
for _, listener := range c.configurationListeners {
listener(conf)
+29
View File
@@ -0,0 +1,29 @@
package server
import (
"context"
"net"
)
type connContextFunc func(context.Context, net.Conn) context.Context
type multipleConnContext struct {
fns []connContextFunc
}
func (m *multipleConnContext) AddConnContextFunc(fn connContextFunc) {
m.fns = append(m.fns, fn)
}
func (m *multipleConnContext) Build() connContextFunc {
if len(m.fns) == 0 {
return nil
}
return func(ctx context.Context, c net.Conn) context.Context {
for _, contextFunc := range m.fns {
ctx = contextFunc(ctx, c)
}
return ctx
}
}
+26
View File
@@ -0,0 +1,26 @@
package server
import (
"context"
"net"
"testing"
"github.com/stretchr/testify/require"
)
type keyTest string
func TestConnContext(t *testing.T) {
var connContext multipleConnContext
connContext.AddConnContextFunc(func(ctx context.Context, _ net.Conn) context.Context {
return context.WithValue(ctx, keyTest("test"), "test")
})
connContext.AddConnContextFunc(func(ctx context.Context, _ net.Conn) context.Context {
return context.WithValue(ctx, keyTest("test2"), "test2")
})
ctx := connContext.Build()(context.Background(), nil)
require.Equal(t, "test", ctx.Value(keyTest("test")))
require.Equal(t, "test2", ctx.Value(keyTest("test2")))
}
+3 -2
View File
@@ -168,11 +168,12 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
// # Otherwise, it will fallback to the default TLS config.
if tlsOptionsName != traefiktls.DefaultTLSConfigName {
logger.Warn().Msgf("No domain found in rule %v, the TLS option %s cannot be applied", routerHTTPConfig.Rule, tlsOptionsName)
routerHTTPConfig.AddError(fmt.Errorf("no domain found in rule %v, the TLS option %s cannot be applied", routerHTTPConfig.Rule, tlsOptionsName), false)
}
}
if routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName {
routerHTTPConfig.AddError(errors.New("found different TLS options for routers on the same host, so using the default TLS options instead"), false)
if len(domains) > 0 && routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName {
routerHTTPConfig.AddError(errors.New("router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"), false)
}
// Even though the error is seemingly ignored (aside from logging it),
+41 -40
View File
@@ -643,6 +643,7 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
configuration.ForwardedHeaders.TrustedIPs,
configuration.ForwardedHeaders.Connection,
configuration.ForwardedHeaders.NotAppendXForwardedFor,
configuration.ForwardedHeaders.AddXForwardedSchemeHeaders,
next)
if err != nil {
return nil, err
@@ -678,6 +679,44 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
handler = denyFragment(handler)
var connContext multipleConnContext
connContext.AddConnContextFunc(func(ctx context.Context, c net.Conn) context.Context {
// This adds an empty struct in order to store a RoundTripper in the ConnContext in case of Kerberos or NTLM.
ctx = service.AddTransportOnContext(ctx)
if tlsConn, ok := c.(*tls.Conn); ok {
if tlsConnWithOptionsName, ok := tlsConn.NetConn().(tcp.TLSConn); ok {
return tcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName)
}
}
return ctx
})
if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) {
connContext.AddConnContextFunc(func(ctx context.Context, c net.Conn) context.Context {
cState := &connState{Start: time.Now()}
if debugConnection {
clientConnectionStatesMu.Lock()
clientConnectionStates[getConnKey(c)] = cState
clientConnectionStatesMu.Unlock()
}
return context.WithValue(ctx, connStateKey, cState)
})
}
var connState func(c net.Conn, state http.ConnState)
if debugConnection {
connState = func(c net.Conn, state http.ConnState) {
clientConnectionStatesMu.Lock()
if clientConnectionStates[getConnKey(c)] != nil {
clientConnectionStates[getConnKey(c)].State = state.String()
}
clientConnectionStatesMu.Unlock()
}
}
serverHTTP := &http.Server{
Protocols: &protocols,
Handler: handler,
@@ -691,46 +730,8 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
MaxDecoderHeaderTableSize: int(configuration.HTTP2.MaxDecoderHeaderTableSize),
MaxEncoderHeaderTableSize: int(configuration.HTTP2.MaxEncoderHeaderTableSize),
},
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
if tlsConn, ok := c.(*tls.Conn); ok {
if tlsConnWithOptionsName, ok := tlsConn.NetConn().(tcp.TLSConn); ok {
return tcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName)
}
}
return ctx
},
}
if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) {
serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
cState := &connState{Start: time.Now()}
if debugConnection {
clientConnectionStatesMu.Lock()
clientConnectionStates[getConnKey(c)] = cState
clientConnectionStatesMu.Unlock()
}
return context.WithValue(ctx, connStateKey, cState)
}
if debugConnection {
serverHTTP.ConnState = func(c net.Conn, state http.ConnState) {
clientConnectionStatesMu.Lock()
if clientConnectionStates[getConnKey(c)] != nil {
clientConnectionStates[getConnKey(c)].State = state.String()
}
clientConnectionStatesMu.Unlock()
}
}
}
prevConnContext := serverHTTP.ConnContext
serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
// This adds an empty struct in order to store a RoundTripper in the ConnContext in case of Kerberos or NTLM.
ctx = service.AddTransportOnContext(ctx)
if prevConnContext != nil {
return prevConnContext(ctx, c)
}
return ctx
ConnContext: connContext.Build(),
ConnState: connState,
}
listener := newHTTPForwarder(ln)
+3 -3
View File
@@ -4,11 +4,11 @@ RepositoryName = "traefik"
OutputType = "file"
FileName = "traefik_changelog.md"
# example new bugfix v3.7.3
# example new bugfix v3.7.5
CurrentRef = "v3.7"
PreviousRef = "v3.7.2"
PreviousRef = "v3.7.4"
BaseBranch = "v3.7"
FutureCurrentRefName = "v3.7.3"
FutureCurrentRefName = "v3.7.5"
ThresholdPreviousRef = 10000
ThresholdCurrentRef = 10000
+5 -3
View File
@@ -64,7 +64,7 @@
"framer-motion": "^11.18.2",
"globals": "^16.0.0",
"jest-extended": "^4.0.2",
"jsdom": "^24.0.0",
"jsdom": "29.1.1",
"lodash": "4.18.1",
"msw": "^2.1.7",
"query-string": "^6.9.0",
@@ -75,7 +75,7 @@
"react-helmet-async": "^2.0.4",
"react-icons": "^5.0.1",
"react-infinite-scroll-hook": "^4.1.1",
"react-router-dom": "6.30.2",
"react-router-dom": "6.30.4",
"swr": "^2.2.4",
"typescript": "^5.2.2",
"typescript-eslint": "^8.38.0",
@@ -98,6 +98,8 @@
"packageManager": "yarn@4.13.0",
"resolutions": {
"form-data": "4.0.4",
"js-yaml": "4.1.1"
"js-yaml": "4.1.1",
"@csstools/css-syntax-patches-for-csstree": "1.1.4",
"undici": "7.27.0"
}
}
+259 -229
View File
@@ -22,16 +22,43 @@ __metadata:
languageName: node
linkType: hard
"@asamuzakjp/css-color@npm:^3.1.1":
version: 3.1.1
resolution: "@asamuzakjp/css-color@npm:3.1.1"
"@asamuzakjp/css-color@npm:^5.1.11":
version: 5.1.11
resolution: "@asamuzakjp/css-color@npm:5.1.11"
dependencies:
"@csstools/css-calc": "npm:^2.1.2"
"@csstools/css-color-parser": "npm:^3.0.8"
"@csstools/css-parser-algorithms": "npm:^3.0.4"
"@csstools/css-tokenizer": "npm:^3.0.3"
lru-cache: "npm:^10.4.3"
checksum: 10c0/4abb010fd29de8acae8571eba738468c22cb45a1f77647df3c59a80f1c83d83d728cae3ebbf99e5c73f2517761abaaffbe5e4176fc46b5f9bf60f1478463b51e
"@asamuzakjp/generational-cache": "npm:^1.0.1"
"@csstools/css-calc": "npm:^3.2.0"
"@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
checksum: 10c0/32720bdff8daea6a8847aba6cdfae55baa3b4a2690b51d21db7f0382bbd183f3d9f2d5126df50afd889062635684b2819e47113629ee2e80c99389e75f48d060
languageName: node
linkType: hard
"@asamuzakjp/dom-selector@npm:^7.1.1":
version: 7.1.1
resolution: "@asamuzakjp/dom-selector@npm:7.1.1"
dependencies:
"@asamuzakjp/generational-cache": "npm:^1.0.1"
"@asamuzakjp/nwsapi": "npm:^2.3.9"
bidi-js: "npm:^1.0.3"
css-tree: "npm:^3.2.1"
is-potential-custom-element-name: "npm:^1.0.1"
checksum: 10c0/8cec1c618781c94de5836a215bbe5aafb4d8b835b18c51faf8547f4574afa39f92def3951e40123860062467613dd825f1e1600ff32e8045cc099a91796dcfb8
languageName: node
linkType: hard
"@asamuzakjp/generational-cache@npm:^1.0.1":
version: 1.0.1
resolution: "@asamuzakjp/generational-cache@npm:1.0.1"
checksum: 10c0/1de62de43764e13fca3b9a31b7ea9b1bf0780fe053d266e40378a19ff8c66b543e011e6a0df02d410cd59bf981126706f176cdbb938985165202c4a079fe1057
languageName: node
linkType: hard
"@asamuzakjp/nwsapi@npm:^2.3.9":
version: 2.3.9
resolution: "@asamuzakjp/nwsapi@npm:2.3.9"
checksum: 10c0/869b81382e775499c96c45c6dbe0d0766a6da04bcf0abb79f5333535c4e19946851acaa43398f896e2ecc5a1de9cf3db7cf8c4b1afac1ee3d15e21584546d74d
languageName: node
linkType: hard
@@ -301,6 +328,17 @@ __metadata:
languageName: node
linkType: hard
"@bramus/specificity@npm:^2.4.2":
version: 2.4.2
resolution: "@bramus/specificity@npm:2.4.2"
dependencies:
css-tree: "npm:^3.0.0"
bin:
specificity: bin/cli.js
checksum: 10c0/c5f4e04e0bca0d2202598207a5eb0733c8109d12a68a329caa26373bec598d99db5bb785b8865fefa00fc01b08c6068138807ceb11a948fe15e904ed6cf4ba72
languageName: node
linkType: hard
"@bundled-es-modules/cookie@npm:^2.0.1":
version: 2.0.1
resolution: "@bundled-es-modules/cookie@npm:2.0.1"
@@ -329,49 +367,61 @@ __metadata:
languageName: node
linkType: hard
"@csstools/color-helpers@npm:^5.0.2":
version: 5.0.2
resolution: "@csstools/color-helpers@npm:5.0.2"
checksum: 10c0/bebaddb28b9eb58b0449edd5d0c0318fa88f3cb079602ee27e88c9118070d666dcc4e09a5aa936aba2fde6ba419922ade07b7b506af97dd7051abd08dfb2959b
"@csstools/color-helpers@npm:^6.0.2":
version: 6.0.2
resolution: "@csstools/color-helpers@npm:6.0.2"
checksum: 10c0/4c66574563d7c960010c11e41c2673675baff07c427cca6e8dddffa5777de45770d13ff3efce1c0642798089ad55de52870d9d8141f78db3fa5bba012f2d3789
languageName: node
linkType: hard
"@csstools/css-calc@npm:^2.1.2":
version: 2.1.2
resolution: "@csstools/css-calc@npm:2.1.2"
"@csstools/css-calc@npm:^3.2.0, @csstools/css-calc@npm:^3.2.1":
version: 3.2.1
resolution: "@csstools/css-calc@npm:3.2.1"
peerDependencies:
"@csstools/css-parser-algorithms": ^3.0.4
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/34ced30553968ef5d5f9e00e3b90b48c47480cf130e282e99d57ec9b09f803aab8bc06325683e72a1518b5e7180a3da8b533f1b462062757c21989a53b482e1a
"@csstools/css-parser-algorithms": ^4.0.0
"@csstools/css-tokenizer": ^4.0.0
checksum: 10c0/0191c8d1cd4dffa0d3b6bfd1e78a721934b1d7a6c972966e4fdaa72208c6789e8ff443ee81764a32f1e6107825695b5524ef2b4dc1681b5b29230f2a1277e5df
languageName: node
linkType: hard
"@csstools/css-color-parser@npm:^3.0.8":
version: 3.0.8
resolution: "@csstools/css-color-parser@npm:3.0.8"
"@csstools/css-color-parser@npm:^4.1.0":
version: 4.1.1
resolution: "@csstools/css-color-parser@npm:4.1.1"
dependencies:
"@csstools/color-helpers": "npm:^5.0.2"
"@csstools/css-calc": "npm:^2.1.2"
"@csstools/color-helpers": "npm:^6.0.2"
"@csstools/css-calc": "npm:^3.2.1"
peerDependencies:
"@csstools/css-parser-algorithms": ^3.0.4
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/90722c5a62ca94e9d578ddf59be604a76400b932bd3d4bd23cb1ae9b7ace8fcf83c06995d2b31f96f4afef24a7cefba79beb11ed7ee4999d7ecfec3869368359
"@csstools/css-parser-algorithms": ^4.0.0
"@csstools/css-tokenizer": ^4.0.0
checksum: 10c0/427bd32f1a8917342a70a6fd97b93bb492aae7c8790e7782b5d6edc8c08064bb8aef0a86099f286db00288f9afea85eb92c46350e9057f5fea058e03a2a09203
languageName: node
linkType: hard
"@csstools/css-parser-algorithms@npm:^3.0.4":
version: 3.0.4
resolution: "@csstools/css-parser-algorithms@npm:3.0.4"
"@csstools/css-parser-algorithms@npm:^4.0.0":
version: 4.0.0
resolution: "@csstools/css-parser-algorithms@npm:4.0.0"
peerDependencies:
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/d411f07765e14eede17bccc6bd4f90ff303694df09aabfede3fd104b2dfacfd4fe3697cd25ddad14684c850328f3f9420ebfa9f78380892492974db24ae47dbd
"@csstools/css-tokenizer": ^4.0.0
checksum: 10c0/94558c2428d6ef0ddef542e86e0a8376aa1263a12a59770abb13ba50d7b83086822c75433f32aa2e7fef00555e1cc88292f9ca5bce79aed232bb3fed73b1528d
languageName: node
linkType: hard
"@csstools/css-tokenizer@npm:^3.0.3":
version: 3.0.3
resolution: "@csstools/css-tokenizer@npm:3.0.3"
checksum: 10c0/c31bf410e1244b942e71798e37c54639d040cb59e0121b21712b40015fced2b0fb1ffe588434c5f8923c9cd0017cfc1c1c8f3921abc94c96edf471aac2eba5e5
"@csstools/css-syntax-patches-for-csstree@npm:1.1.4":
version: 1.1.4
resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.4"
peerDependencies:
css-tree: ^3.2.1
peerDependenciesMeta:
css-tree:
optional: true
checksum: 10c0/3872a7befb553c53249c87e964ac00f55d059f4574d2cc023e03e1dafc86a5ad19f6a6d05fa2c14fb192e6a4538a73158104cc2e32e0688f27fd841b9ba76568
languageName: node
linkType: hard
"@csstools/css-tokenizer@npm:^4.0.0":
version: 4.0.0
resolution: "@csstools/css-tokenizer@npm:4.0.0"
checksum: 10c0/669cf3d0f9c8e1ffdf8c9955ad8beba0c8cfe03197fe29a4fcbd9ee6f7a18856cfa42c62670021a75183d9ab37f5d14a866e6a9df753a6c07f59e36797a9ea9f
languageName: node
linkType: hard
@@ -850,6 +900,18 @@ __metadata:
languageName: node
linkType: hard
"@exodus/bytes@npm:^1.11.0, @exodus/bytes@npm:^1.15.0, @exodus/bytes@npm:^1.6.0":
version: 1.15.1
resolution: "@exodus/bytes@npm:1.15.1"
peerDependencies:
"@noble/hashes": ^1.8.0 || ^2.0.0
peerDependenciesMeta:
"@noble/hashes":
optional: true
checksum: 10c0/333056a6953bbf875d9f3b86c32314de29458d842e5f56f6ef8034b18c2d9660184550093d1bae5de0064043d5e23f54cc03148798d9d29cf5167ac03f2e9f8c
languageName: node
linkType: hard
"@floating-ui/core@npm:^1.6.0":
version: 1.6.9
resolution: "@floating-ui/core@npm:1.6.9"
@@ -2417,10 +2479,10 @@ __metadata:
languageName: node
linkType: hard
"@remix-run/router@npm:1.23.1":
version: 1.23.1
resolution: "@remix-run/router@npm:1.23.1"
checksum: 10c0/94ac9632c0070199b8275cd6dffe78eb4c02e8926328937c65561c5c30d7ddf842743df3c8f7df302f00a593dd204846d93667fbbbe3c3641437d7b8f333ed90
"@remix-run/router@npm:1.23.3":
version: 1.23.3
resolution: "@remix-run/router@npm:1.23.3"
checksum: 10c0/73622465dd9e9a2c5a7ced7d1151ea6e9195a8a979c99b4f70a67093eeff7f339daf03a7c6304709a9c27deb2081a8fff6737663aacb33529c1a12a0a0827a17
languageName: node
linkType: hard
@@ -3841,13 +3903,6 @@ __metadata:
languageName: node
linkType: hard
"asynckit@npm:^0.4.0":
version: 0.4.0
resolution: "asynckit@npm:0.4.0"
checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d
languageName: node
linkType: hard
"available-typed-arrays@npm:^1.0.7":
version: 1.0.7
resolution: "available-typed-arrays@npm:1.0.7"
@@ -3878,6 +3933,15 @@ __metadata:
languageName: node
linkType: hard
"bidi-js@npm:^1.0.3":
version: 1.0.3
resolution: "bidi-js@npm:1.0.3"
dependencies:
require-from-string: "npm:^2.0.2"
checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1
languageName: node
linkType: hard
"brace-expansion@npm:^1.1.7":
version: 1.1.11
resolution: "brace-expansion@npm:1.1.11"
@@ -4103,15 +4167,6 @@ __metadata:
languageName: node
linkType: hard
"combined-stream@npm:^1.0.8":
version: 1.0.8
resolution: "combined-stream@npm:1.0.8"
dependencies:
delayed-stream: "npm:~1.0.0"
checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5
languageName: node
linkType: hard
"commander@npm:^13.1.0":
version: 13.1.0
resolution: "commander@npm:13.1.0"
@@ -4151,6 +4206,16 @@ __metadata:
languageName: node
linkType: hard
"css-tree@npm:^3.0.0, css-tree@npm:^3.2.1":
version: 3.2.1
resolution: "css-tree@npm:3.2.1"
dependencies:
mdn-data: "npm:2.27.1"
source-map-js: "npm:^1.2.1"
checksum: 10c0/1f65e9ccaa56112a4706d6f003dd43d777f0dbcf848e66fd320f823192533581f8dd58daa906cb80622658332d50284d6be13b87a6ab4556cbbfe9ef535bbf7e
languageName: node
linkType: hard
"css-what@npm:^6.1.0":
version: 6.2.2
resolution: "css-what@npm:6.2.2"
@@ -4172,16 +4237,6 @@ __metadata:
languageName: node
linkType: hard
"cssstyle@npm:^4.0.1":
version: 4.3.0
resolution: "cssstyle@npm:4.3.0"
dependencies:
"@asamuzakjp/css-color": "npm:^3.1.1"
rrweb-cssom: "npm:^0.8.0"
checksum: 10c0/770ccb288a99257fd0d5b129e03878f848e922d3b017358acb02e8dd530e8f0c7c6f74e6ae5367d715e2da36a490a734b4177fc1b78f3f08eca25f204a56a692
languageName: node
linkType: hard
"csstype@npm:^3.0.2":
version: 3.1.3
resolution: "csstype@npm:3.1.3"
@@ -4203,13 +4258,13 @@ __metadata:
languageName: node
linkType: hard
"data-urls@npm:^5.0.0":
version: 5.0.0
resolution: "data-urls@npm:5.0.0"
"data-urls@npm:^7.0.0":
version: 7.0.0
resolution: "data-urls@npm:7.0.0"
dependencies:
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^14.0.0"
checksum: 10c0/1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad
whatwg-mimetype: "npm:^5.0.0"
whatwg-url: "npm:^16.0.0"
checksum: 10c0/08d88ef50d8966a070ffdaa703e1e4b29f01bb2da364dfbc1612b1c2a4caa8045802c9532d81347b21781100132addb36a585071c8323b12cce97973961dee9f
languageName: node
linkType: hard
@@ -4298,10 +4353,10 @@ __metadata:
languageName: node
linkType: hard
"decimal.js@npm:^10.4.3":
version: 10.5.0
resolution: "decimal.js@npm:10.5.0"
checksum: 10c0/785c35279df32762143914668df35948920b6c1c259b933e0519a69b7003fc0a5ed2a766b1e1dda02574450c566b21738a45f15e274b47c2ac02072c0d1f3ac3
"decimal.js@npm:^10.6.0":
version: 10.6.0
resolution: "decimal.js@npm:10.6.0"
checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa
languageName: node
linkType: hard
@@ -4393,13 +4448,6 @@ __metadata:
languageName: node
linkType: hard
"delayed-stream@npm:~1.0.0":
version: 1.0.0
resolution: "delayed-stream@npm:1.0.0"
checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19
languageName: node
linkType: hard
"dequal@npm:^2.0.3":
version: 2.0.3
resolution: "dequal@npm:2.0.3"
@@ -4499,10 +4547,10 @@ __metadata:
languageName: node
linkType: hard
"entities@npm:^4.5.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250
"entities@npm:^8.0.0":
version: 8.0.0
resolution: "entities@npm:8.0.0"
checksum: 10c0/938e631664c19451823344a351aeeafd74fae2d5fa51e4d5b6ff635afaefd4bacf0f609989888c04c42733f46ffdac15211608267ebb02488005891a4793e94d
languageName: node
linkType: hard
@@ -5430,19 +5478,6 @@ __metadata:
languageName: node
linkType: hard
"form-data@npm:4.0.4":
version: 4.0.4
resolution: "form-data@npm:4.0.4"
dependencies:
asynckit: "npm:^0.4.0"
combined-stream: "npm:^1.0.8"
es-set-tostringtag: "npm:^2.1.0"
hasown: "npm:^2.0.2"
mime-types: "npm:^2.1.12"
checksum: 10c0/373525a9a034b9d57073e55eab79e501a714ffac02e7a9b01be1c820780652b16e4101819785e1e18f8d98f0aee866cc654d660a435c378e16a72f2e7cac9695
languageName: node
linkType: hard
"framer-motion@npm:^11.18.2":
version: 11.18.2
resolution: "framer-motion@npm:11.18.2"
@@ -5761,12 +5796,12 @@ __metadata:
languageName: node
linkType: hard
"html-encoding-sniffer@npm:^4.0.0":
version: 4.0.0
resolution: "html-encoding-sniffer@npm:4.0.0"
"html-encoding-sniffer@npm:^6.0.0":
version: 6.0.0
resolution: "html-encoding-sniffer@npm:6.0.0"
dependencies:
whatwg-encoding: "npm:^3.1.1"
checksum: 10c0/523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140
"@exodus/bytes": "npm:^1.6.0"
checksum: 10c0/66dc3f6f5539cc3beb814fcbfae7eacf4ec38cf824d6e1425b72039b51a40f4456bd8541ba66f4f4fe09cdf885ab5cd5bae6ec6339d6895a930b2fdb83c53025
languageName: node
linkType: hard
@@ -5784,7 +5819,7 @@ __metadata:
languageName: node
linkType: hard
"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2":
"http-proxy-agent@npm:^7.0.0":
version: 7.0.2
resolution: "http-proxy-agent@npm:7.0.2"
dependencies:
@@ -5794,7 +5829,7 @@ __metadata:
languageName: node
linkType: hard
"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.5":
"https-proxy-agent@npm:^7.0.1":
version: 7.0.6
resolution: "https-proxy-agent@npm:7.0.6"
dependencies:
@@ -5820,7 +5855,7 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
"iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@@ -6355,37 +6390,37 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:^24.0.0":
version: 24.1.3
resolution: "jsdom@npm:24.1.3"
"jsdom@npm:29.1.1":
version: 29.1.1
resolution: "jsdom@npm:29.1.1"
dependencies:
cssstyle: "npm:^4.0.1"
data-urls: "npm:^5.0.0"
decimal.js: "npm:^10.4.3"
form-data: "npm:^4.0.0"
html-encoding-sniffer: "npm:^4.0.0"
http-proxy-agent: "npm:^7.0.2"
https-proxy-agent: "npm:^7.0.5"
"@asamuzakjp/css-color": "npm:^5.1.11"
"@asamuzakjp/dom-selector": "npm:^7.1.1"
"@bramus/specificity": "npm:^2.4.2"
"@csstools/css-syntax-patches-for-csstree": "npm:^1.1.3"
"@exodus/bytes": "npm:^1.15.0"
css-tree: "npm:^3.2.1"
data-urls: "npm:^7.0.0"
decimal.js: "npm:^10.6.0"
html-encoding-sniffer: "npm:^6.0.0"
is-potential-custom-element-name: "npm:^1.0.1"
nwsapi: "npm:^2.2.12"
parse5: "npm:^7.1.2"
rrweb-cssom: "npm:^0.7.1"
lru-cache: "npm:^11.3.5"
parse5: "npm:^8.0.1"
saxes: "npm:^6.0.0"
symbol-tree: "npm:^3.2.4"
tough-cookie: "npm:^4.1.4"
tough-cookie: "npm:^6.0.1"
undici: "npm:^7.25.0"
w3c-xmlserializer: "npm:^5.0.0"
webidl-conversions: "npm:^7.0.0"
whatwg-encoding: "npm:^3.1.1"
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^14.0.0"
ws: "npm:^8.18.0"
webidl-conversions: "npm:^8.0.1"
whatwg-mimetype: "npm:^5.0.0"
whatwg-url: "npm:^16.0.1"
xml-name-validator: "npm:^5.0.0"
peerDependencies:
canvas: ^2.11.2
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
checksum: 10c0/e48b342afacd7418a23dac204a62deea729c50f4d072a7c04c09fd32355fdb4335f8779fa79fd0277a2dbeb2d356250a950955719d00047324b251233b11277f
checksum: 10c0/20e2174b09d9d06393cb48e1392b7a1cb7191d6656a6f7b3b8fbf9853b4ab0ef60b4a42c2c55f71b55ca5da50ffa75bcdc6986210963182e7993c6f9cd4f499b
languageName: node
linkType: hard
@@ -6595,6 +6630,13 @@ __metadata:
languageName: node
linkType: hard
"lru-cache@npm:^11.3.5":
version: 11.5.1
resolution: "lru-cache@npm:11.5.1"
checksum: 10c0/7b341cea79a8efe9c6a6f20c8757a77eca5b25d7ff983ccf4e11e547b81f6787824baa1c84705251dff84ab4ffac85717ac354b9d02e465f86a9f8b166409979
languageName: node
linkType: hard
"lru-cache@npm:^5.1.1":
version: 5.1.1
resolution: "lru-cache@npm:5.1.1"
@@ -6677,6 +6719,13 @@ __metadata:
languageName: node
linkType: hard
"mdn-data@npm:2.27.1":
version: 2.27.1
resolution: "mdn-data@npm:2.27.1"
checksum: 10c0/eb8abf5d22e4d1e090346f5e81b67d23cef14c83940e445da5c44541ad874dc8fb9f6ca236e8258c3a489d9fb5884188a4d7d58773adb9089ac2c0b966796393
languageName: node
linkType: hard
"media-query-parser@npm:^2.0.2":
version: 2.0.2
resolution: "media-query-parser@npm:2.0.2"
@@ -6710,22 +6759,6 @@ __metadata:
languageName: node
linkType: hard
"mime-db@npm:1.52.0":
version: 1.52.0
resolution: "mime-db@npm:1.52.0"
checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa
languageName: node
linkType: hard
"mime-types@npm:^2.1.12":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
mime-db: "npm:1.52.0"
checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2
languageName: node
linkType: hard
"mimic-fn@npm:^4.0.0":
version: 4.0.0
resolution: "mimic-fn@npm:4.0.0"
@@ -7015,13 +7048,6 @@ __metadata:
languageName: node
linkType: hard
"nwsapi@npm:^2.2.12":
version: 2.2.20
resolution: "nwsapi@npm:2.2.20"
checksum: 10c0/07f4dafa3186aef7c007863e90acd4342a34ba9d44b22f14f644fdb311f6086887e21c2fc15efaa826c2bc39ab2bc841364a1a630e7c87e0cb723ba59d729297
languageName: node
linkType: hard
"object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
@@ -7205,12 +7231,12 @@ __metadata:
languageName: node
linkType: hard
"parse5@npm:^7.1.2":
version: 7.2.1
resolution: "parse5@npm:7.2.1"
"parse5@npm:^8.0.1":
version: 8.0.1
resolution: "parse5@npm:8.0.1"
dependencies:
entities: "npm:^4.5.0"
checksum: 10c0/829d37a0c709215a887e410a7118d754f8e1afd7edb529db95bc7bbf8045fb0266a7b67801331d8e8d9d073ea75793624ec27ce9ff3b96862c3b9008f4d68e80
entities: "npm:^8.0.0"
checksum: 10c0/c3c1c5aab55f6e4be5245599790e56e64be7764a4a0edd7f98db4fe3bb380f63add752fa047dff0496446c25f4104f0c7c1967723de640bde92306a7bb67ed2f
languageName: node
linkType: hard
@@ -7604,27 +7630,27 @@ __metadata:
languageName: node
linkType: hard
"react-router-dom@npm:6.30.2":
version: 6.30.2
resolution: "react-router-dom@npm:6.30.2"
"react-router-dom@npm:6.30.4":
version: 6.30.4
resolution: "react-router-dom@npm:6.30.4"
dependencies:
"@remix-run/router": "npm:1.23.1"
react-router: "npm:6.30.2"
"@remix-run/router": "npm:1.23.3"
react-router: "npm:6.30.4"
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
checksum: 10c0/d0c6edf4e2aa7639b4a4f64a7747f03d8861bdf4857e8981b1cff1451b7cb91fcdcd7e315a6e3df910271b2f5071825d2aec218d5f7890f2269fc074f198e42a
checksum: 10c0/1b25ab26a288da852f7b58eec5c14b3f37919e6773a557cd846d3a05d7a7d890c8d49bda93c0a7f497f240df1df8b0ad50635f990aafb55f2fc545ad8269a822
languageName: node
linkType: hard
"react-router@npm:6.30.2":
version: 6.30.2
resolution: "react-router@npm:6.30.2"
"react-router@npm:6.30.4":
version: 6.30.4
resolution: "react-router@npm:6.30.4"
dependencies:
"@remix-run/router": "npm:1.23.1"
"@remix-run/router": "npm:1.23.3"
peerDependencies:
react: ">=16.8"
checksum: 10c0/cff5ea92d248d2230adc46d4e2ed3fbeddfaf1ae2e63411da8b7ea6ddc207d71dbc522c05c492e671e553e2153934f4ab180ac02bd36205b274e097f2cfe6fc4
checksum: 10c0/fb6de7d1002bcab9ea12c4072d93792eca494f1e4f30cfec334ccb6523756d23ce3e093c7c1241399296cebed94789a3fb89f96ee76004e0e746458a8f6bab33
languageName: node
linkType: hard
@@ -7707,6 +7733,13 @@ __metadata:
languageName: node
linkType: hard
"require-from-string@npm:^2.0.2":
version: 2.0.2
resolution: "require-from-string@npm:2.0.2"
checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2
languageName: node
linkType: hard
"requires-port@npm:^1.0.0":
version: 1.0.0
resolution: "requires-port@npm:1.0.0"
@@ -7967,20 +8000,6 @@ __metadata:
languageName: node
linkType: hard
"rrweb-cssom@npm:^0.7.1":
version: 0.7.1
resolution: "rrweb-cssom@npm:0.7.1"
checksum: 10c0/127b8ca6c8aac45e2755abbae6138d4a813b1bedc2caabf79466ae83ab3cfc84b5bfab513b7033f0aa4561c7753edf787d0dd01163ceacdee2e8eb1b6bf7237e
languageName: node
linkType: hard
"rrweb-cssom@npm:^0.8.0":
version: 0.8.0
resolution: "rrweb-cssom@npm:0.8.0"
checksum: 10c0/56f2bfd56733adb92c0b56e274c43f864b8dd48784d6fe946ef5ff8d438234015e59ad837fc2ad54714b6421384141c1add4eb569e72054e350d1f8a50b8ac7b
languageName: node
linkType: hard
"run-parallel@npm:^1.1.9":
version: 1.2.0
resolution: "run-parallel@npm:1.2.0"
@@ -8628,6 +8647,24 @@ __metadata:
languageName: node
linkType: hard
"tldts-core@npm:^7.4.2":
version: 7.4.2
resolution: "tldts-core@npm:7.4.2"
checksum: 10c0/e8e02a43f6823ea1beab8f5a9da370b9a6cbf1a942d4f7d828700e65f03a119348f8d19faa95b599f3ca76fcb5fe5c4611d5b9c274f5a58c7487d342f6083a06
languageName: node
linkType: hard
"tldts@npm:^7.0.5":
version: 7.4.2
resolution: "tldts@npm:7.4.2"
dependencies:
tldts-core: "npm:^7.4.2"
bin:
tldts: bin/cli.js
checksum: 10c0/68f7ec58c9ea41f1583a6384f0fdf1b33d2d8ee55e7d403ec5cf9de4a7226a25c585b5ce1a73de8ddc9abbbe7b783bb3554d1c811fd47fceb0d5511e06d0d766
languageName: node
linkType: hard
"to-regex-range@npm:^5.0.1":
version: 5.0.1
resolution: "to-regex-range@npm:5.0.1"
@@ -8649,12 +8686,21 @@ __metadata:
languageName: node
linkType: hard
"tr46@npm:^5.1.0":
version: 5.1.0
resolution: "tr46@npm:5.1.0"
"tough-cookie@npm:^6.0.1":
version: 6.0.1
resolution: "tough-cookie@npm:6.0.1"
dependencies:
tldts: "npm:^7.0.5"
checksum: 10c0/ec70bd6b1215efe4ed31a158f0be3e4c9088fcbd8620edc23a5860d4f3d85c757b77e274baaa700f7b25e409f4181552ed189603c2b2e1a9f88104da3a61a37d
languageName: node
linkType: hard
"tr46@npm:^6.0.0":
version: 6.0.0
resolution: "tr46@npm:6.0.0"
dependencies:
punycode: "npm:^2.3.1"
checksum: 10c0/d761f7144e0cb296187674ef245c74f761e334d7cf25ca73ef60e4c72c097c75051031c093fa1a2fee04b904977b316716a7915854bcae8fb1a371746513cbe8
checksum: 10c0/83130df2f649228aa91c17754b66248030a3af34911d713b5ea417066fa338aa4bc8668d06bd98aa21a2210f43fc0a3db8b9099e7747fb5830e40e39a6a1058e
languageName: node
linkType: hard
@@ -8691,7 +8737,7 @@ __metadata:
globals: "npm:^16.0.0"
husky: "npm:^9.0.0"
jest-extended: "npm:^4.0.2"
jsdom: "npm:^24.0.0"
jsdom: "npm:29.1.1"
lint-staged: "npm:^15.0.0"
lodash: "npm:4.18.1"
msw: "npm:^2.1.7"
@@ -8704,7 +8750,7 @@ __metadata:
react-helmet-async: "npm:^2.0.4"
react-icons: "npm:^5.0.1"
react-infinite-scroll-hook: "npm:^4.1.1"
react-router-dom: "npm:6.30.2"
react-router-dom: "npm:6.30.4"
swr: "npm:^2.2.4"
typescript: "npm:^5.2.2"
typescript-eslint: "npm:^8.38.0"
@@ -8888,6 +8934,13 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:7.27.0":
version: 7.27.0
resolution: "undici@npm:7.27.0"
checksum: 10c0/6fd15a81b0ca177b2667738b830ed175363e5e2164e992251d03aaee6c6517098b930085bd68b8fe5911920371076526657de035e07dc72377b9c5c731b90f0b
languageName: node
linkType: hard
"unique-filename@npm:^4.0.0":
version: 4.0.0
resolution: "unique-filename@npm:4.0.0"
@@ -9266,36 +9319,28 @@ __metadata:
languageName: node
linkType: hard
"webidl-conversions@npm:^7.0.0":
version: 7.0.0
resolution: "webidl-conversions@npm:7.0.0"
checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4
"webidl-conversions@npm:^8.0.1":
version: 8.0.1
resolution: "webidl-conversions@npm:8.0.1"
checksum: 10c0/3f6f327ca5fa0c065ed8ed0ef3b72f33623376e68f958e9b7bd0df49fdb0b908139ac2338d19fb45bd0e05595bda96cb6d1622222a8b413daa38a17aacc4dd46
languageName: node
linkType: hard
"whatwg-encoding@npm:^3.1.1":
version: 3.1.1
resolution: "whatwg-encoding@npm:3.1.1"
"whatwg-mimetype@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-mimetype@npm:5.0.0"
checksum: 10c0/eead164fe73a00dd82f817af6fc0bd22e9c273e1d55bf4bc6bdf2da7ad8127fca82ef00ea6a37892f5f5641f8e34128e09508f92126086baba126b9e0d57feb4
languageName: node
linkType: hard
"whatwg-url@npm:^16.0.0, whatwg-url@npm:^16.0.1":
version: 16.0.1
resolution: "whatwg-url@npm:16.0.1"
dependencies:
iconv-lite: "npm:0.6.3"
checksum: 10c0/273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e
languageName: node
linkType: hard
"whatwg-mimetype@npm:^4.0.0":
version: 4.0.0
resolution: "whatwg-mimetype@npm:4.0.0"
checksum: 10c0/a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df
languageName: node
linkType: hard
"whatwg-url@npm:^14.0.0":
version: 14.2.0
resolution: "whatwg-url@npm:14.2.0"
dependencies:
tr46: "npm:^5.1.0"
webidl-conversions: "npm:^7.0.0"
checksum: 10c0/f746fc2f4c906607d09537de1227b13f9494c171141e5427ed7d2c0dd0b6a48b43d8e71abaae57d368d0c06b673fd8ec63550b32ad5ed64990c7b0266c2b4272
"@exodus/bytes": "npm:^1.11.0"
tr46: "npm:^6.0.0"
webidl-conversions: "npm:^8.0.1"
checksum: 10c0/e75565566abf3a2cdbd9f06c965dbcccee6ec4e9f0d3728ad5e08ceb9944279848bcaa211d35a29cb6d2df1e467dd05cfb59fbddf8a0adcd7d0bce9ffb703fd2
languageName: node
linkType: hard
@@ -9445,21 +9490,6 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^8.18.0":
version: 8.18.1
resolution: "ws@npm:8.18.1"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ">=5.0.2"
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: 10c0/e498965d6938c63058c4310ffb6967f07d4fa06789d3364829028af380d299fe05762961742971c764973dce3d1f6a2633fe8b2d9410c9b52e534b4b882a99fa
languageName: node
linkType: hard
"xml-name-validator@npm:^5.0.0":
version: 5.0.0
resolution: "xml-name-validator@npm:5.0.0"