Merge branch v3.7 into master

This commit is contained in:
romain 2026-04-09 11:46:50 +02:00
commit 786f7192e1
220 changed files with 9606 additions and 1752 deletions

View File

@ -2,7 +2,7 @@ name: Build Web UI
on:
workflow_call: {}
env:
SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS: 360 # 15 days
SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS: 72 # 3 days
jobs:
build-webui:

View File

@ -1,3 +1,46 @@
## [v3.7.0-rc.1](https://github.com/traefik/traefik/tree/v3.7.0-rc.1) (2026-04-07)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.0-ea.3...v3.7.0-rc.1)
**Bug fixes:**
- **[k8s/ingress-nginx]** Fix rewrite-target annotation handling with empty path and non-regex path ([#12905](https://github.com/traefik/traefik/pull/12905) @LBF38)
- **[middleware]** Bump github.com/klauspost/compress v1.18.4 ([#12937](https://github.com/traefik/traefik/pull/12937) @thaJeztah)
**Enhancement:**
- **[webui]** Display server weight in service detail view ([#12325](https://github.com/traefik/traefik/pull/12325) @murataslan1)
- **[webui, tls]** Add certificates menu and overview ([#12628](https://github.com/traefik/traefik/pull/12628) @holomekc)
- **[provider]** Add providers routing precedence configuration ([#12895](https://github.com/traefik/traefik/pull/12895) @juliens)
- **[k8s/ingress-nginx]** Support NGINX global auth annotation ([#12893](https://github.com/traefik/traefik/pull/12893) @foxcool)
- **[k8s/ingress-nginx]** Add limit-burst-multiplier annotation support ([#12899](https://github.com/traefik/traefik/pull/12899) @amazon7737)
- **[k8s/ingress-nginx, k8s/ingress, rules]** Add wildcard host in Host and HostSNI matchers ([#12884](https://github.com/traefik/traefik/pull/12884) @juliens)
- **[k8s/gatewayapi]** Support multiple certificateRefs on gateway listeners ([#12590](https://github.com/traefik/traefik/pull/12590) @mortennordbye)
- **[k8s/gatewayapi]** Add secret support for BackendTLSPolicy caCertificateRefs ([#12927](https://github.com/traefik/traefik/pull/12927) @kevinpollet)
- **[accesslogs, k8s/ingress-nginx]** Support nginx.ingress.kubernetes.io/enable-access-log annotation ([#12908](https://github.com/traefik/traefik/pull/12908) @ris-tlp)
- **[accesslogs, k8s/ingress-nginx, k8s/ingress]** Add Kubernetes Ingress logs fields ([#12913](https://github.com/traefik/traefik/pull/12913) @rtribotte)
**Documentation:**
- **[docker]** Fix docker-compose.yaml location in Docker setup page ([#12860](https://github.com/traefik/traefik/pull/12860) @ScottA38)
- **[docker, consul, ecs, k8s]** Fix documentation on how to restrict the scope of service discovery ([#12645](https://github.com/traefik/traefik/pull/12645) @mloiseleur)
- **[k8s/gatewayapi]** Update gateway-api link in getting-started to v1.5.1 ([#12930](https://github.com/traefik/traefik/pull/12930) @isayme)
- **[k8s/ingress-nginx]** Add OVHcloud (OpenStack Octavia) to Cloud-Specific IP Management ([#12759](https://github.com/traefik/traefik/pull/12759) @antonin-a)
- **[k8s/ingress-nginx]** Clarify IngressClass selection logic ([#12926](https://github.com/traefik/traefik/pull/12926) @kevinpollet)
- Add redirects for deleted pages ([#12889](https://github.com/traefik/traefik/pull/12889) @sheddy-traefik)
- Fix default value of http.sanitizePath ([#12904](https://github.com/traefik/traefik/pull/12904) @iTob191)
## [v3.6.13](https://github.com/traefik/traefik/tree/v3.6.13) (2026-04-07)
[All Commits](https://github.com/traefik/traefik/compare/v3.6.12...v3.6.13)
**Bug fixes:**
- **[middleware]** Bump github.com/klauspost/compress v1.18.4 and fix TestNegotiation ([#12937](https://github.com/traefik/traefik/pull/12937) @thaJeztah)
**Documentation:**
- **[docker]** Fix docker-compose.yaml location in Docker setup page ([#12860](https://github.com/traefik/traefik/pull/12860) @ScottA38)
- **[docker, consul, ecs, k8s]** Fix documentation on how to restrict the scope of service discovery ([#12645](https://github.com/traefik/traefik/pull/12645) @mloiseleur)
- **[k8s/ingress-nginx]** Add OVHcloud (OpenStack Octavia) to Cloud-Specific IP Management ([#12759](https://github.com/traefik/traefik/pull/12759) @antonin-a)
- **[k8s/ingress-nginx]** Clarify IngressClass selection logic ([#12926](https://github.com/traefik/traefik/pull/12926) @kevinpollet)
- Add missing redirects for Getting started ([#12886](https://github.com/traefik/traefik/pull/12886) @nmengin)
- Add redirects for deleted pages ([#12889](https://github.com/traefik/traefik/pull/12889) @sheddy-traefik)
- Fix default value of http.sanitizePath ([#12904](https://github.com/traefik/traefik/pull/12904) @iTob191)
## [v3.7.0-ea.3](https://github.com/traefik/traefik/tree/v3.7.0-ea.3) (2026-03-26)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.0-ea.2...v3.7.0-ea.3)

View File

@ -303,7 +303,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
dialerManager := tcp.NewDialerManager(spiffeX509Source)
acmeHTTPHandler := getHTTPChallengeHandler(acmeProviders, httpChallengeProvider)
managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, observabilityMgr, transportManager, proxyBuilder, acmeHTTPHandler)
managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, observabilityMgr, transportManager, proxyBuilder, acmeHTTPHandler, tlsManager)
// Router factory

View File

@ -250,7 +250,7 @@ To use the Gateway API:
Install the Gateway API CRDs in your cluster:
```bash
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.1/standard-install.yaml
```
Create an HTTPRoute. This configuration:

View File

@ -443,6 +443,42 @@ kubectl get svc -n ingress-nginx ingress-nginx-controller -o go-template='{{ $in
For more details, see the [GKE LoadBalancer Service parameters documentation](https://cloud.google.com/kubernetes-engine/docs/concepts/service-load-balancer-parameters).
??? note "OVHcloud"
OVHcloud supports static IP on OVHcloud Public Load Balancer, it is based on Openstack Octavia which allocates floating IPs to LoadBalancer services. This requires the [Openstack Cloud Controller Manager](https://github.com/kubernetes/cloud-provider-openstack/blob/master/docs/openstack-cloud-controller-manager/using-openstack-cloud-controller-manager.md) to be installed in your cluster. If you are using OVHcloud Managed Kubernetes Service (MKS), the Openstack Cloud Controller Manager is already installed and managed for you.
To retain your existing floating IP when migrating from NGINX to Traefik:
**Identify the existing public IP:**
```bash
NGINX_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \
-o go-template='{{ $ing := index .status.loadBalancer.ingress 0 }}{{ if $ing.ip }}{{ $ing.ip }}{{ else }}{{ $ing.hostname }}{{ end }}')
echo "NGINX IP: $NGINX_IP"
```
**Edit your existing NGINX LoadBalancer service to ensure that the floating IP is not released when the loadbalancer service is deleted:**
kubectl annotate svc my-lb-svc loadbalancer.openstack.org/keep-floatingip=true
```
The `keep-floatingip` annotation prevents the floating IP from being released when the service is deleted or modified.
**Delete the NGINX LoadBalancer service to release the floating IP**
**Update `traefik-values.yaml`:**
```yaml
service:
type: LoadBalancer
spec:
loadBalancerIP: "<your-existing-floating-ip>"
```
To learn more, see the [OVHcloud MKS Public Load Balancer annotations documentation](https://help.ovhcloud.com/csm/en-public-cloud-kubernetes-expose-applications-using-load-balancer?id=kb_article_view&sysparm_article=KB0062878#supported-annotations-features).
??? note "Other Cloud Providers"
- **DigitalOcean:** Supports `loadBalancerIP` with floating IPs

View File

@ -682,3 +682,20 @@ To use the new options of the `retry` middleware with the Kubernetes CRD provide
```shell
kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.7/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
```
### Wildcard Host and HostSNI
Since `v3.7.0`, the `Host` and `HostSNI` matchers support wildcard subdomain matching (e.g., `*.example.com`).
This allows matching any direct subdomain of a domain with a single-level wildcard prefix.
For example, `*.example.com` matches `foo.example.com` but not `foo.bar.example.com` or `example.com` itself.
This feature is only available with the v3 rule syntax (the default).
#### TLSOptions with Wildcard Domains
Since `v3.7.0`, TLSOptions can now be associated with routers using wildcard `Host` and `HostSNI` matchers (e.g., `Host(`*.example.com`)`).
This enables configuring different TLS options for wildcard domains.
Previously, TLSOptions selection was limited to exact `Host` matches, and using `HostRegexp` or wildcards would fall back to the default TLS options with a warning message like: `No domain found in rule HostRegexp(...) the TLS option foo cannot be applied`.
Note: TLSOptions for `HostRegexp` matchers remains unsupported. Use wildcard `Host` matchers as an alternative.

View File

@ -403,6 +403,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetesingressnginx-disablesvcexternalname" href="#opt-providers-kubernetesingressnginx-disablesvcexternalname" title="#opt-providers-kubernetesingressnginx-disablesvcexternalname">providers.kubernetesingressnginx.disablesvcexternalname</a> | Disable support for Services of type ExternalName. | false |
| <a id="opt-providers-kubernetesingressnginx-endpoint" href="#opt-providers-kubernetesingressnginx-endpoint" title="#opt-providers-kubernetesingressnginx-endpoint">providers.kubernetesingressnginx.endpoint</a> | Kubernetes server endpoint (required for external cluster client). | |
| <a id="opt-providers-kubernetesingressnginx-globalallowedresponseheaders" href="#opt-providers-kubernetesingressnginx-globalallowedresponseheaders" title="#opt-providers-kubernetesingressnginx-globalallowedresponseheaders">providers.kubernetesingressnginx.globalallowedresponseheaders</a> | List of allowed response headers inside the custom headers annotations. | |
| <a id="opt-providers-kubernetesingressnginx-globalauthurl" href="#opt-providers-kubernetesingressnginx-globalauthurl" title="#opt-providers-kubernetesingressnginx-globalauthurl">providers.kubernetesingressnginx.globalauthurl</a> | URL to the service that provides authentication for all the locations. Per ingress auth-url annotation has precedence over this option. | |
| <a id="opt-providers-kubernetesingressnginx-httpentrypoint" href="#opt-providers-kubernetesingressnginx-httpentrypoint" title="#opt-providers-kubernetesingressnginx-httpentrypoint">providers.kubernetesingressnginx.httpentrypoint</a> | Defines the EntryPoint to use for HTTP requests. | |
| <a id="opt-providers-kubernetesingressnginx-httpsentrypoint" href="#opt-providers-kubernetesingressnginx-httpsentrypoint" title="#opt-providers-kubernetesingressnginx-httpsentrypoint">providers.kubernetesingressnginx.httpsentrypoint</a> | Defines the EntryPoint to use for HTTPS requests. | |
| <a id="opt-providers-kubernetesingressnginx-ingressclass" href="#opt-providers-kubernetesingressnginx-ingressclass" title="#opt-providers-kubernetesingressnginx-ingressclass">providers.kubernetesingressnginx.ingressclass</a> | Name of the ingress class this controller satisfies. | nginx |
@ -447,6 +448,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-nomad-throttleduration" href="#opt-providers-nomad-throttleduration" title="#opt-providers-nomad-throttleduration">providers.nomad.throttleduration</a> | Watch throttle duration. | 0 |
| <a id="opt-providers-nomad-watch" href="#opt-providers-nomad-watch" title="#opt-providers-nomad-watch">providers.nomad.watch</a> | Watch Nomad Service events. | false |
| <a id="opt-providers-plugin-name" href="#opt-providers-plugin-name" title="#opt-providers-plugin-name">providers.plugin._name_</a> | Plugins configuration. | |
| <a id="opt-providers-precedence" href="#opt-providers-precedence" title="#opt-providers-precedence">providers.precedence</a> | Defines the routing precedence between providers. | kubernetesgateway, kubernetescrd, kubernetes, kubernetesingressnginx, swarm, docker, file, redis, knative, consul, consulcatalog, nomad, etcd, ecs, http, zookeeper, rest |
| <a id="opt-providers-providersthrottleduration" href="#opt-providers-providersthrottleduration" title="#opt-providers-providersthrottleduration">providers.providersthrottleduration</a> | Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time. | 2 |
| <a id="opt-providers-redis" href="#opt-providers-redis" title="#opt-providers-redis">providers.redis</a> | Enables Redis provider. | false |
| <a id="opt-providers-redis-db" href="#opt-providers-redis-db" title="#opt-providers-redis-db">providers.redis.db</a> | Database to be selected after connecting to the server. | 0 |

View File

@ -105,7 +105,7 @@ additionalArguments:
| <a id="opt-http-encodedCharacters-allowEncodedQuestionMark" href="#opt-http-encodedCharacters-allowEncodedQuestionMark" title="#opt-http-encodedCharacters-allowEncodedQuestionMark">`http.encodedCharacters.`<br />`allowEncodedQuestionMark`</a> | Defines whether requests with encoded question mark characters in the path are allowed. | true | No |
| <a id="opt-http-encodedCharacters-allowEncodedHash" href="#opt-http-encodedCharacters-allowEncodedHash" title="#opt-http-encodedCharacters-allowEncodedHash">`http.encodedCharacters.`<br />`allowEncodedHash`</a> | Defines whether requests with encoded hash characters in the path are allowed. | true | No |
| <a id="opt-http-encodeQuerySemicolons" href="#opt-http-encodeQuerySemicolons" title="#opt-http-encodeQuerySemicolons">`http.encodeQuerySemicolons`</a> | Enable query semicolons encoding. <br /> Use this option to avoid non-encoded semicolons to be interpreted as query parameter separators by Traefik. <br /> When using this option, the non-encoded semicolons characters in query will be transmitted encoded to the backend.<br /> More information [here](#encodequerysemicolons). | false | No |
| <a id="opt-http-sanitizePath" href="#opt-http-sanitizePath" title="#opt-http-sanitizePath">`http.sanitizePath`</a> | Defines whether to enable the request path sanitization.<br /> More information [here](#sanitizepath). | false | No |
| <a id="opt-http-sanitizePath" href="#opt-http-sanitizePath" title="#opt-http-sanitizePath">`http.sanitizePath`</a> | Defines whether to enable the request path sanitization.<br /> More information [here](#sanitizepath). | true | No |
| <a id="opt-http-maxHeaderBytes" href="#opt-http-maxHeaderBytes" title="#opt-http-maxHeaderBytes">`http.maxHeaderBytes`</a> | Set the maximum size of request headers in bytes. | 1048576 | No |
| <a id="opt-http-middlewares" href="#opt-http-middlewares" title="#opt-http-middlewares">`http.middlewares`</a> | Set the list of middlewares that are prepended by default to the list of middlewares of each router associated to the named entry point. <br />More information [here](#httpmiddlewares). | - | No |
| <a id="opt-http-tls" href="#opt-http-tls" title="#opt-http-tls">`http.tls`</a> | Enable TLS on every router attached to the `entryPoint`. <br /> If no certificate are set, a default self-signed certificate is generated by Traefik. <br /> We recommend to not use self signed certificates in production. | - | No |

View File

@ -384,6 +384,10 @@ Below the fields displayed with the generic CLF format:
| <a id="opt-TLSVersion" href="#opt-TLSVersion" title="#opt-TLSVersion">`TLSVersion`</a> | The TLS version used by the connection (e.g. `1.2`) (if connection is TLS). |
| <a id="opt-TLSCipher" href="#opt-TLSCipher" title="#opt-TLSCipher">`TLSCipher`</a> | The TLS cipher used by the connection (e.g. `TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA`) (if connection is TLS). |
| <a id="opt-TLSClientSubject" href="#opt-TLSClientSubject" title="#opt-TLSClientSubject">`TLSClientSubject`</a> | The string representation of the TLS client certificate's Subject (e.g. `CN=username,O=organization`). |
| <a id="opt-KubernetesIngressNamespace" href="#opt-KubernetesIngressNamespace" title="#opt-KubernetesIngressNamespace">`KubernetesIngressNamespace`</a> | The namespace of the Kubernetes Ingress resource the router handles. Only available with the Kubernetes Ingress and Kubernetes Ingress Nginx providers. |
| <a id="opt-KubernetesIngressName" href="#opt-KubernetesIngressName" title="#opt-KubernetesIngressName">`KubernetesIngressName`</a> | The name of the Kubernetes Ingress resource the router handles. Only available with the Kubernetes Ingress and Kubernetes Ingress Nginx providers. |
| <a id="opt-KubernetesServiceName" href="#opt-KubernetesServiceName" title="#opt-KubernetesServiceName">`KubernetesServiceName`</a> | The name of the Kubernetes Service associated with the Ingress the router handles. Only available with the Kubernetes Ingress and Kubernetes Ingress Nginx providers. |
| <a id="opt-KubernetesServicePort" href="#opt-KubernetesServicePort" title="#opt-KubernetesServicePort">`KubernetesServicePort`</a> | The port of the Kubernetes Service associated with the Ingress the router handles. Only available with the Kubernetes Ingress and Kubernetes Ingress Nginx providers. |
### Log Rotation

View File

@ -45,7 +45,7 @@ services:
| <a id="opt-providers-docker-username" href="#opt-providers-docker-username" title="#opt-providers-docker-username">`providers.docker.username`</a> | Defines the username for Basic HTTP authentication. This should be used when the Docker daemon socket is exposed through an HTTP proxy that requires Basic HTTP authentication.| "" | No |
| <a id="opt-providers-docker-password" href="#opt-providers-docker-password" title="#opt-providers-docker-password">`providers.docker.password`</a> | Defines the password for Basic HTTP authentication. This should be used when the Docker daemon socket is exposed through an HTTP proxy that requires Basic HTTP authentication.| "" | No |
| <a id="opt-providers-docker-useBindPortIP" href="#opt-providers-docker-useBindPortIP" title="#opt-providers-docker-useBindPortIP">`providers.docker.useBindPortIP`</a> | Instructs Traefik to use the IP/Port attached to the container's binding instead of its inner network IP/Port. See [here](#usebindportip) for more information | false | No |
| <a id="opt-providers-docker-exposedByDefault" href="#opt-providers-docker-exposedByDefault" title="#opt-providers-docker-exposedByDefault">`providers.docker.exposedByDefault`</a> | Expose containers by default through Traefik. See [here](./overview.md#exposedbydefault-and-traefikenable) for additional information | true | No |
| <a id="opt-providers-docker-exposedByDefault" href="#opt-providers-docker-exposedByDefault" title="#opt-providers-docker-exposedByDefault">`providers.docker.exposedByDefault`</a> | Expose containers by default through Traefik. If set to _false_, containers that do not have a `traefik.enable=true` label are ignored from the resulting routing configuration.<br>See [here](./overview.md#restrict-the-scope-of-service-discovery) for additional information | true | No |
| <a id="opt-providers-docker-network" href="#opt-providers-docker-network" title="#opt-providers-docker-network">`providers.docker.network`</a> | Defines a default docker network to use for connections to all containers. This option can be overridden on a per-container basis with the `traefik.docker.network` label.| "" | No |
| <a id="opt-providers-docker-defaultRule" href="#opt-providers-docker-defaultRule" title="#opt-providers-docker-defaultRule">`providers.docker.defaultRule`</a> | Defines what routing rule to apply to a container if no rule is defined by a label. See [here](#defaultrule) for more information. | ```"Host(`{{ normalize .Name }}`)"``` | No |
| <a id="opt-providers-docker-httpClientTimeout" href="#opt-providers-docker-httpClientTimeout" title="#opt-providers-docker-httpClientTimeout">`providers.docker.httpClientTimeout`</a> | Defines the client timeout (in seconds) for HTTP connections. If its value is 0, no timeout is set. | 0 | No |

View File

@ -36,7 +36,7 @@ Attaching tags to services:
| <a id="opt-providers-consulCatalog-refreshInterval" href="#opt-providers-consulCatalog-refreshInterval" title="#opt-providers-consulCatalog-refreshInterval">`providers.consulCatalog.refreshInterval`</a> | Defines the polling interval.| 15s | No |
| <a id="opt-providers-consulCatalog-prefix" href="#opt-providers-consulCatalog-prefix" title="#opt-providers-consulCatalog-prefix">`providers.consulCatalog.prefix`</a> | Defines the prefix for Consul Catalog tags defining Traefik labels.| traefik | yes |
| <a id="opt-providers-consulCatalog-requireConsistent" href="#opt-providers-consulCatalog-requireConsistent" title="#opt-providers-consulCatalog-requireConsistent">`providers.consulCatalog.requireConsistent`</a> | Forces the read to be fully consistent. See [here](#requireconsistent) for more information.| false | yes |
| <a id="opt-providers-consulCatalog-exposedByDefault" href="#opt-providers-consulCatalog-exposedByDefault" title="#opt-providers-consulCatalog-exposedByDefault">`providers.consulCatalog.exposedByDefault`</a> | Expose Consul Catalog services by default in Traefik. If set to `false`, services that do not have a `traefik.enable=true` tag will be ignored from the resulting routing configuration. See [here](../overview.md#exposedbydefault-and-traefikenable). | true | no |
| <a id="opt-providers-consulCatalog-exposedByDefault" href="#opt-providers-consulCatalog-exposedByDefault" title="#opt-providers-consulCatalog-exposedByDefault">`providers.consulCatalog.exposedByDefault`</a> | Expose Consul Catalog services by default through Traefik. If set to _false_, services that do not have a `traefik.enable=true` tag are ignored from the resulting routing configuration.<br>See [here](../overview.md#restrict-the-scope-of-service-discovery) for additional information. | true | no |
| <a id="opt-providers-consulCatalog-defaultRule" href="#opt-providers-consulCatalog-defaultRule" title="#opt-providers-consulCatalog-defaultRule">`providers.consulCatalog.defaultRule`</a> | The Default Host rule for all services. See [here](#defaultrule) for more information. | ```"Host(`{{ normalize .Name }}`)"``` | No |
| <a id="opt-providers-consulCatalog-connectAware" href="#opt-providers-consulCatalog-connectAware" title="#opt-providers-consulCatalog-connectAware">`providers.consulCatalog.connectAware`</a> | Enable Consul Connect support. If set to `true`, Traefik will be enabled to communicate with Connect services. | false | No |
| <a id="opt-providers-consulCatalog-connectByDefault" href="#opt-providers-consulCatalog-connectByDefault" title="#opt-providers-consulCatalog-connectByDefault">`providers.consulCatalog.connectByDefault`</a> | Consider every service as Connect capable by default. If set to true, Traefik will consider every Consul Catalog service to be Connect capable by default. The option can be overridden on an instance basis with the traefik.consulcatalog.connect tag. | false | No |

View File

@ -75,7 +75,7 @@ providers:
| <a id="opt-providers-kubernetesGateway-token" href="#opt-providers-kubernetesGateway-token" title="#opt-providers-kubernetesGateway-token">`providers.kubernetesGateway.token`</a> | Bearer token used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesGateway-certAuthFilePath" href="#opt-providers-kubernetesGateway-certAuthFilePath" title="#opt-providers-kubernetesGateway-certAuthFilePath">`providers.kubernetesGateway.certAuthFilePath`</a> | Path to the certificate authority file.<br />Used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesGateway-namespaces" href="#opt-providers-kubernetesGateway-namespaces" title="#opt-providers-kubernetesGateway-namespaces">`providers.kubernetesGateway.namespaces`</a> | Array of namespaces to watch.<br />If left empty, watch all namespaces. | [] | No |
| <a id="opt-providers-kubernetesGateway-labelSelector" href="#opt-providers-kubernetesGateway-labelSelector" title="#opt-providers-kubernetesGateway-labelSelector">`providers.kubernetesGateway.labelSelector`</a> | Allow filtering on specific resource objects only using label selectors.<br />Only to Traefik [Custom Resources](./kubernetes-crd.md#routing-configuration) (they all must match the filter).<br />No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.<br />See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
| <a id="opt-providers-kubernetesGateway-labelselector" href="#opt-providers-kubernetesGateway-labelselector" title="#opt-providers-kubernetesGateway-labelselector">`providers.kubernetesGateway.labelselector`</a> | Allow filtering on `GatewayClass` only. If left empty, Traefik processes all GatewayClass objects in the configured namespaces.<br />See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
| <a id="opt-providers-kubernetesGateway-throttleDuration" href="#opt-providers-kubernetesGateway-throttleDuration" title="#opt-providers-kubernetesGateway-throttleDuration">`providers.kubernetesGateway.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-kubernetesGateway-nativeLBByDefault" href="#opt-providers-kubernetesGateway-nativeLBByDefault" title="#opt-providers-kubernetesGateway-nativeLBByDefault">`providers.kubernetesGateway.nativeLBByDefault`</a> | Defines whether to use Native Kubernetes load-balancing mode by default. For more information, please check out the `traefik.io/service.nativelb` service annotation documentation. | false | No |
| <a id="opt-providers-kubernetesGateway-statusAddress-hostname" href="#opt-providers-kubernetesGateway-statusAddress-hostname" title="#opt-providers-kubernetesGateway-statusAddress-hostname">`providers.kubernetesGateway.`<br />`statusAddress.hostname`</a> | Hostname copied to the Gateway `status.addresses`. | "" | No |

View File

@ -42,6 +42,12 @@ This provider discovers all Ingresses in the cluster by default, which may lead
- Configure `watchNamespace` to limit discovery to specific namespaces
- Use `watchNamespaceSelector` to target Ingresses based on namespace labels
### IngressClass Selection Logic
By default, the provider selects all IngressClasses whose `spec.controller` matches `controllerClass` (default: `k8s.io/ingress-nginx`) and picks up every Ingress referencing any of them.
Setting `ingressClassByName: true` adds a second inclusion path: IngressClasses whose name matches `ingressClass` are also picked up, even if their `spec.controller` does not match `controllerClass`.
It does not narrow down the controller-based selection — both paths apply independently.
## Configuration Example
You can enable the Kubernetes Ingress NGINX provider as detailed below:
@ -160,10 +166,10 @@ This provider watches for incoming Ingress events and automatically translates N
| <a id="opt-providers-kubernetesIngressNGINX-throttleDuration" href="#opt-providers-kubernetesIngressNGINX-throttleDuration" title="#opt-providers-kubernetesIngressNGINX-throttleDuration">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`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-kubernetesIngressNGINX-watchNamespace" href="#opt-providers-kubernetesIngressNGINX-watchNamespace" title="#opt-providers-kubernetesIngressNGINX-watchNamespace">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`watchNamespace`</a> | Namespace the controller watches for updates to Kubernetes objects. All namespaces are watched if this parameter is left empty. | "" | No |
| <a id="opt-providers-kubernetesIngressNGINX-watchNamespaceSelector" href="#opt-providers-kubernetesIngressNGINX-watchNamespaceSelector" title="#opt-providers-kubernetesIngressNGINX-watchNamespaceSelector">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`watchNamespaceSelector`</a> | Selector selects namespaces the controller watches for updates to Kubernetes objects. | "" | No |
| <a id="opt-providers-kubernetesIngressNGINX-ingressClass" href="#opt-providers-kubernetesIngressNGINX-ingressClass" title="#opt-providers-kubernetesIngressNGINX-ingressClass">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`ingressClass`</a> | Name of the ingress class this controller satisfies. | "nginx" | No |
| <a id="opt-providers-kubernetesIngressNGINX-ingressClass" href="#opt-providers-kubernetesIngressNGINX-ingressClass" title="#opt-providers-kubernetesIngressNGINX-ingressClass">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`ingressClass`</a> | Name of the IngressClass this controller handles. When `ingressClassByName` is `true`, IngressClasses with this name are included in discovery regardless of their `spec.controller` value. | "nginx" | No |
| <a id="opt-providers-kubernetesIngressNGINX-controllerClass" href="#opt-providers-kubernetesIngressNGINX-controllerClass" title="#opt-providers-kubernetesIngressNGINX-controllerClass">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`controllerClass`</a> | Ingress Class Controller value this controller satisfies. | "" | No |
| <a id="opt-providers-kubernetesIngressNGINX-watchIngressWithoutClass" href="#opt-providers-kubernetesIngressNGINX-watchIngressWithoutClass" title="#opt-providers-kubernetesIngressNGINX-watchIngressWithoutClass">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`watchIngressWithoutClass`</a> | Define if Ingress Controller should also watch for Ingresses without an IngressClass or the annotation specified. | false | No |
| <a id="opt-providers-kubernetesIngressNGINX-ingressClassByName" href="#opt-providers-kubernetesIngressNGINX-ingressClassByName" title="#opt-providers-kubernetesIngressNGINX-ingressClassByName">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`ingressClassByName`</a> | Define if Ingress Controller should watch for Ingress Class by Name together with Controller Class. | false | No |
| <a id="opt-providers-kubernetesIngressNGINX-ingressClassByName" href="#opt-providers-kubernetesIngressNGINX-ingressClassByName" title="#opt-providers-kubernetesIngressNGINX-ingressClassByName">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`ingressClassByName`</a> | When `true`, any IngressClass whose **name** matches `ingressClass` is include in discovery, even if its `spec.controller` does not match `controllerClass`. This is evaluated alongside the controller-based selection, not instead of it. | false | No |
| <a id="opt-providers-kubernetesIngressNGINX-publishService" href="#opt-providers-kubernetesIngressNGINX-publishService" title="#opt-providers-kubernetesIngressNGINX-publishService">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`publishService`</a> | Service fronting the Ingress controller. Takes the form `namespace/name`. | "" | No |
| <a id="opt-providers-kubernetesIngressNGINX-publishStatusAddress" href="#opt-providers-kubernetesIngressNGINX-publishStatusAddress" title="#opt-providers-kubernetesIngressNGINX-publishStatusAddress">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`publishStatusAddress`</a> | Customized address (or addresses, separated by comma) to set as the load-balancer status of Ingress objects this controller satisfies. | "" | No |
| <a id="opt-providers-kubernetesIngressNGINX-defaultBackendService" href="#opt-providers-kubernetesIngressNGINX-defaultBackendService" title="#opt-providers-kubernetesIngressNGINX-defaultBackendService">`providers.`<br/>`kubernetesIngressNGINX.`<br/>`defaultBackendService`</a> | Service used to serve HTTP requests not matching any known server name (catch-all). Takes the form 'namespace/name'. | "" | No |

View File

@ -52,7 +52,7 @@ which in turn creates the resulting routers, services, handlers, etc.
| <a id="opt-providers-kubernetesIngress-token" href="#opt-providers-kubernetesIngress-token" title="#opt-providers-kubernetesIngress-token">`providers.kubernetesIngress.token`</a> | Bearer token used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesIngress-certAuthFilePath" href="#opt-providers-kubernetesIngress-certAuthFilePath" title="#opt-providers-kubernetesIngress-certAuthFilePath">`providers.kubernetesIngress.certAuthFilePath`</a> | Path to the certificate authority file.<br />Used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesIngress-namespaces" href="#opt-providers-kubernetesIngress-namespaces" title="#opt-providers-kubernetesIngress-namespaces">`providers.kubernetesIngress.namespaces`</a> | Array of namespaces to watch.<br />If left empty, watch all namespaces. | | No |
| <a id="opt-providers-kubernetesIngress-labelSelector" href="#opt-providers-kubernetesIngress-labelSelector" title="#opt-providers-kubernetesIngress-labelSelector">`providers.kubernetesIngress.labelSelector`</a> | Allow filtering on Ingress objects using label selectors.<br />No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.<br />See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
| <a id="opt-providers-kubernetesIngress-labelselector" href="#opt-providers-kubernetesIngress-labelselector" title="#opt-providers-kubernetesIngress-labelselector">`providers.kubernetesIngress.labelselector`</a> | Allow filtering on `Ingress` objects using label selectors.<br />No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.<br />See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
| <a id="opt-providers-kubernetesIngress-ingressClass" href="#opt-providers-kubernetesIngress-ingressClass" title="#opt-providers-kubernetesIngress-ingressClass">`providers.kubernetesIngress.ingressClass`</a> | The `IngressClass` resource name or the `kubernetes.io/ingress.class` annotation value that identifies resource objects to be processed.<br />If empty, resources missing the annotation, having an empty value, or the value `traefik` are processed. | "" | No |
| <a id="opt-providers-kubernetesIngress-disableIngressClassLookup" href="#opt-providers-kubernetesIngress-disableIngressClassLookup" title="#opt-providers-kubernetesIngress-disableIngressClassLookup">`providers.kubernetesIngress.disableIngressClassLookup`</a> | Prevent to discover IngressClasses in the cluster.<br />It alleviates the requirement of giving Traefik the rights to look IngressClasses up.<br />Ignore Ingresses with IngressClass.<br />Annotations are not affected by this option. | false | No |
| <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 |

View File

@ -30,7 +30,7 @@ providers:
| <a id="opt-providers-ecs-autoDiscoverClusters" href="#opt-providers-ecs-autoDiscoverClusters" title="#opt-providers-ecs-autoDiscoverClusters">`providers.ecs.autoDiscoverClusters`</a> | Search for services in cluster list. If set to `true` service discovery is enabled for all clusters. | false | No |
| <a id="opt-providers-ecs-ecsAnywhere" href="#opt-providers-ecs-ecsAnywhere" title="#opt-providers-ecs-ecsAnywhere">`providers.ecs.ecsAnywhere`</a> | Enable ECS Anywhere support. | false | No |
| <a id="opt-providers-ecs-clusters" href="#opt-providers-ecs-clusters" title="#opt-providers-ecs-clusters">`providers.ecs.clusters`</a> | Search for services in cluster list. This option is ignored if `autoDiscoverClusters` is set to `true`. | `["default"]` | No |
| <a id="opt-providers-ecs-exposedByDefault" href="#opt-providers-ecs-exposedByDefault" title="#opt-providers-ecs-exposedByDefault">`providers.ecs.exposedByDefault`</a> | Expose ECS services by default in Traefik. | true | No |
| <a id="opt-providers-ecs-exposedByDefault" href="#opt-providers-ecs-exposedByDefault" title="#opt-providers-ecs-exposedByDefault">`providers.ecs.exposedByDefault`</a> | Expose ECS services by default through Traefik. If set to _false_, containers that do not have a `traefik.enable=true` label are ignored from the resulting routing configuration.<br>See [here](../overview.md#restrict-the-scope-of-service-discovery) for additional information. | true | No |
| <a id="opt-providers-ecs-constraints" href="#opt-providers-ecs-constraints" title="#opt-providers-ecs-constraints">`providers.ecs.constraints`</a> | Defines an expression that Traefik matches against the container labels to determine whether to create any route for that container. See [here](#constraints) for more information. | true | No |
| <a id="opt-providers-ecs-healthyTasksOnly" href="#opt-providers-ecs-healthyTasksOnly" title="#opt-providers-ecs-healthyTasksOnly">`providers.ecs.healthyTasksOnly`</a> | Defines whether Traefik discovers only healthy tasks (`HEALTHY` healthStatus). | false | No |
| <a id="opt-providers-ecs-defaultRule" href="#opt-providers-ecs-defaultRule" title="#opt-providers-ecs-defaultRule">`providers.ecs.defaultRule`</a> | The Default Host rule for all services. See [here](#defaultrule) for more information. | ```"Host(`{{ normalize .Name }}`)"``` | No |

View File

@ -133,31 +133,94 @@ metadata:
spec:
```
## Restrict the Scope of Service Discovery
By default, Traefik creates routes for all detected containers.
If you want to limit the scope of the Traefik service discovery,
i.e. disallow route creation for some containers,
you can do so in two different ways:
- the generic configuration option `exposedByDefault`,
- a finer granularity mechanism based on constraints.
1. With [Consul Catalog](./hashicorp/consul-catalog.md#opt-providers-consulCatalog-exposedByDefault),
[Docker](./docker.md#opt-providers-docker-exposedByDefault),
[ECS](./others/ecs.md#opt-providers-ecs-exposedByDefault),
[Nomad](./hashicorp/nomad.md#opt-providers-nomad-exposedByDefault) and
[Swarm](./swarm.md#opt-providers-swarm-exposedByDefault)
providers, you can set `exposedByDefault` to `false` and add a label `traefik.enable=true`
on containers you want to expose
### `exposedByDefault` and `traefik.enable`
2. Use a finer-grained mechanism based on label selector or constraints.
List of providers that support these features:
!!! info "The following providers support constraints"
- [Docker](./docker.md#configuration-options)
- [ECS](./others/ecs.md#configuration-options)
- [Consul Catalog](./hashicorp/consul-catalog.md#configuration-options)
- [Nomad](./hashicorp/nomad.md#configuration-options)
- [Consul Catalog](./hashicorp/consul-catalog.md#constraints)
- [Docker](./docker.md#constraints)
- [ECS](./others/ecs.md#constraints)
- [Nomad](./hashicorp/nomad.md#constraints)
- [Swarm](./swarm.md#constraints)
### Constraints
!!! info "The following providers support label selectors"
List of providers that support constraints:
- [Kubernetes CRD](./kubernetes/kubernetes-crd.md#opt-providers-kubernetesCRD-labelselector)
- [Kubernetes Gateway API](./kubernetes/kubernetes-gateway.md#opt-providers-kubernetesGateway-labelselector)
- [Kubernetes Ingress](./kubernetes/kubernetes-ingress.md#opt-providers-kubernetesIngress-labelselector)
- [Docker](./docker.md#constraints)
- [ECS](./others/ecs.md#constraints)
- [Consul Catalog](./hashicorp/consul-catalog.md#constraints)
- [Nomad](./hashicorp/nomad.md#constraints)
## Providers Precedence
### `providers.precedence`
_Optional_
When two routers from **different providers** define the same rule with equal numeric [priority](../../routing-configuration/http/routing/rules-and-priority.md#priority-calculation),
the `precedence` option determines which provider's route takes precedence.
The list is ordered from highest to lowest precedence: a provider listed first wins over providers listed later.
```yaml tab="File (YAML)"
providers:
precedence:
- kubernetescrd
- kubernetes
- file
```
```toml tab="File (TOML)"
[providers]
precedence = ["kubernetescrd", "kubernetes", "file"]
```
```bash tab="CLI"
--providers.precedence=kubernetescrd,kubernetes,file
```
#### Default precedence
When `precedence` is not set, Traefik uses the following default order (highest precedence first):
| Position | Provider name |
|----------|--------------------------|
| <a id="opt-1" href="#opt-1" title="#opt-1">1</a> | `kubernetesgateway` |
| <a id="opt-2" href="#opt-2" title="#opt-2">2</a> | `kubernetescrd` |
| <a id="opt-3" href="#opt-3" title="#opt-3">3</a> | `kubernetes` |
| <a id="opt-4" href="#opt-4" title="#opt-4">4</a> | `kubernetesingressnginx` |
| <a id="opt-5" href="#opt-5" title="#opt-5">5</a> | `swarm` |
| <a id="opt-6" href="#opt-6" title="#opt-6">6</a> | `docker` |
| <a id="opt-7" href="#opt-7" title="#opt-7">7</a> | `file` |
| <a id="opt-8" href="#opt-8" title="#opt-8">8</a> | `redis` |
| <a id="opt-9" href="#opt-9" title="#opt-9">9</a> | `knative` |
| <a id="opt-10" href="#opt-10" title="#opt-10">10</a> | `consul` |
| <a id="opt-11" href="#opt-11" title="#opt-11">11</a> | `consulcatalog` |
| <a id="opt-12" href="#opt-12" title="#opt-12">12</a> | `nomad` |
| <a id="opt-13" href="#opt-13" title="#opt-13">13</a> | `etcd` |
| <a id="opt-14" href="#opt-14" title="#opt-14">14</a> | `ecs` |
| <a id="opt-15" href="#opt-15" title="#opt-15">15</a> | `http` |
| <a id="opt-16" href="#opt-16" title="#opt-16">16</a> | `zookeeper` |
| <a id="opt-17" href="#opt-17" title="#opt-17">17</a> | `rest` |
!!! note
- `precedence` only acts as a **tiebreaker**: it is applied only when two routes from different providers share the same numeric `priority` value. An explicit router priority always takes precedence.
- A provider absent from `precedence` loses to any listed provider.
- Provider names are case-insensitive.
{% include-markdown "includes/traefik-for-business-applications.md" %}

View File

@ -50,7 +50,7 @@ services:
| <a id="opt-providers-swarm-username" href="#opt-providers-swarm-username" title="#opt-providers-swarm-username">`providers.swarm.username`</a> | Defines the username for Basic HTTP authentication. This should be used when the Docker daemon socket is exposed through an HTTP proxy that requires Basic HTTP authentication. | "" | No |
| <a id="opt-providers-swarm-password" href="#opt-providers-swarm-password" title="#opt-providers-swarm-password">`providers.swarm.password`</a> | Defines the password for Basic HTTP authentication. This should be used when the Docker daemon socket is exposed through an HTTP proxy that requires Basic HTTP authentication. | "" | No |
| <a id="opt-providers-swarm-useBindPortIP" href="#opt-providers-swarm-useBindPortIP" title="#opt-providers-swarm-useBindPortIP">`providers.swarm.useBindPortIP`</a> | Instructs Traefik to use the IP/Port attached to the container's binding instead of its inner network IP/Port. See [here](#usebindportip) for more information | false | No |
| <a id="opt-providers-swarm-exposedByDefault" href="#opt-providers-swarm-exposedByDefault" title="#opt-providers-swarm-exposedByDefault">`providers.swarm.exposedByDefault`</a> | Expose containers by default through Traefik. See [here](./overview.md#exposedbydefault-and-traefikenable) for additional information | true | No |
| <a id="opt-providers-swarm-exposedByDefault" href="#opt-providers-swarm-exposedByDefault" title="#opt-providers-swarm-exposedByDefault">`providers.swarm.exposedByDefault`</a> | Expose containers by default through Traefik. If set to _false_, containers that do not have a `traefik.enable=true` label are excluded from the resulting routing configuration.<br>See [here](./overview.md#restrict-the-scope-of-service-discovery) for additional information | true | No |
| <a id="opt-providers-swarm-network" href="#opt-providers-swarm-network" title="#opt-providers-swarm-network">`providers.swarm.network`</a> | Defines a default docker network to use for connections to all containers. This option can be overridden on a per-container basis with the `traefik.swarm.network` label. | "" | No |
| <a id="opt-providers-swarm-defaultRule" href="#opt-providers-swarm-defaultRule" title="#opt-providers-swarm-defaultRule">`providers.swarm.defaultRule`</a> | Defines what routing rule to apply to a container if no rule is defined by a label. See [here](#defaultrule) for more information | ```"Host(`{{ normalize .Name }}`)"``` | No |
| <a id="opt-providers-swarm-refreshSeconds" href="#opt-providers-swarm-refreshSeconds" title="#opt-providers-swarm-refreshSeconds">`providers.swarm.refreshSeconds`</a> | Defines the polling interval for Swarm Mode. | "15s" | No |

View File

@ -24,7 +24,7 @@ The table below lists all the available matchers:
|-----------------------------------------------------------------|:-------------------------------------------------------------------------------|
| <a id="opt-Headerkey-value" href="#opt-Headerkey-value" title="#opt-Headerkey-value">[```Header(`key`, `value`)```](#header-and-headerregexp)</a> | Matches requests containing a header named `key` set to `value`. |
| <a id="opt-HeaderRegexpkey-regexp" href="#opt-HeaderRegexpkey-regexp" title="#opt-HeaderRegexpkey-regexp">[```HeaderRegexp(`key`, `regexp`)```](#header-and-headerregexp)</a> | Matches requests containing a header named `key` matching `regexp`. |
| <a id="opt-Hostdomain" href="#opt-Hostdomain" title="#opt-Hostdomain">[```Host(`domain`)```](#host-and-hostregexp)</a> | Matches requests host set to `domain`. |
| <a id="opt-Hostdomain" href="#opt-Hostdomain" title="#opt-Hostdomain">[```Host(`domain`)```](#host-and-hostregexp)</a> | Matches requests host set to `domain`. Supports wildcard subdomain matching (e.g. `*.example.com`). |
| <a id="opt-HostRegexpregexp" href="#opt-HostRegexpregexp" title="#opt-HostRegexpregexp">[```HostRegexp(`regexp`)```](#host-and-hostregexp)</a> | Matches requests host matching `regexp`. |
| <a id="opt-Methodmethod" href="#opt-Methodmethod" title="#opt-Methodmethod">[```Method(`method`)```](#method)</a> | Matches requests method set to `method`. |
| <a id="opt-Pathpath" href="#opt-Pathpath" title="#opt-Pathpath">[```Path(`path`)```](#path-pathprefix-and-pathregexp)</a> | Matches requests path set to `path`. |
@ -54,6 +54,15 @@ If no `Host` is set in the request URL (for example, it's an IP address), these
These matchers will match the request's host in lowercase.
!!! info "Wildcard subdomain matching"
The `Host` matcher supports a single-level wildcard prefix (`*.example.com`) to match any direct subdomain of `example.com`.
It should be preferred over the `HostRegexp` matcher as it allows attaching a TLS option and is more efficient.
A wildcard matches exactly one subdomain label: `*.example.com` matches `foo.example.com` but not `foo.bar.example.com` or `example.com` itself.
This is only available with the **v3 rule syntax** (the default).
| Behavior | Rule |
|-----------------------------------------------------------------|:------------------------------------------------------------------------|
| <a id="opt-Match-requests-with-Host-set-to-example-com" href="#opt-Match-requests-with-Host-set-to-example-com" title="#opt-Match-requests-with-Host-set-to-example-com">Match requests with `Host` set to `example.com`.</a> | ```Host(`example.com`)``` |
@ -232,6 +241,12 @@ Traefik reserves a range of priorities for its internal routers, the maximum use
- `(MaxInt32 - 1000)` = `2147482647` for 32-bit platforms,
- `(MaxInt64 - 1000)` = `9223372036854774807` for 64-bit platforms.
!!! info "Providers Precedence"
When two routes from **different providers** share the same numeric priority,
Traefik uses the [`providers.precedence`](../../../install-configuration/providers/overview.md#providers-precedence) install configuration option to determine which route takes precedence.
The provider listed first in `precedence` wins the tie.
### Example
```yaml tab="Structured (YAML)"

View File

@ -110,6 +110,36 @@ data:
LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRUUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Nzd2dna25BZ0VBQW9JQ0FRQ1pNNm1WNUJkV2V0QzYKbXZ6dGFQaGxTY2dJY253emdzbDhBcXp3RDJNOVJWVkZwRUxBbTh2OTNlRWtMZEY2ZnNkY0FhUXQxWlFDSFdYbwovZkxwUTVVa1B4dXY2TVArdjRtSjB2OGRLRlo3UjcwaTVud1lCMkVlVkw2RUNZaWlxNmZ6VEtsa3F6U0QvNW93CnpTdy96amtHVGEwSXcvdkg5YXNINzRIajNXdEE3RythenZjaVlhQTZxK1dWYlZxNlBIanF6em9obEFuMkh1anoKNmhEamFpZXN2TGx3QS9CL3JoRHNBS2graTByOEFKUTFqM0JiTGJuYkJyWmZqYnBJUjNhYVh5amkwK3RQWENnVQo5M0JlOXZtS2U2U2NHUjcvNk9ubWJrU3NMY2RXaFpVNzcrL2c4WllsZGhwS01nczc4ekJYUlh4bHlzSThHRUtqClNYbnd5OVpmTU9kSTRKQU1jbU9JTmlZQWNtWm5JTUJIa0xacFl3aUl0eFdseXVJcWxoZFpkSHNrTFdNSjBSTHUKMUZoUzJwRVdDTmUzN1QwYjlobTRYNDlydUFiekl1M01xSmczcXFIdGRMNGRkZ1JZRHNjMXd0cDJrR2dBZGxDaQpySHJReitZeDBCTU9WbGRHM2htUlFIeWRxMkhyWW0xeEFDNWpMZ3FvaVZhY09wd0xKY21PcGsrZWVNQkNZNVo0CnpDWDdYV3l3YVZjbTJzdmhpTzE4QmRSK2g5aFliMkRNZDFCendDbE95endQcUlvQy9uNGRURG96Ry9GT3NySzgKLzRGeHc2NkNWZjNxODMwTVB3UndsQ0sxQ0I3RjNQK3lKWkpPelRuK05QZ2dGQW9NaGZUYXBQWTFhUGlWajBzVApvb0IwWjhjRVdUZE5ycUFObVBrOWgzaEIycG44SndJREFRQUJBb0lDQUVCa2dKRXA3ODAvamVBQktQSTR2cjhFCkJmblc5UEZKdFpwVUhaQkJSM3NIVzFJTU9xcHVVWTJBNXhLbjEzWmZOemdxMEhFYlpqeUZVc0pkaXU0VW8razYKUlU3b3pRaVVSU0VTK0h1dTZycWlhcEx5d1pIditCZ2hrbm80NzU4Lyt6VytNU3pJOFNmU0ZXTVJ1ZG1QdWxRMQo3ZGJUV1U2d3FaU0tUTlFUeXZMYzdnUHBuZUpybWtkTzNRNnppZ0RoVGdtVDFHRXNzZ3NxN3NzbXhMWnhkZithCnkyNlRtVkJ4UDFlUzV6OVpHTWxYRFBSK044RjdOTFVrMng3S21WT3NCZVBZdjN5bmlpNHZGQUhNQndWRFZadXAKWUlUajRpMjZIaVhtanlLM2t5T0F2anNWSElRMXh1QTBCZFROdC84WXRtYllJL005QitydVg0UDJiRFNUMktRKwo4TlN2Uk9wbVppcnBHZkY3bExMSGpJUjlTMFhCWDd6VDRoWnBRWnpqK3NEWnhDM2Y3TGIwRFlKYkp0TmlDYTQxCmNpTjhNUlNldzNneHZ0RVk0RzdnN3hjbkJNdjdNT3RwQTE3d2gvMHdLd0h0amYzSWh2TmIzdkZwT0k5d1FqSzYKSlRQMng4bENJV0tyalpObVN0UksreHJTN3hTOUZVdnBhSVlyclRLQkZWSmcyMURCYWI1L3hqRlBlQWxXejczSwpvVkhsa0hLdXNMSjZLczZzcXo0ZG9mbzg3dkFsUFJzTXRkZ1ZnZFIzNXhLTGtEWXNIbGxML3Z5dE9oSkNieXB5CkJqQm1TR0RMdzBDdWplaHVtU2czYjdSUGVTNk5rbHNqUEIrMVpzRjhpVGdCcjMyM1hvTmNha2dhWWVYQlg4NFAKaE1WZHUxWk1rbXJZMWhXTzEydnhBb0lCQVFEWU5Vb2xCMkhsdWlDcVFqdmU4UFNhV0YxRmhwNUlOMGJIZEppeApIdkhqMkplVHJ6V1pUZFlIdFNJR2RzalhTOTBESXo2bXJhMW9YZXFRYlgrODVlOUFQQkZnRTJmNU5uTzBCSVVJCk5hMXRiVGpIOUhjRGRzQmJKUkZwYnk1ODZUb3lhdFY3bS9zcjVpQUZlZFMyenFOTm1XUmZOdllZS2xselZoSEUKdUd4ZjZxMHJTWktVQUhja2s1bU5Yais3WFhZaTgyemErVEE3ZjBDVm5OamR3OXFpd3B2aTJKTFB2SnA0bWt6KwpyMEN1RW9yV2NhMUdTL2hTVWdXemw3NzhQdlRpZFI2RW4zMDB2ZlIyTE84aG1xRjhVL3Bpb2UrTDVjSllRNnNKCk1YMngrYThzWFFpZ3dwdG02aUNxQ2FuS3ExN3NUZS9RTmQ1czdwb3ZOaHVKOHd3dEFvSUJBUUMxWmM5ZktPUFgKVzZSN1VoaHRRcmhIc3htQnRIQ1BuNWRLbjN5MElNNWRBaEFSdFZDV1U0M0hOTHpCNW1LbjU2dnZrSVNFaXdBbAoxTGhuY2I3YXQ2cHpTSlZtMm5oTDhjeUZrdGQrNzVyL1FHNFlpNnZQbHNBODV5ZXZpZDhZcWswaHdaZXExY05xCmxETUN3NWsrV0drckM2VW5jZXNIc2FWbDJTUGdZV1c1L3I1NnVxUnBsaVFka1EyZmlEYWRyblVueU8ycHg3bFMKVG1HemZaNmtzTWh2MlZFR1NPRm05aUo0dWlPb0xyZVhoU1RQRmxTdjdZUTZSWWtSaGgwT0tqdXM5bXZacEIxWApjcytYN0UyVTNlM0RZelpCR0NFdmxxaFNWTFRScjdIN21pMWxUMEozR0RzbDdiUk9xOE50WVdQa3hhSlNCUnQ5Ck9TcTlkTm9CcGRvakFvSUJBQ2lQdnN3NW1WVW0yUS80QXhGdE5RWnJ3M3ZTcUlrMXpaS0h2a21rVzQ3NlNGMk4KaGttdmY1TE1tWWlLNmx6eHY1SGlIOVBYUzJ3RUNvaHo4bjMyeVM3TTFobW5LbDlucHNkRC9jMHZmTXpGcTl4ZgpjYUIxdTlxZGxxbW9FUm1nQzZuL3Z2TkVyUmRzUWQrbEhwSDVMRXZYbGl3Q3ZLS0Y5MmdhNHBSOFlPQ1J2MUVhCnFXUVl2a0ZmYTNSSkZUM0taK3BncnJCYUJZRnoreUxXWFIwbHJEUFN2TG9QRldQaHB6MHUvWGplV2cwT0wzdlIKc2NjNVkybldOM21jNDFpaFd3SE5KUitPYUVmbnh5QVFpQUJPNlRMUThtMWtvZk1sOUpMb2h3TGZoUXhKb21KNQpSYkFiTWxwWlhDMXFTSzliL1IvcDh5NmxuSWZsTDRuaDVjSzRsVFVDZ2dFQUpSSHVSQU1tTksrTXVJcjVaUEs2Cm1DUjR0UEg4QXMzWmJDMlZuWFlLMWlVQ3hhdXBFVjkzM05yaExEcjV0Rmg2NFpWR0Q1UWNicDYvSkp5eEpSOWQKblB1YlZJNlhBT1lrSnJQd2lBZE5SSmFWS1R6NTJvMXpNYjhIZEM4WHdZR2tDNTcxY0xzSW1YSTV6bm5NaWxvaworK0FBVzBSRGhLb0FKQVV3K0x6T3ZpamFJbGljR3R2TSs2SFdCK0VkVURJRHpTS1p0eFdTd01nMTNTbHh6elExCmNlNFdTZE9CQkxxT0p0L2JRNVp3ZkcyQUxUWGlEcVhhWE5JekJickRtMDUwTFkrYVVMcmlLQ25WVkxXODBReGQKZDQyQjIrR2pmb2NxVk5Ec3R1RlIzUm9QNXVGQXN2Zm50b09TVW5WMWxaZk9nMFVFUEFEQk1tRUpZL2hLU1FYcwp3d0tDQVFBNWQya2hFQ1c1V3QrMzRYWnl1b3NFTjZ4UDExbC96VDRBZjhGSWtQLzlkb0JXRnBhc28zbG1NcXZHCmhPeFErbnZBSjFhNzhZRjA4N3p1UC9DZkQ0UElOUTV4YzZHMDNQdG5JOVNVT0dpMDB4Zlg5MU5NMHBHYWJqb0QKZ0RqVzJxSkJDaVB5N0RIR1RlZkU5eUNUbkhrY1NBbWllVGc3aGFyeEZPOUREZTJKbzhKQXV2SHI1aGVxazVIcgpLYlgzTy9vNUMwcWVnYW1rWVRLcHZzV2VFdXhkY2l5LzFQd3NnV3BuV1JPWllQNENrSkJweEx1bDNVamVSY3dkCnRhcjBJYU52WlV2NFd4U0JZdWVHMDFyYUd2SDZtTTcyTEExR3MrMytwTnZwUVo3bGo2S09tcFlhQUlhemVxY2MKTjJjT2R5U1RqZmQ5OFlNVFAxbmIyK3N1Yy91VAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==
```
### Multiple TLS Certificates
Traefik supports multiple secret `certificateRefs` per Gateway listener.
If one of the certificates is invalid or cannot be loaded, the listener will be considered invalid and will not be able to serve traffic until the issue is resolved.
For example, the following `Gateway` listener references two different certificates:
```yaml
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: multi-cert-gateway
namespace: default
spec:
gatewayClassName: traefik
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: example-com-tls
- name: example-org-tls
allowedRoutes:
namespaces:
from: Same
```
## Exposing a Route
Once a `Gateway` is deployed (see [Deploying a Gateway](#deploying-a-gateway)) `HTTPRoute`, `TCPRoute`,

View File

@ -384,6 +384,7 @@ The following annotations are organized by category for easier navigation.
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |-----------------------------------------------------------------------------------------------------------|
| <a id="opt-nginx-ingress-kubernetes-iolimit-rps" href="#opt-nginx-ingress-kubernetes-iolimit-rps" title="#opt-nginx-ingress-kubernetes-iolimit-rps">`nginx.ingress.kubernetes.io/limit-rps`</a> | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rpm" href="#opt-nginx-ingress-kubernetes-iolimit-rpm" title="#opt-nginx-ingress-kubernetes-iolimit-rpm">`nginx.ingress.kubernetes.io/limit-rpm`</a> | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. |
| <a id="opt-nginx-ingress-kubernetes-iolimit-burst-multiplier" href="#opt-nginx-ingress-kubernetes-iolimit-burst-multiplier" title="#opt-nginx-ingress-kubernetes-iolimit-burst-multiplier">`nginx.ingress.kubernetes.io/limit-burst-multiplier`</a> | Default to a multiplier of 5 if the configured value is less than 1. Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. |
### Buffering
@ -397,6 +398,12 @@ The following annotations are organized by category for easier navigation.
| <a id="opt-nginx-ingress-kubernetes-ioproxy-buffers-number" href="#opt-nginx-ingress-kubernetes-ioproxy-buffers-number" title="#opt-nginx-ingress-kubernetes-ioproxy-buffers-number">`nginx.ingress.kubernetes.io/proxy-buffers-number`</a> | With Traefik, `proxy-buffer-numbers` is actually used to compute the size of a single buffer (size * number). |
| <a id="opt-nginx-ingress-kubernetes-ioproxy-max-temp-file-size" href="#opt-nginx-ingress-kubernetes-ioproxy-max-temp-file-size" title="#opt-nginx-ingress-kubernetes-ioproxy-max-temp-file-size">`nginx.ingress.kubernetes.io/proxy-max-temp-file-size`</a> | |
### Observability
| Annotation | Limitations / Notes |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <a id="opt-nginx-ingress-kubernetes-ioenable-access-log" href="#opt-nginx-ingress-kubernetes-ioenable-access-log" title="#opt-nginx-ingress-kubernetes-ioenable-access-log">`nginx.ingress.kubernetes.io/enable-access-log`</a> | Access logs must first be enabled in the [install configuration](../../../install-configuration/observability/logs-and-accesslogs/#access-logs) (globally or per entrypoint) for this annotation to take effect. When access logs are enabled, this annotation allows opting out specific Ingresses by setting it to `"false"`. Conversely, when access logs are disabled on an entrypoint, setting this annotation to `"true"` allows opting in specific Ingresses. |
### Timeout
| Annotation | Limitations / Notes |
@ -456,7 +463,6 @@ In practice, Traefik is slightly more lenient under bursty load, as it smooths o
| <a id="opt-nginx-ingress-kubernetes-iolimit-rate-after" href="#opt-nginx-ingress-kubernetes-iolimit-rate-after" title="#opt-nginx-ingress-kubernetes-iolimit-rate-after">`nginx.ingress.kubernetes.io/limit-rate-after`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rate" href="#opt-nginx-ingress-kubernetes-iolimit-rate" title="#opt-nginx-ingress-kubernetes-iolimit-rate">`nginx.ingress.kubernetes.io/limit-rate`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-whitelist" href="#opt-nginx-ingress-kubernetes-iolimit-whitelist" title="#opt-nginx-ingress-kubernetes-iolimit-whitelist">`nginx.ingress.kubernetes.io/limit-whitelist`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-burst-multiplier" href="#opt-nginx-ingress-kubernetes-iolimit-burst-multiplier" title="#opt-nginx-ingress-kubernetes-iolimit-burst-multiplier">`nginx.ingress.kubernetes.io/limit-burst-multiplier`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-connections" href="#opt-nginx-ingress-kubernetes-iolimit-connections" title="#opt-nginx-ingress-kubernetes-iolimit-connections">`nginx.ingress.kubernetes.io/limit-connections`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioglobal-rate-limit" href="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit" title="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit">`nginx.ingress.kubernetes.io/global-rate-limit`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioglobal-rate-limit-window" href="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit-window" title="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit-window">`nginx.ingress.kubernetes.io/global-rate-limit-window`</a> | |
@ -477,7 +483,6 @@ In practice, Traefik is slightly more lenient under bursty load, as it smooths o
| <a id="opt-nginx-ingress-kubernetes-iossl-ciphers" href="#opt-nginx-ingress-kubernetes-iossl-ciphers" title="#opt-nginx-ingress-kubernetes-iossl-ciphers">`nginx.ingress.kubernetes.io/ssl-ciphers`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iossl-prefer-server-ciphers" href="#opt-nginx-ingress-kubernetes-iossl-prefer-server-ciphers" title="#opt-nginx-ingress-kubernetes-iossl-prefer-server-ciphers">`nginx.ingress.kubernetes.io/ssl-prefer-server-ciphers`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioconnection-proxy-header" href="#opt-nginx-ingress-kubernetes-ioconnection-proxy-header" title="#opt-nginx-ingress-kubernetes-ioconnection-proxy-header">`nginx.ingress.kubernetes.io/connection-proxy-header`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioenable-access-log" href="#opt-nginx-ingress-kubernetes-ioenable-access-log" title="#opt-nginx-ingress-kubernetes-ioenable-access-log">`nginx.ingress.kubernetes.io/enable-access-log`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioenable-opentracing" href="#opt-nginx-ingress-kubernetes-ioenable-opentracing" title="#opt-nginx-ingress-kubernetes-ioenable-opentracing">`nginx.ingress.kubernetes.io/enable-opentracing`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioopentracing-trust-incoming-span" href="#opt-nginx-ingress-kubernetes-ioopentracing-trust-incoming-span" title="#opt-nginx-ingress-kubernetes-ioopentracing-trust-incoming-span">`nginx.ingress.kubernetes.io/opentracing-trust-incoming-span`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioenable-opentelemetry" href="#opt-nginx-ingress-kubernetes-ioenable-opentelemetry" title="#opt-nginx-ingress-kubernetes-ioenable-opentelemetry">`nginx.ingress.kubernetes.io/enable-opentelemetry`</a> | |

View File

@ -18,7 +18,7 @@ The table below lists all the available matchers:
| Rule | Description |
|-------------------------------------------------------------|:-------------------------------------------------------------------------------------------------|
| <a id="opt-HostSNIdomain" href="#opt-HostSNIdomain" title="#opt-HostSNIdomain">[```HostSNI(`domain`)```](#hostsni-and-hostsniregexp)</a> | Checks if the connection's Server Name Indication is equal to `domain`.<br /> More information [here](#hostsni-and-hostsniregexp). |
| <a id="opt-HostSNIdomain" href="#opt-HostSNIdomain" title="#opt-HostSNIdomain">[```HostSNI(`domain`)```](#hostsni-and-hostsniregexp)</a> | Checks if the connection's Server Name Indication is equal to `domain`. Supports wildcard subdomain matching (e.g. `*.example.com`).<br /> More information [here](#hostsni-and-hostsniregexp). |
| <a id="opt-HostSNIRegexpregexp" href="#opt-HostSNIRegexpregexp" title="#opt-HostSNIRegexpregexp">[```HostSNIRegexp(`regexp`)```](#hostsni-and-hostsniregexp)</a> | Checks if the connection's Server Name Indication matches `regexp`.<br />Use a [Go](https://golang.org/pkg/regexp/) flavored syntax.<br /> More information [here](#hostsni-and-hostsniregexp). |
| <a id="opt-ClientIPip" href="#opt-ClientIPip" title="#opt-ClientIPip">[```ClientIP(`ip`)```](#clientip)</a> | Checks if the connection's client IP correspond to `ip`. It accepts IPv4, IPv6 and CIDR formats.<br /> More information [here](#clientip). |
| <a id="opt-ALPNprotocol" href="#opt-ALPNprotocol" title="#opt-ALPNprotocol">[```ALPN(`protocol`)```](#alpn)</a> | Checks if the connection's ALPN protocol equals `protocol`.<br /> More information [here](#alpn). |
@ -59,6 +59,15 @@ These matchers do not support non-ASCII characters, use punycode encoded values
when one wants a non-TLS router that matches all (non-TLS) requests,
one should use the specific ```HostSNI(`*`)``` syntax.
!!! info "Wildcard subdomain matching"
The `HostSNI` matcher supports a single-level wildcard prefix (`*.example.com`) to match any direct subdomain of `example.com`.
It should be preferred over the `HostSNIRegexp` matcher as it allows attaching a TLS option and is more efficient.
A wildcard matches exactly one subdomain label: `*.example.com` matches `foo.example.com` but not `foo.bar.example.com` or `example.com` itself.
This is only available with the **v3 rule syntax** (the default).
#### Examples
Match all connections:
@ -77,7 +86,13 @@ Match TCP connections sent to `example.com`:
HostSNI(`example.com`)
```
Match TCP connections opened on any subdomain of `example.com`:
Match TCP connections opened on any direct subdomain of `example.com` (e.g. `foo.example.com`):
```yaml
HostSNI(`*.example.com`)
```
Match TCP connections opened on any subdomain of `example.com` (including nested subdomains), using a regular expression:
```yaml
HostSNIRegexp(`^.+\.example\.com$`)
@ -201,3 +216,9 @@ Traefik reserves a range of priorities for its internal routers, the maximum use
- `(MaxInt32 - 1000)` for 32-bit platforms,
- `(MaxInt64 - 1000)` for 64-bit platforms.
!!! info "Providers Precedence"
When two routes from **different providers** share the same numeric priority,
Traefik uses the [`providers.precedence`](../../../install-configuration/providers/overview.md#providers-precedence) install configuration option to determine which route takes precedence.
The provider listed first in `precedence` wins the tie.

View File

@ -59,7 +59,7 @@ tls:
keyFile: /certs/local.key
```
In the same folder as the `dynamic/tls.yaml` file, create a `docker-compose.yaml` file and include the following:
In your project root folder (the parent folder to the `dynamic/tls.yaml` file), create a `docker-compose.yaml` file and include the following:
```yaml
services:

View File

@ -110,6 +110,7 @@ plugins:
'https/acme.md': 'reference/install-configuration/tls/certificate-resolvers/acme.md'
'https/tailscale.md': 'reference/install-configuration/tls/certificate-resolvers/tailscale.md'
'https/spiffe.md': 'reference/install-configuration/tls/spiffe.md'
'https/ocsp.md': 'reference/install-configuration/tls/ocsp.md'
# Middlewares
'middlewares/overview.md': 'reference/routing-configuration/http/middlewares/overview.md'
# HTTP
@ -168,6 +169,11 @@ plugins:
"reference/dynamic-configuration/nomad.md": 'reference/routing-configuration/other-providers/nomad.md'
'reference/dynamic-configuration/ecs.md': 'reference/routing-configuration/other-providers/ecs.md'
'reference/dynamic-configuration/kv.md': 'reference/routing-configuration/other-providers/kv.md'
'reference/dynamic-configuration/kv-ref.md': 'reference/routing-configuration/other-providers/kv.md'
'reference/install-configuration/cli-options-list.md': 'reference/install-configuration/configuration-options.md'
'reference/install-configuration/observability/healthcheck/cli.md': 'reference/install-configuration/observability/healthcheck.md'
'reference/install-configuration/observability/healthcheck/ping.md': 'reference/install-configuration/observability/healthcheck.md'
'reference/install-configuration/observability/options-list.md': 'reference/install-configuration/configuration-options.md'
## Plugins
'plugins/index.md': "extend/extend-traefik.md"
## Migration

2
go.mod
View File

@ -39,7 +39,7 @@ require (
github.com/huandu/xstrings v1.5.0
github.com/influxdata/influxdb-client-go/v2 v2.7.0
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab // No tag on the repo.
github.com/klauspost/compress v1.18.2
github.com/klauspost/compress v1.18.5
github.com/kvtools/consul v1.0.2
github.com/kvtools/etcdv3 v1.0.3
github.com/kvtools/redis v1.2.0

4
go.sum
View File

@ -782,8 +782,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=

View File

@ -0,0 +1,36 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.websecure]
address = ":4443"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.routers]
# Wildcard router: routes any *.snitest.com subdomain to service1.
[http.routers.wildcard]
service = "service1"
rule = "Host(`*.snitest.com`)"
[http.routers.wildcard.tls]
[http.services]
[http.services.service1]
[http.services.service1.loadBalancer]
[[http.services.service1.loadBalancer.servers]]
url = "http://127.0.0.1:9040"
[[tls.certificates]]
certFile = "fixtures/https/wildcard.snitest.com.cert"
keyFile = "fixtures/https/wildcard.snitest.com.key"

View File

@ -0,0 +1,64 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.websecure]
address = ":4443"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.routers]
# Wildcard router covering all *.snitest.com subdomains with TLS option "foo" (minTLS12).
[http.routers.wildcard]
service = "service1"
rule = "Host(`*.snitest.com`)"
[http.routers.wildcard.tls]
options = "foo"
# foo.snitest.com uses TLS option "bar" (minTLS13)
[http.routers.bar]
service = "service1"
rule = "Host(`foo.snitest.com`)"
[http.routers.bar.tls]
options = "bar"
# minTLS11
[http.routers.other]
service = "service1"
rule = "Host(`other.snitest.com`)"
[http.routers.other.tls]
[http.services]
[http.services.service1]
[http.services.service1.loadBalancer]
[[http.services.service1.loadBalancer.servers]]
url = "{{ .BackendURL }}"
[[tls.certificates]]
certFile = "fixtures/https/wildcard.snitest.com.cert"
keyFile = "fixtures/https/wildcard.snitest.com.key"
[tls.options]
[tls.options.foo]
minVersion = "VersionTLS12"
maxVersion = "VersionTLS12"
[tls.options.bar]
minVersion = "VersionTLS13"
maxVersion = "VersionTLS13"
[tls.options.default]
minVersion = "VersionTLS11"
maxVersion = "VersionTLS11"

View File

@ -0,0 +1,48 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.web]
address = ":8000"
[api]
insecure = true
[providers]
precedence = {{ .Precedence }}
[providers.file]
filename = "{{ .SelfFilename }}"
[providers.docker]
endpoint = "{{ .DockerHost }}"
exposedByDefault = false
## dynamic configuration ##
[http.routers]
[http.routers.file-router]
rule = "PathPrefix(`/http`)"
service = "file-service"
entryPoints = ["web"]
[http.services]
[http.services.file-service.loadBalancer]
[[http.services.file-service.loadBalancer.servers]]
url = "http://{{ .FileBackendAddress }}"
[tcp.routers]
[tcp.routers.file-router]
rule = "HostSNI(`*`)"
service = "file-service"
entryPoints = ["web"]
[tcp.services]
[tcp.services.file-service.loadBalancer]
[[tcp.services.file-service.loadBalancer.servers]]
address = "{{ .FileBackendAddress }}"

View File

@ -0,0 +1,65 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.tcp]
address = ":8093"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[tcp]
[tcp.routers]
# Wildcard router covering *.snitest.com with TLS option "foo" (minTLS12).
[tcp.routers.wildcard]
rule = "HostSNI(`*.snitest.com`)"
service = "backend"
entryPoints = ["tcp"]
[tcp.routers.wildcard.tls]
options = "foo"
# Override: bar.snitest.com uses TLS option "bar" (minTLS13), stricter than the wildcard.
[tcp.routers.bar]
rule = "HostSNI(`bar.snitest.com`)"
service = "backend"
entryPoints = ["tcp"]
[tcp.routers.bar.tls]
options = "bar"
[tcp.routers.default]
rule = "HostSNI(`other.snitest.com`)"
service = "backend"
entryPoints = ["tcp"]
[tcp.routers.default.tls]
[tcp.services]
[tcp.services.backend.loadBalancer]
[[tcp.services.backend.loadBalancer.servers]]
address = "{{ .Backend }}"
[[tls.certificates]]
certFile = "fixtures/https/wildcard.snitest.com.cert"
keyFile = "fixtures/https/wildcard.snitest.com.key"
[tls.options]
[tls.options.default]
minVersion = "VersionTLS11"
maxVersion = "VersionTLS11"
[tls.options.foo]
minVersion = "VersionTLS12"
maxVersion = "VersionTLS12"
[tls.options.bar]
minVersion = "VersionTLS13"
maxVersion = "VersionTLS13"

View File

@ -577,7 +577,7 @@ func (s *HealthCheckSuite) TestPropagateNoHealthCheck() {
s.traefikCmd(withConfigFile(file))
// wait for traefik
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("Host(`noop.localhost`)"), try.BodyNotContains("Host(`root.localhost`)"))
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("Host(`noop.localhost`)"), try.BodyContains("cannot register wsp1 as updater for wsp-service1@file"))
require.NoError(s.T(), err)
rootReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000", nil)

View File

@ -30,6 +30,41 @@ func TestHTTPSSuite(t *testing.T) {
suite.Run(t, &HTTPSSuite{})
}
// TestWithWildcardHost verifies that a wildcard Host rule Host(`*.snitest.com`)
// routes HTTPS requests for any matching subdomain to the configured service.
func (s *SimpleSuite) TestWithWildcardHost() {
backend := startTestServer("9040", http.StatusOK, "")
defer backend.Close()
file := s.adaptFile("fixtures/https/https_wildcard_host.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
// Wait for Traefik to load the wildcard router.
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second,
try.BodyContains("Host(`*.snitest.com`)"))
require.NoError(s.T(), err)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
// foo.snitest.com matches the wildcard and must be routed to the backend.
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
require.NoError(s.T(), err)
req.Host = "foo.snitest.com"
err = try.RequestWithTransport(req, 5*time.Second, tr, try.StatusCodeIs(http.StatusOK))
require.NoError(s.T(), err)
// bar.snitest.com also matches the wildcard and must be routed to the backend.
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
require.NoError(s.T(), err)
req.Host = "bar.snitest.com"
err = try.RequestWithTransport(req, 3*time.Second, tr, try.StatusCodeIs(http.StatusOK))
require.NoError(s.T(), err)
}
// TestWithSNIConfigHandshake involves a client sending a SNI hostname of
// "snitest.com", which happens to match the CN of 'snitest.com.crt'. The test
// verifies that traefik presents the correct certificate.
@ -115,7 +150,6 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute() {
}
// TestWithTLSOptions verifies that traefik routes the requests with the associated tls options.
func (s *HTTPSSuite) TestWithTLSOptions() {
file := s.adaptFile("fixtures/https/https_tls_options.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@ -196,8 +230,71 @@ func (s *HTTPSSuite) TestWithTLSOptions() {
require.NoError(s.T(), err)
}
// TestWithConflictingTLSOptions checks that routers with same SNI but different TLS options get fallbacked to the default TLS options.
func (s *HTTPSSuite) TestWithTLSOptionsAndWildcard() {
backend := startTestServer("0", http.StatusNoContent, "")
defer backend.Close()
err := try.GetRequest(backend.URL, 1*time.Second, try.StatusCodeIs(http.StatusNoContent))
require.NoError(s.T(), err)
file := s.adaptFile("fixtures/https/https_wildcard_tls_options.toml", struct{ BackendURL string }{backend.URL})
s.traefikCmd(withConfigFile(file))
// wait for Traefik
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`*.snitest.com`)"))
require.NoError(s.T(), err)
tr1 := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MaxVersion: tls.VersionTLS12,
MinVersion: tls.VersionTLS12,
ServerName: "bar.snitest.com",
},
}
tr2 := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MaxVersion: tls.VersionTLS13,
MinVersion: tls.VersionTLS13,
ServerName: "foo.snitest.com",
},
}
tr3 := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MaxVersion: tls.VersionTLS11,
MinVersion: tls.VersionTLS11,
ServerName: "other.snitest.com",
},
}
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
require.NoError(s.T(), err)
req.Host = tr1.TLSClientConfig.ServerName
err = try.RequestWithTransport(req, 30*time.Second, tr1, try.StatusCodeIs(http.StatusNoContent))
require.NoError(s.T(), err)
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
require.NoError(s.T(), err)
req.Host = tr2.TLSClientConfig.ServerName
err = try.RequestWithTransport(req, 3*time.Second, tr2, try.StatusCodeIs(http.StatusNoContent))
require.NoError(s.T(), err)
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
require.NoError(s.T(), err)
req.Host = tr3.TLSClientConfig.ServerName
err = try.RequestWithTransport(req, 3*time.Second, tr3, try.StatusCodeIs(http.StatusNoContent))
require.NoError(s.T(), err)
}
// TestWithConflictingTLSOptions checks that routers with same SNI but different TLS options get fallbacked to the
// default TLS options.
func (s *HTTPSSuite) TestWithConflictingTLSOptions() {
file := s.adaptFile("fixtures/https/https_tls_options.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@ -262,7 +359,6 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions() {
// TestWithSNIStrictNotMatchedRequest involves a client sending a SNI hostname of
// "snitest.org", which does not match the CN of 'snitest.com.crt'. The test
// verifies that traefik closes the connection.
func (s *HTTPSSuite) TestWithSNIStrictNotMatchedRequest() {
file := s.adaptFile("fixtures/https/https_sni_strict.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@ -284,7 +380,6 @@ func (s *HTTPSSuite) TestWithSNIStrictNotMatchedRequest() {
// TestWithDefaultCertificate involves a client sending a SNI hostname of
// "snitest.org", which does not match the CN of 'snitest.com.crt'. The test
// verifies that traefik returns the default certificate.
func (s *HTTPSSuite) TestWithDefaultCertificate() {
file := s.adaptFile("fixtures/https/https_sni_default_cert.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@ -316,7 +411,6 @@ func (s *HTTPSSuite) TestWithDefaultCertificate() {
// TestWithDefaultCertificateNoSNI involves a client sending a request with no ServerName
// which does not match the CN of 'snitest.com.crt'. The test
// verifies that traefik returns the default certificate.
func (s *HTTPSSuite) TestWithDefaultCertificateNoSNI() {
file := s.adaptFile("fixtures/https/https_sni_default_cert.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@ -348,7 +442,6 @@ func (s *HTTPSSuite) TestWithDefaultCertificateNoSNI() {
// "www.snitest.com", which matches the CN of two static certificates:
// 'wildcard.snitest.com.crt', and `www.snitest.com.crt`. The test
// verifies that traefik returns the non-wildcard certificate.
func (s *HTTPSSuite) TestWithOverlappingStaticCertificate() {
file := s.adaptFile("fixtures/https/https_sni_default_cert.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@ -381,7 +474,6 @@ func (s *HTTPSSuite) TestWithOverlappingStaticCertificate() {
// "www.snitest.com", which matches the CN of two dynamic certificates:
// 'wildcard.snitest.com.crt', and `www.snitest.com.crt`. The test
// verifies that traefik returns the non-wildcard certificate.
func (s *HTTPSSuite) TestWithOverlappingDynamicCertificate() {
file := s.adaptFile("fixtures/https/dynamic_https_sni_default_cert.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@ -410,9 +502,8 @@ func (s *HTTPSSuite) TestWithOverlappingDynamicCertificate() {
assert.Equal(s.T(), "h2", proto)
}
// TestWithClientCertificateAuthentication
// The client can send a certificate signed by a CA trusted by the server but it's optional.
// TestWithClientCertificateAuthentication tests that a client can send a certificate signed by a CA trusted by the server
// but it's optional.
func (s *HTTPSSuite) TestWithClientCertificateAuthentication() {
file := s.adaptFile("fixtures/https/clientca/https_1ca1config.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@ -464,9 +555,8 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthentication() {
assert.NoError(s.T(), err, "should be allowed to connect to server")
}
// TestWithClientCertificateAuthentication
// Use two CA:s and test that clients with client signed by either of them can connect.
// TestWithClientCertificateAuthenticationMultipleCAs uses two CA:s
// and test that clients with client signed by either of them can connect.
func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipleCAs() {
server1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { _, _ = rw.Write([]byte("server1")) }))
server2 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { _, _ = rw.Write([]byte("server2")) }))
@ -557,9 +647,8 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipleCAs() {
assert.Error(s.T(), err)
}
// TestWithClientCertificateAuthentication
// Use two CA:s in two different files and test that clients with client signed by either of them can connect.
// TestWithClientCertificateAuthenticationMultipleCAsMultipleFiles uses two CA:s in two different files
// and test that clients with client signed by either of them can connect.
func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipleCAsMultipleFiles() {
server1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { _, _ = rw.Write([]byte("server1")) }))
server2 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { _, _ = rw.Write([]byte("server2")) }))
@ -768,7 +857,6 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithNoChange() {
// that traefik updates its configuration when the HTTPS configuration is modified and
// it routes the requests to the expected backends thanks to given certificate if possible
// otherwise thanks to the default one.
func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange() {
dynamicConfFileName := s.adaptFile("fixtures/https/dynamic_https.toml", struct{}{})
confFileName := s.adaptFile("fixtures/https/dynamic_https_sni.toml", struct {
@ -833,7 +921,6 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange() {
// that traefik updates its configuration when the HTTPS configuration is modified, even if it totally deleted, and
// it routes the requests to the expected backends thanks to given certificate if possible
// otherwise thanks to the default one.
func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion() {
dynamicConfFileName := s.adaptFile("fixtures/https/dynamic_https.toml", struct{}{})
confFileName := s.adaptFile("fixtures/https/dynamic_https_sni.toml", struct {
@ -1143,7 +1230,7 @@ func (s *HTTPSSuite) TestWithInvalidTLSOption() {
}
}
// modifyCertificateConfFileContent replaces the content of a HTTPS configuration file.
// modifyCertificateConfFileContent replaces the content of an HTTPS configuration file.
func (s *HTTPSSuite) modifyCertificateConfFileContent(certFileName, confFileName string) {
file, err := os.OpenFile("./"+confFileName, os.O_WRONLY, os.ModeExclusive)
require.NoError(s.T(), err)

View File

@ -111,13 +111,13 @@ func (s *K8sSuite) TestGatewayConfiguration() {
s.testConfiguration("testdata/rawdata-gateway.json", "8080")
}
func (s *K8sSuite) TestIngressclass() {
func (s *K8sSuite) TestIngressClass() {
s.traefikCmd(withConfigFile("fixtures/k8s_ingressclass.toml"))
s.testConfiguration("testdata/rawdata-ingressclass.json", "8080")
}
func (s *K8sSuite) TestDisableIngressclassLookup() {
func (s *K8sSuite) TestDisableIngressClassLookup() {
s.traefikCmd(withConfigFile("fixtures/k8s_ingressclass_disabled.toml"))
s.testConfiguration("testdata/rawdata-ingressclass-disabled.json", "8080")

View File

@ -0,0 +1,9 @@
services:
whoami:
image: traefik/whoami
labels:
traefik.enable: "true"
traefik.http.routers.docker-router.rule: "PathPrefix(`/http`)"
traefik.http.routers.docker-router.entryPoints: web
traefik.tcp.routers.docker-router.rule: "HostSNI(`*`)"
traefik.tcp.routers.docker-router.entryPoints: web

View File

@ -2656,3 +2656,88 @@ func (s *SimpleSuite) TestServiceMiddleware() {
// The whoami service should have received the X-Custom-Header that was added by the service middleware
assert.Contains(s.T(), string(body), "X-Custom-Header: service-middleware-test")
}
// TestProviderPrecedenceFileWins verifies that, when two providers define
// routes with the same rule and auto-computed priority, the provider listed
// first in providers.precedence takes precedence (lower index = higher
// provider priority).
//
// Setup:
// - providers.file → file-router → fileBackend (body: "from-file")
// - providers.docker → docker-router → whoami container
// - precedence = ["file", "docker"] → file is index 0 → wins
func (s *SimpleSuite) TestProviderPrecedenceFileWins() {
s.createComposeProject("providers-precedence")
s.composeUp("whoami")
defer s.composeDown()
fileBackend := startTestServer("9042", http.StatusOK, "from-file")
defer fileBackend.Close()
file := s.adaptFile("fixtures/providers-precedence.toml", struct {
Precedence string
FileBackendAddress string
DockerHost string
}{
Precedence: `["file", "docker"]`,
FileBackendAddress: "127.0.0.1:9042",
DockerHost: s.getDockerHost(),
})
s.traefikCmd(withConfigFile(file))
// Wait for both providers to have loaded their routers.
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second,
try.StatusCodeIs(http.StatusOK),
try.BodyContains("file-router@file"),
try.BodyContains("docker-router@docker"))
require.NoError(s.T(), err)
// The file provider has higher priority → requests must reach the file backend.
err = try.GetRequest("http://127.0.0.1:8000/http", 5*time.Second, try.BodyContains("from-file"))
require.NoError(s.T(), err)
// This request should be handled by the TCP route.
err = try.GetRequest("http://127.0.0.1:8000/tcp", 5*time.Second, try.BodyContains("from-file"))
require.NoError(s.T(), err)
}
// TestProviderPrecedenceDockerWins mirrors TestProviderPrecedenceFileWins
// but reverses the precedence so that the Docker provider wins instead.
//
// Setup:
// - providers.file → file-router → fileBackend (body: "from-file")
// - providers.docker → docker-router → whoami container
// - precedence = ["docker", "file"] → docker is index 0 → wins
func (s *SimpleSuite) TestProviderPrecedenceDockerWins() {
s.createComposeProject("providers-precedence")
s.composeUp("whoami")
defer s.composeDown()
fileBackend := startTestServer("9042", http.StatusOK, "from-file")
defer fileBackend.Close()
file := s.adaptFile("fixtures/providers-precedence.toml", struct {
Precedence string
FileBackendAddress string
DockerHost string
}{
Precedence: `["docker", "file"]`,
FileBackendAddress: "127.0.0.1:9042",
DockerHost: s.getDockerHost(),
})
s.traefikCmd(withConfigFile(file))
// Wait for both providers to have loaded their routers.
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second,
try.BodyContains("file-router@file"),
try.BodyContains("docker-router@docker"))
require.NoError(s.T(), err)
// The Docker provider has higher priority → requests must reach the whoami container.
err = try.GetRequest("http://127.0.0.1:8000/http", 5*time.Second, try.BodyContains("Hostname:"))
require.NoError(s.T(), err)
// This request should be handled by the TCP route.
err = try.GetRequest("http://127.0.0.1:8000/tcp", 5*time.Second, try.BodyContains("Hostname:"))
require.NoError(s.T(), err)
}

View File

@ -438,6 +438,95 @@ func (s *TCPSuite) TestPostgresSTARTTLSPassthrough() {
assert.Equal(s.T(), byte('R'), header[0])
}
// TestTCPWildcardHostSNI verifies that a wildcard HostSNI rule HostSNI(`*.snitest.com`)
// routes TLS connections for any matching subdomain to the configured backend.
func (s *SimpleSuite) TestTCPWildcardHostSNI() {
backend := startTestServer("9041", http.StatusOK, "")
defer backend.Close()
file := s.adaptFile("fixtures/tcp/wildcard-hostsni-tls-options.toml", struct {
Backend string
}{
Backend: "127.0.0.1:9041",
})
s.traefikCmd(withConfigFile(file))
// Wait for the wildcard TCP router to be loaded.
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second,
try.BodyContains("HostSNI(`*.snitest.com`)"))
require.NoError(s.T(), err)
// foo.snitest.com matches the wildcard: TLS connection must succeed.
conn, err := tls.Dial("tcp", "127.0.0.1:8093", &tls.Config{
ServerName: "foo.snitest.com",
InsecureSkipVerify: true,
})
require.NoError(s.T(), err)
conn.Close()
// bar.snitest.com also matches the wildcard: TLS connection must succeed.
conn, err = tls.Dial("tcp", "127.0.0.1:8093", &tls.Config{
ServerName: "bar.snitest.com",
InsecureSkipVerify: true,
})
require.NoError(s.T(), err)
conn.Close()
}
// TestTCPWildcardHostSNITLSOptions verifies that:
// - a wildcard HostSNI rule HostSNI(`*.snitest.com`) with TLS option "foo" (minTLS12)
// routes and accepts TLS 1.2 connections for any matching subdomain;
// - a more specific rule HostSNI(`bar.snitest.com`) with TLS option "bar" (minTLS13)
// takes priority for that subdomain and rejects TLS 1.2-only connections.
func (s *SimpleSuite) TestTCPWildcardHostSNITLSOptions() {
backend := startTestServer("9041", http.StatusOK, "")
defer backend.Close()
file := s.adaptFile("fixtures/tcp/wildcard-hostsni-tls-options.toml", struct {
Backend string
}{
Backend: "127.0.0.1:9041",
})
s.traefikCmd(withConfigFile(file))
// Wait for both TCP routers to be loaded.
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second,
try.BodyContains("HostSNI(`*.snitest.com`)"))
require.NoError(s.T(), err)
// foo.snitest.com matches the wildcard (TLS option "foo", minTLS12).
// A TLS 1.2 connection must succeed.
conn, err := tls.Dial("tcp", "127.0.0.1:8093", &tls.Config{
ServerName: "foo.snitest.com",
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
})
require.NoError(s.T(), err)
conn.Close()
// bar.snitest.com has a specific rule with TLS option "bar" (minTLS13).
// A TLS 1.2-only connection must be rejected.
conn, err = tls.Dial("tcp", "127.0.0.1:8093", &tls.Config{
ServerName: "bar.snitest.com",
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
})
require.NoError(s.T(), err)
conn.Close()
// bar.snitest.com without a version cap: connection must succeed.
conn, err = tls.Dial("tcp", "127.0.0.1:8093", &tls.Config{
ServerName: "other.snitest.com",
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS11,
MaxVersion: tls.VersionTLS11,
})
require.NoError(s.T(), err)
conn.Close()
}
func welcome(addr string) (string, error) {
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {

View File

@ -47,13 +47,21 @@
"web"
],
"service": "default-whoami-http",
"rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)",
"rule": "Host(\"whoami.test\") \u0026\u0026 PathPrefix(\"/whoami\")",
"priority": 44,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "test.ingress",
"serviceName": "whoami",
"servicePort": "http"
}
}
},
"status": "enabled",
"using": [

View File

@ -47,13 +47,21 @@
"web"
],
"service": "default-whoami-http",
"rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)",
"rule": "Host(\"whoami.test.https\") \u0026\u0026 PathPrefix(\"/whoami\")",
"priority": 50,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "test.ingress.https",
"serviceName": "whoami",
"servicePort": "http"
}
}
},
"status": "enabled",
"using": [
@ -65,13 +73,21 @@
"web"
],
"service": "default-whoami-http",
"rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)",
"rule": "Host(\"whoami.test\") \u0026\u0026 PathPrefix(\"/whoami\")",
"priority": 44,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "test.ingress",
"serviceName": "whoami",
"servicePort": "http"
}
}
},
"status": "enabled",
"using": [
@ -83,13 +99,21 @@
"web"
],
"service": "default-whoami-80",
"rule": "Host(`whoami.test.drop`) \u0026\u0026 PathPrefix(`/drop`)",
"rule": "Host(\"whoami.test.drop\") \u0026\u0026 PathPrefix(\"/drop\")",
"priority": 47,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "whoami-drop-route",
"serviceName": "whoami",
"servicePort": "80"
}
}
},
"status": "enabled",
"using": [
@ -101,13 +125,21 @@
"web"
],
"service": "default-whoami-80",
"rule": "Host(`whoami.test.keep`) \u0026\u0026 PathPrefix(`/keep`)",
"rule": "Host(\"whoami.test.keep\") \u0026\u0026 PathPrefix(\"/keep\")",
"priority": 47,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "whoami-keep-route",
"serviceName": "whoami",
"servicePort": "80"
}
}
},
"status": "enabled",
"using": [

View File

@ -47,13 +47,21 @@
"web"
],
"service": "default-whoami-80",
"rule": "Host(`whoami.test.keep`) \u0026\u0026 PathPrefix(`/keep`)",
"rule": "Host(\"whoami.test.keep\") \u0026\u0026 PathPrefix(\"/keep\")",
"priority": 47,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "whoami-keep-route",
"serviceName": "whoami",
"servicePort": "80"
}
}
},
"status": "enabled",
"using": [

View File

@ -1,6 +1,7 @@
package try
import (
"bytes"
"context"
"errors"
"fmt"
@ -25,6 +26,8 @@ func BodyContains(values ...string) ResponseCondition {
return fmt.Errorf("failed to read response body: %w", err)
}
res.Body = io.NopCloser(bytes.NewBuffer(body))
for _, value := range values {
if !strings.Contains(string(body), value) {
return fmt.Errorf("could not find '%s' in body '%s'", value, string(body))
@ -43,6 +46,8 @@ func BodyNotContains(values ...string) ResponseCondition {
return fmt.Errorf("failed to read response body: %w", err)
}
res.Body = io.NopCloser(bytes.NewBuffer(body))
for _, value := range values {
if strings.Contains(string(body), value) {
return fmt.Errorf("find '%s' in body '%s'", value, string(body))
@ -61,6 +66,8 @@ func BodyContainsOr(values ...string) ResponseCondition {
return fmt.Errorf("failed to read response body: %w", err)
}
res.Body = io.NopCloser(bytes.NewBuffer(body))
for _, value := range values {
if strings.Contains(string(body), value) {
return nil
@ -79,6 +86,8 @@ func HasBody() ResponseCondition {
return fmt.Errorf("failed to read response body: %w", err)
}
res.Body = io.NopCloser(bytes.NewBuffer(body))
if len(body) == 0 {
return errors.New("response doesn't have body content")
}

172
pkg/api/certificate.go Normal file
View File

@ -0,0 +1,172 @@
package api
import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"fmt"
"sort"
"time"
)
const (
certStatusEnabled = "enabled"
certStatusWarning = "warning"
certStatusExpired = "expired"
)
// certificateRepresentation represents a certificate in the API.
type certificateRepresentation struct {
Name string `json:"name"` // SHA-256 fingerprint of the DER-encoded certificate.
SANs []string `json:"sans"`
NotAfter time.Time `json:"notAfter"`
NotBefore time.Time `json:"notBefore"`
SerialNumber string `json:"serialNumber"`
CommonName string `json:"commonName"`
IssuerOrg string `json:"issuerOrg,omitempty"`
IssuerCN string `json:"issuerCN,omitempty"`
IssuerCountry string `json:"issuerCountry,omitempty"`
Organization string `json:"organization,omitempty"`
Country string `json:"country,omitempty"`
Version string `json:"version"`
KeyType string `json:"keyType"`
KeySize int `json:"keySize,omitempty"`
SignatureAlgorithm string `json:"signatureAlgorithm"`
CertFingerprint string `json:"certFingerprint"`
PublicKeyFingerprint string `json:"publicKeyFingerprint"`
Status string `json:"status"`
}
// Interface methods for sort.go compatibility.
func (c certificateRepresentation) name() string {
return c.CommonName
}
func (c certificateRepresentation) status() string {
return c.Status
}
func (c certificateRepresentation) issuer() string {
if c.IssuerOrg != "" {
return c.IssuerOrg
}
return c.IssuerCN
}
func (c certificateRepresentation) validUntil() time.Time {
return c.NotAfter
}
// buildCertificateRepresentation builds a certificateRepresentation from an x509 certificate.
func buildCertificateRepresentation(cert *x509.Certificate) certificateRepresentation {
keyType, keySize := extractKeyInfo(cert)
certFingerprint, pubKeyFingerprint := extractFingerprints(cert)
issuerOrg, issuerCN, issuerCountry := extractIssuerInfo(cert)
organization, country := extractSubjectInfo(cert)
return certificateRepresentation{
Name: certFingerprint,
SANs: extractSANs(cert),
NotAfter: cert.NotAfter,
NotBefore: cert.NotBefore,
SerialNumber: cert.SerialNumber.String(),
CommonName: cert.Subject.CommonName,
IssuerOrg: issuerOrg,
IssuerCN: issuerCN,
IssuerCountry: issuerCountry,
Organization: organization,
Country: country,
Version: formatVersion(cert.Version),
KeyType: keyType,
KeySize: keySize,
SignatureAlgorithm: cert.SignatureAlgorithm.String(),
CertFingerprint: certFingerprint,
PublicKeyFingerprint: pubKeyFingerprint,
Status: getCertificateStatus(cert.NotAfter),
}
}
// extractSANs extracts Subject Alternative Names from a certificate.
func extractSANs(cert *x509.Certificate) []string {
sans := make([]string, 0, len(cert.DNSNames)+len(cert.IPAddresses))
sans = append(sans, cert.DNSNames...)
for _, ip := range cert.IPAddresses {
sans = append(sans, ip.String())
}
sort.Strings(sans)
return sans
}
// extractKeyInfo determines the key type and size from a certificate.
func extractKeyInfo(cert *x509.Certificate) (keyType string, keySize int) {
keyType = "Unknown"
keySize = 0
switch pubKey := cert.PublicKey.(type) {
case *rsa.PublicKey:
keyType = "RSA"
keySize = pubKey.N.BitLen()
case *ecdsa.PublicKey:
keyType = "ECDSA"
keySize = pubKey.Curve.Params().BitSize
}
return keyType, keySize
}
// extractFingerprints calculates SHA-256 fingerprints for certificate and public key.
func extractFingerprints(cert *x509.Certificate) (certFingerprint, pubKeyFingerprint string) {
certHash := sha256.Sum256(cert.Raw)
certFingerprint = hex.EncodeToString(certHash[:])
pubKeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err == nil {
pubKeyHash := sha256.Sum256(pubKeyBytes)
pubKeyFingerprint = hex.EncodeToString(pubKeyHash[:])
}
return certFingerprint, pubKeyFingerprint
}
// extractIssuerInfo extracts issuer information from a certificate.
func extractIssuerInfo(cert *x509.Certificate) (org, cn, country string) {
if len(cert.Issuer.Organization) > 0 {
org = cert.Issuer.Organization[0]
}
cn = cert.Issuer.CommonName
if len(cert.Issuer.Country) > 0 {
country = cert.Issuer.Country[0]
}
return org, cn, country
}
// extractSubjectInfo extracts subject information from a certificate.
func extractSubjectInfo(cert *x509.Certificate) (organization, country string) {
if len(cert.Subject.Organization) > 0 {
organization = cert.Subject.Organization[0]
}
if len(cert.Subject.Country) > 0 {
country = cert.Subject.Country[0]
}
return organization, country
}
// formatVersion formats the X.509 version for display.
func formatVersion(version int) string {
return fmt.Sprintf("v%d", version)
}
// getCertificateStatus returns the status of a certificate based on its expiry.
func getCertificateStatus(notAfter time.Time) string {
remaining := time.Until(notAfter)
if remaining < 0 {
return certStatusExpired
}
// Show warning for certificates with validity less than 30 days left.
if remaining < 30*24*time.Hour {
return certStatusWarning
}
return certStatusEnabled
}

View File

@ -11,6 +11,7 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/runtime"
"github.com/traefik/traefik/v3/pkg/config/static"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/version"
)
@ -58,12 +59,14 @@ type Handler struct {
// runtimeConfiguration is the data set used to create all the data representations exposed by the API.
runtimeConfiguration *runtime.Configuration
tlsManager *tls.Manager
}
// NewBuilder returns a http.Handler builder based on runtime.Configuration.
func NewBuilder(staticConfig static.Configuration) func(*runtime.Configuration) http.Handler {
func NewBuilder(staticConfig static.Configuration, tlsManager *tls.Manager) func(*runtime.Configuration) http.Handler {
return func(configuration *runtime.Configuration) http.Handler {
return New(staticConfig, configuration).createRouter()
return New(staticConfig, configuration).WithTLSManager(tlsManager).createRouter()
}
}
@ -81,8 +84,14 @@ func New(staticConfig static.Configuration, runtimeConfig *runtime.Configuration
}
}
// WithTLSManager sets the TLS manager on the handler, enabling the certificate API endpoints.
func (h *Handler) WithTLSManager(tlsManager *tls.Manager) *Handler {
h.tlsManager = tlsManager
return h
}
// createRouter creates API routes and router.
func (h Handler) createRouter() *mux.Router {
func (h *Handler) createRouter() *mux.Router {
router := mux.NewRouter().UseEncodedPath()
apiRouter := router.PathPrefix(h.staticConfig.API.BasePath).Subrouter().UseEncodedPath()
@ -120,12 +129,15 @@ func (h Handler) createRouter() *mux.Router {
apiRouter.Methods(http.MethodGet).Path("/api/udp/services").HandlerFunc(h.getUDPServices)
apiRouter.Methods(http.MethodGet).Path("/api/udp/services/{serviceID}").HandlerFunc(h.getUDPService)
apiRouter.Methods(http.MethodGet).Path("/api/certificates").HandlerFunc(h.getCertificates)
apiRouter.Methods(http.MethodGet).Path("/api/certificates/{certificateID}").HandlerFunc(h.getCertificate)
version.Handler{}.Append(apiRouter)
return router
}
func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.Request) {
siRepr := make(map[string]*serviceInfoRepresentation, len(h.runtimeConfiguration.Services))
for k, v := range h.runtimeConfiguration.Services {
siRepr[k] = &serviceInfoRepresentation{

View File

@ -0,0 +1,99 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
)
func keepCertificate(cert certificateRepresentation, criterion *searchCriterion) bool {
if criterion == nil {
return true
}
// Combine all searchable fields
searchFields := make([]string, 0, 3+len(cert.SANs))
searchFields = append(searchFields, cert.CommonName, cert.IssuerOrg, cert.IssuerCN)
searchFields = append(searchFields, cert.SANs...)
return criterion.withStatus(cert.Status) &&
criterion.searchIn(searchFields...)
}
func (h *Handler) getCertificates(rw http.ResponseWriter, request *http.Request) {
rw.Header().Set("Content-Type", "application/json")
allCerts := h.extractCertificates()
query := request.URL.Query()
criterion := newSearchCriterion(query)
results := make([]certificateRepresentation, 0, len(allCerts))
for _, cert := range allCerts {
if keepCertificate(cert, criterion) {
results = append(results, cert)
}
}
sortCertificates(query, results)
pageInfo, err := pagination(request, len(results))
if err != nil {
writeError(rw, err.Error(), http.StatusBadRequest)
return
}
rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage))
err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex])
if err != nil {
log.Error().Err(err).Msg("Unable to encode certificates")
writeError(rw, err.Error(), http.StatusInternalServerError)
}
}
func (h *Handler) getCertificate(rw http.ResponseWriter, request *http.Request) {
rw.Header().Set("Content-Type", "application/json")
certID := mux.Vars(request)["certificateID"]
if h.tlsManager == nil {
writeError(rw, fmt.Sprintf("certificate not found: %s", certID), http.StatusNotFound)
return
}
certs := h.tlsManager.GetServerCertificates()
x509Cert, ok := certs[certID]
if !ok {
writeError(rw, fmt.Sprintf("certificate not found: %s", certID), http.StatusNotFound)
return
}
cert := buildCertificateRepresentation(x509Cert)
if err := json.NewEncoder(rw).Encode(cert); err != nil {
log.Error().Err(err).Str("id", certID).Msg("Unable to encode certificate")
writeError(rw, err.Error(), http.StatusInternalServerError)
}
}
func (h *Handler) extractCertificates() []certificateRepresentation {
if h.tlsManager == nil {
return []certificateRepresentation{}
}
x509Certs := h.tlsManager.GetServerCertificates()
result := make([]certificateRepresentation, 0, len(x509Certs))
for _, cert := range x509Certs {
rep := buildCertificateRepresentation(cert)
result = append(result, rep)
}
return result
}

View File

@ -0,0 +1,603 @@
package api
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/json"
"encoding/pem"
"io"
"math/big"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/static"
tlspkg "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
)
// generateTestCertificate creates a test certificate with the given parameters.
// The certificate will be valid from notBefore to notAfter.
func generateTestCertificate(commonName string, sans []string, notBefore, notAfter time.Time) (types.FileOrContent, types.FileOrContent, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return "", "", err
}
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return "", "", err
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Acme Co"},
CommonName: commonName,
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}
// Add SANs, distinguishing IP addresses from DNS names.
for _, san := range sans {
if ip := net.ParseIP(san); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, san)
}
}
certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return "", "", err
}
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
})
return types.FileOrContent(certPEM), types.FileOrContent(keyPEM), nil
}
func TestHandler_Certificates(t *testing.T) {
type expected struct {
statusCode int
validateResponse func(t *testing.T, body []byte)
}
type certSetup struct {
loadCerts bool
loadMultipleCerts bool
}
// Generate test certificates dynamically with valid expiration dates
now := time.Now()
// Certificate valid for 50+ years (status: "enabled")
localhostCert, localhostKey, err := generateTestCertificate(
"",
[]string{"127.0.0.1", "::1", "example.com"},
now.Add(-24*time.Hour),
now.Add(50*365*24*time.Hour),
)
require.NoError(t, err)
// Certificate with warning status (expires in 15 days)
warnCert, warnKey, err := generateTestCertificate(
"warning.com",
[]string{"warning.com", "www.warning.com"},
now.Add(-24*time.Hour),
now.Add(15*24*time.Hour),
)
require.NoError(t, err)
// Certificate with expired status (already expired)
expiredCert, expiredKey, err := generateTestCertificate(
"expired.com",
[]string{"expired.com"},
now.Add(-365*24*time.Hour),
now.Add(-24*time.Hour),
)
require.NoError(t, err)
// Certificate for search testing (different common name / SANs)
acmeCert, acmeKey, err := generateTestCertificate(
"acme.example.org",
[]string{"acme.example.org", "api.acme.example.org"},
now.Add(-24*time.Hour),
now.Add(50*365*24*time.Hour),
)
require.NoError(t, err)
// Compute fingerprint from the generated localhost cert PEM
block, _ := pem.Decode([]byte(localhostCert))
parsed, _ := x509.ParseCertificate(block.Bytes)
hash := sha256.Sum256(parsed.Raw)
localhostFingerprint := hex.EncodeToString(hash[:])
testCases := []struct {
desc string
path string
setup certSetup
expected expected
}{
{
desc: "all certificates, but no certificates loaded",
path: "/api/certificates",
setup: certSetup{loadCerts: false},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
assert.Empty(t, certs)
},
},
},
{
desc: "all certificates, with one certificate loaded",
path: "/api/certificates",
setup: certSetup{loadCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 1)
cert := certs[0]
assert.Regexp(t, `^[0-9a-f]{64}$`, cert["name"])
assert.Equal(t, "Acme Co", cert["issuerOrg"])
assert.Equal(t, "enabled", cert["status"])
assert.ElementsMatch(t, []any{"127.0.0.1", "::1", "example.com"}, cert["sans"])
},
},
},
{
desc: "certificates filtered by search text - example",
path: "/api/certificates?search=example",
setup: certSetup{loadCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 1)
sans := certs[0]["sans"].([]any)
assert.Contains(t, sans, "example.com")
},
},
},
{
desc: "certificates filtered by search text - no match",
path: "/api/certificates?search=nonexistent",
setup: certSetup{loadCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
assert.Empty(t, certs)
},
},
},
{
desc: "certificates sorted by status",
path: "/api/certificates?sortBy=status",
setup: certSetup{loadCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 1)
assert.Equal(t, "enabled", certs[0]["status"])
},
},
},
{
desc: "certificates sorted by validUntil descending",
path: "/api/certificates?sortBy=validUntil&direction=desc",
setup: certSetup{loadCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 1)
},
},
},
{
desc: "certificates filtered by status - enabled",
path: "/api/certificates?status=enabled",
setup: certSetup{loadCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 1)
assert.Equal(t, "enabled", certs[0]["status"])
},
},
},
{
desc: "certificates filtered by status - expired",
path: "/api/certificates?status=expired",
setup: certSetup{loadCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
assert.Empty(t, certs)
},
},
},
{
desc: "one certificate by fingerprint",
path: "/api/certificates/" + localhostFingerprint,
setup: certSetup{loadCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var cert map[string]any
require.NoError(t, json.Unmarshal(body, &cert))
assert.Regexp(t, `^[0-9a-f]{64}$`, cert["name"])
assert.Equal(t, "enabled", cert["status"])
assert.ElementsMatch(t, []any{"127.0.0.1", "::1", "example.com"}, cert["sans"])
},
},
},
{
desc: "certificate does not exist",
path: "/api/certificates/non-existent-certificate",
setup: certSetup{loadCerts: false},
expected: expected{
statusCode: http.StatusNotFound,
},
},
{
desc: "multiple certificates with different statuses",
path: "/api/certificates",
setup: certSetup{loadMultipleCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 4)
// Verify all statuses are present
statuses := make(map[string]int)
for _, cert := range certs {
status := cert["status"].(string)
statuses[status]++
}
assert.Equal(t, 2, statuses["enabled"])
assert.Equal(t, 1, statuses["warning"])
assert.Equal(t, 1, statuses["expired"])
},
},
},
{
desc: "certificates sorted by name ascending",
path: "/api/certificates?sortBy=name&direction=asc",
setup: certSetup{loadMultipleCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 4)
// Verify names are in ascending order
prevName := ""
for _, cert := range certs {
commonName := cert["commonName"].(string)
if prevName != "" {
assert.LessOrEqual(t, prevName, commonName)
}
prevName = commonName
}
},
},
},
{
desc: "certificates sorted by name descending",
path: "/api/certificates?sortBy=name&direction=desc",
setup: certSetup{loadMultipleCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 4)
// Verify names are in descending order
prevName := "zzzzzzz"
for _, cert := range certs {
commonName := cert["commonName"].(string)
assert.GreaterOrEqual(t, prevName, commonName)
prevName = commonName
}
},
},
},
{
desc: "certificates sorted by status ascending",
path: "/api/certificates?sortBy=status&direction=asc",
setup: certSetup{loadMultipleCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 4)
// Verify statuses are in ascending order (enabled < expired < warning)
prevStatus := ""
for _, cert := range certs {
status := cert["status"].(string)
if prevStatus != "" {
assert.LessOrEqual(t, prevStatus, status)
}
prevStatus = status
}
},
},
},
{
desc: "certificates sorted by issuer",
path: "/api/certificates?sortBy=issuer",
setup: certSetup{loadMultipleCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 4)
// All certificates have same issuer "Acme Co"
for _, cert := range certs {
assert.Equal(t, "Acme Co", cert["issuerOrg"])
}
},
},
},
{
desc: "certificates sorted by validUntil ascending",
path: "/api/certificates?sortBy=validUntil&direction=asc",
setup: certSetup{loadMultipleCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 4)
// Verify notAfter dates are in ascending order
var prevTime time.Time
for _, cert := range certs {
notAfter := cert["notAfter"].(string)
certTime, err := time.Parse(time.RFC3339, notAfter)
require.NoError(t, err)
if !prevTime.IsZero() {
assert.False(t, certTime.Before(prevTime))
}
prevTime = certTime
}
},
},
},
{
desc: "certificates filtered by status - warning",
path: "/api/certificates?status=warning",
setup: certSetup{loadMultipleCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 1)
assert.Equal(t, "warning", certs[0]["status"])
assert.Contains(t, certs[0]["sans"], "warning.com")
},
},
},
{
desc: "certificates filtered by status - expired",
path: "/api/certificates?status=expired",
setup: certSetup{loadMultipleCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 1)
assert.Equal(t, "expired", certs[0]["status"])
assert.Contains(t, certs[0]["sans"], "expired.com")
},
},
},
{
desc: "certificates filtered by search - commonName",
path: "/api/certificates?search=acme.example.org",
setup: certSetup{loadMultipleCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 1)
assert.Equal(t, "acme.example.org", certs[0]["commonName"])
},
},
},
{
desc: "certificates filtered by search - issuerOrg",
path: "/api/certificates?search=Acme",
setup: certSetup{loadMultipleCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
// All certificates have "Acme Co" as issuer
require.Len(t, certs, 4)
},
},
},
{
desc: "certificates with comprehensive field validation",
path: "/api/certificates",
setup: certSetup{loadMultipleCerts: true},
expected: expected{
statusCode: http.StatusOK,
validateResponse: func(t *testing.T, body []byte) {
t.Helper()
var certs []map[string]any
require.NoError(t, json.Unmarshal(body, &certs))
require.Len(t, certs, 4)
// Check the certificate with commonName set (warning.com)
var certWithCN map[string]any
for _, c := range certs {
if c["commonName"] == "warning.com" {
certWithCN = c
break
}
}
require.NotNil(t, certWithCN, "Should find certificate with commonName")
// Validate all expected fields are present
assert.NotEmpty(t, certWithCN["name"])
assert.NotEmpty(t, certWithCN["sans"])
assert.NotEmpty(t, certWithCN["notAfter"])
assert.NotEmpty(t, certWithCN["notBefore"])
assert.NotEmpty(t, certWithCN["serialNumber"])
assert.Equal(t, "warning.com", certWithCN["commonName"])
assert.NotEmpty(t, certWithCN["issuerOrg"])
assert.NotEmpty(t, certWithCN["version"])
assert.Equal(t, "RSA", certWithCN["keyType"])
assert.InDelta(t, float64(2048), certWithCN["keySize"], 0)
assert.NotEmpty(t, certWithCN["signatureAlgorithm"])
assert.NotEmpty(t, certWithCN["certFingerprint"])
assert.NotEmpty(t, certWithCN["publicKeyFingerprint"])
assert.Equal(t, "warning", certWithCN["status"])
},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
tlsManager := tlspkg.NewManager(nil)
if test.setup.loadCerts {
dynamicConfigs := []*tlspkg.CertAndStores{{
Certificate: tlspkg.Certificate{
CertFile: localhostCert,
KeyFile: localhostKey,
},
}}
tlsManager.UpdateConfigs(t.Context(), nil, nil, dynamicConfigs)
}
if test.setup.loadMultipleCerts {
dynamicConfigs := []*tlspkg.CertAndStores{
{
Certificate: tlspkg.Certificate{
CertFile: localhostCert,
KeyFile: localhostKey,
},
},
{
Certificate: tlspkg.Certificate{
CertFile: warnCert,
KeyFile: warnKey,
},
},
{
Certificate: tlspkg.Certificate{
CertFile: expiredCert,
KeyFile: expiredKey,
},
},
{
Certificate: tlspkg.Certificate{
CertFile: acmeCert,
KeyFile: acmeKey,
},
},
}
tlsManager.UpdateConfigs(t.Context(), nil, nil, dynamicConfigs)
}
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, nil).WithTLSManager(tlsManager)
server := httptest.NewServer(handler.createRouter())
resp, err := http.DefaultClient.Get(server.URL + test.path)
require.NoError(t, err)
require.Equal(t, test.expected.statusCode, resp.StatusCode)
contents, err := io.ReadAll(resp.Body)
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
// Only validate content type and body for success responses
if resp.StatusCode == http.StatusOK {
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
if test.expected.validateResponse != nil {
test.expected.validateResponse(t, contents)
}
}
})
}
}

View File

@ -19,7 +19,7 @@ type entryPointRepresentation struct {
Name string `json:"name,omitempty"`
}
func (h Handler) getEntryPoints(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getEntryPoints(rw http.ResponseWriter, request *http.Request) {
results := make([]entryPointRepresentation, 0, len(h.staticConfig.EntryPoints))
for name, ep := range h.staticConfig.EntryPoints {
@ -50,7 +50,7 @@ func (h Handler) getEntryPoints(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getEntryPoint(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getEntryPoint(rw http.ResponseWriter, request *http.Request) {
scapedEntryPointID := mux.Vars(request)["entryPointID"]
entryPointID, err := url.PathUnescape(scapedEntryPointID)

View File

@ -71,7 +71,7 @@ func newMiddlewareRepresentation(name string, mi *runtime.MiddlewareInfo) middle
}
}
func (h Handler) getRouters(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getRouters(rw http.ResponseWriter, request *http.Request) {
results := make([]routerRepresentation, 0, len(h.runtimeConfiguration.Routers))
query := request.URL.Query()
@ -102,7 +102,7 @@ func (h Handler) getRouters(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getRouter(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getRouter(rw http.ResponseWriter, request *http.Request) {
scapedRouterID := mux.Vars(request)["routerID"]
routerID, err := url.PathUnescape(scapedRouterID)
@ -128,7 +128,7 @@ func (h Handler) getRouter(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getServices(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getServices(rw http.ResponseWriter, request *http.Request) {
results := make([]serviceRepresentation, 0, len(h.runtimeConfiguration.Services))
query := request.URL.Query()
@ -159,7 +159,7 @@ func (h Handler) getServices(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getService(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getService(rw http.ResponseWriter, request *http.Request) {
scapedServiceID := mux.Vars(request)["serviceID"]
serviceID, err := url.PathUnescape(scapedServiceID)
@ -185,7 +185,7 @@ func (h Handler) getService(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getMiddlewares(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getMiddlewares(rw http.ResponseWriter, request *http.Request) {
results := make([]middlewareRepresentation, 0, len(h.runtimeConfiguration.Middlewares))
query := request.URL.Query()
@ -216,7 +216,7 @@ func (h Handler) getMiddlewares(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getMiddleware(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getMiddleware(rw http.ResponseWriter, request *http.Request) {
scapedMiddlewareID := mux.Vars(request)["middlewareID"]
middlewareID, err := url.PathUnescape(scapedMiddlewareID)

View File

@ -8,6 +8,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/runtime"
"github.com/traefik/traefik/v3/pkg/config/static"
"github.com/traefik/traefik/v3/pkg/tls"
)
type schemeOverview struct {
@ -30,14 +31,15 @@ type features struct {
}
type overview struct {
HTTP schemeOverview `json:"http"`
TCP schemeOverview `json:"tcp"`
UDP schemeOverview `json:"udp"`
Features features `json:"features,omitempty"`
Providers []string `json:"providers,omitempty"`
HTTP schemeOverview `json:"http"`
TCP schemeOverview `json:"tcp"`
UDP schemeOverview `json:"udp"`
Certificates *section `json:"certificates,omitempty"`
Features features `json:"features,omitempty"`
Providers []string `json:"providers,omitempty"`
}
func (h Handler) getOverview(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getOverview(rw http.ResponseWriter, request *http.Request) {
result := overview{
HTTP: schemeOverview{
Routers: getHTTPRouterSection(h.runtimeConfiguration.Routers),
@ -53,8 +55,9 @@ func (h Handler) getOverview(rw http.ResponseWriter, request *http.Request) {
Routers: getUDPRouterSection(h.runtimeConfiguration.UDPRouters),
Services: getUDPServiceSection(h.runtimeConfiguration.UDPServices),
},
Features: getFeatures(h.staticConfig),
Providers: getProviders(h.staticConfig),
Certificates: getCertificatesSection(h.tlsManager),
Features: getFeatures(h.staticConfig),
Providers: getProviders(h.staticConfig),
}
rw.Header().Set("Content-Type", "application/json")
@ -285,3 +288,29 @@ func getTracing(conf static.Configuration) string {
return ""
}
func getCertificatesSection(tlsManager *tls.Manager) *section {
if tlsManager == nil {
return nil
}
x509Certs := tlsManager.GetServerCertificates()
var countWarnings int
var countErrors int
for _, cert := range x509Certs {
status := getCertificateStatus(cert.NotAfter)
switch status {
case certStatusExpired:
countErrors++
case certStatusWarning:
countWarnings++
}
}
return &section{
Total: len(x509Certs),
Warnings: countWarnings,
Errors: countErrors,
}
}

View File

@ -232,6 +232,7 @@ func TestHandler_Overview(t *testing.T) {
Global: &static.Global{},
API: &static.API{},
Providers: &static.Providers{
Precedence: []string{"foo"},
Docker: &docker.Provider{},
Swarm: &docker.SwarmProvider{},
File: &file.Provider{},

View File

@ -13,7 +13,7 @@ import (
"github.com/traefik/traefik/v3/pkg/version"
)
func (h Handler) getSupportDump(rw http.ResponseWriter, req *http.Request) {
func (h *Handler) getSupportDump(rw http.ResponseWriter, req *http.Request) {
logger := log.Ctx(req.Context())
staticConfig, err := redactor.Anonymize(h.staticConfig)

View File

@ -66,7 +66,7 @@ func newTCPMiddlewareRepresentation(name string, mi *runtime.TCPMiddlewareInfo)
}
}
func (h Handler) getTCPRouters(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getTCPRouters(rw http.ResponseWriter, request *http.Request) {
results := make([]tcpRouterRepresentation, 0, len(h.runtimeConfiguration.TCPRouters))
query := request.URL.Query()
@ -97,7 +97,7 @@ func (h Handler) getTCPRouters(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getTCPRouter(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getTCPRouter(rw http.ResponseWriter, request *http.Request) {
scapedRouterID := mux.Vars(request)["routerID"]
routerID, err := url.PathUnescape(scapedRouterID)
@ -123,7 +123,7 @@ func (h Handler) getTCPRouter(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getTCPServices(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getTCPServices(rw http.ResponseWriter, request *http.Request) {
results := make([]tcpServiceRepresentation, 0, len(h.runtimeConfiguration.TCPServices))
query := request.URL.Query()
@ -154,7 +154,7 @@ func (h Handler) getTCPServices(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getTCPService(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getTCPService(rw http.ResponseWriter, request *http.Request) {
scapedServiceID := mux.Vars(request)["serviceID"]
serviceID, err := url.PathUnescape(scapedServiceID)
@ -180,7 +180,7 @@ func (h Handler) getTCPService(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getTCPMiddlewares(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getTCPMiddlewares(rw http.ResponseWriter, request *http.Request) {
results := make([]tcpMiddlewareRepresentation, 0, len(h.runtimeConfiguration.Middlewares))
query := request.URL.Query()
@ -211,7 +211,7 @@ func (h Handler) getTCPMiddlewares(rw http.ResponseWriter, request *http.Request
}
}
func (h Handler) getTCPMiddleware(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getTCPMiddleware(rw http.ResponseWriter, request *http.Request) {
scapedMiddlewareID := mux.Vars(request)["middlewareID"]
middlewareID, err := url.PathUnescape(scapedMiddlewareID)

View File

@ -45,7 +45,7 @@ func newUDPServiceRepresentation(name string, si *runtime.UDPServiceInfo) udpSer
}
}
func (h Handler) getUDPRouters(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getUDPRouters(rw http.ResponseWriter, request *http.Request) {
results := make([]udpRouterRepresentation, 0, len(h.runtimeConfiguration.UDPRouters))
query := request.URL.Query()
@ -76,7 +76,7 @@ func (h Handler) getUDPRouters(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getUDPRouter(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getUDPRouter(rw http.ResponseWriter, request *http.Request) {
scapedRouterID := mux.Vars(request)["routerID"]
routerID, err := url.PathUnescape(scapedRouterID)
@ -102,7 +102,7 @@ func (h Handler) getUDPRouter(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getUDPServices(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getUDPServices(rw http.ResponseWriter, request *http.Request) {
results := make([]udpServiceRepresentation, 0, len(h.runtimeConfiguration.UDPServices))
query := request.URL.Query()
@ -133,7 +133,7 @@ func (h Handler) getUDPServices(rw http.ResponseWriter, request *http.Request) {
}
}
func (h Handler) getUDPService(rw http.ResponseWriter, request *http.Request) {
func (h *Handler) getUDPService(rw http.ResponseWriter, request *http.Request) {
scapedServiceID := mux.Vars(request)["serviceID"]
serviceID, err := url.PathUnescape(scapedServiceID)

View File

@ -4,6 +4,7 @@ import (
"cmp"
"net/url"
"sort"
"time"
)
const (
@ -14,6 +15,9 @@ const (
const (
ascendantSorting = "asc"
descendantSorting = "desc"
sortFieldName = "name"
sortFieldStatus = "status"
)
type orderedWithName interface {
@ -31,6 +35,14 @@ type orderedRouter interface {
entryPointsCount() int
}
type orderedCertificate interface {
orderedWithName
status() string
issuer() string
validUntil() time.Time
}
func sortRouters[T orderedRouter](values url.Values, routers []T) {
sortBy := values.Get(sortByParam)
@ -40,7 +52,7 @@ func sortRouters[T orderedRouter](values url.Values, routers []T) {
}
switch sortBy {
case "name":
case sortFieldName:
sortByName(direction, routers)
case "provider":
@ -49,7 +61,7 @@ func sortRouters[T orderedRouter](values url.Values, routers []T) {
case "priority":
sortByFunc(direction, routers, func(i int) int { return routers[i].priority() })
case "status":
case sortFieldStatus:
sortByFunc(direction, routers, func(i int) string { return routers[i].status() })
case "rule":
@ -170,7 +182,7 @@ func sortServices[T orderedService](values url.Values, services []T) {
}
switch sortBy {
case "name":
case sortFieldName:
sortByName(direction, services)
case "type":
@ -182,7 +194,7 @@ func sortServices[T orderedService](values url.Values, services []T) {
case "provider":
sortByFunc(direction, services, func(i int) string { return services[i].provider() })
case "status":
case sortFieldStatus:
sortByFunc(direction, services, func(i int) string { return services[i].status() })
default:
@ -291,7 +303,7 @@ func sortMiddlewares[T orderedMiddleware](values url.Values, middlewares []T) {
}
switch sortBy {
case "name":
case sortFieldName:
sortByName(direction, middlewares)
case "type":
@ -300,7 +312,7 @@ func sortMiddlewares[T orderedMiddleware](values url.Values, middlewares []T) {
case "provider":
sortByFunc(direction, middlewares, func(i int) string { return middlewares[i].provider() })
case "status":
case sortFieldStatus:
sortByFunc(direction, middlewares, func(i int) string { return middlewares[i].status() })
default:
@ -340,6 +352,56 @@ func (m tcpMiddlewareRepresentation) status() string {
return m.Status
}
func sortCertificates[T orderedCertificate](values url.Values, certificates []T) {
sortBy := values.Get(sortByParam)
direction := values.Get(directionParam)
if direction == "" {
direction = ascendantSorting
}
switch sortBy {
case sortFieldName, "cn":
sortByName(direction, certificates)
case sortFieldStatus:
sortByFunc(direction, certificates, func(i int) string { return certificates[i].status() })
case "issuer":
sortByFunc(direction, certificates, func(i int) string { return certificates[i].issuer() })
case "validUntil":
sortByTime(direction, certificates, func(i int) time.Time { return certificates[i].validUntil() })
default:
sortByName(direction, certificates)
}
}
func sortByTime[T orderedWithName](direction string, results []T, fn func(int) time.Time) {
// Ascending
if direction == ascendantSorting {
sort.Slice(results, func(i, j int) bool {
ti, tj := fn(i), fn(j)
if ti.Equal(tj) {
return results[i].name() < results[j].name()
}
return ti.Before(tj)
})
return
}
// Descending
sort.Slice(results, func(i, j int) bool {
ti, tj := fn(i), fn(j)
if ti.Equal(tj) {
return results[i].name() > results[j].name()
}
return ti.After(tj)
})
}
func sortByName[T orderedWithName](direction string, results []T) {
// Ascending
if direction == ascendantSorting {

View File

@ -168,6 +168,10 @@ type RouterObservabilityConfig struct {
// +kubebuilder:validation:Enum=minimal;detailed
// +kubebuilder:default=minimal
TraceVerbosity otypes.TracingVerbosity `json:"traceVerbosity,omitempty" toml:"traceVerbosity,omitempty" yaml:"traceVerbosity,omitempty" export:"true"`
// Metadata holds the metadata for this router.
// Metadata cannot be user-defined for now.
Metadata *ObservabilityMetadata `json:"metadata,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-"`
}
// SetDefaults Default values for a RouterObservabilityConfig.
@ -177,6 +181,23 @@ func (r *RouterObservabilityConfig) SetDefaults() {
// +k8s:deepcopy-gen=true
// ObservabilityMetadata holds the observability metadata configuration.
type ObservabilityMetadata struct {
Ingress *KubernetesIngressMetadata `json:"ingress,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-"`
}
// +k8s:deepcopy-gen=true
// KubernetesIngressMetadata holds the Kubernetes Ingress metadata.
type KubernetesIngressMetadata struct {
Namespace string `json:"namespace,omitempty"`
IngressName string `json:"ingressName,omitempty"`
ServiceName string `json:"serviceName,omitempty"`
ServicePort string `json:"servicePort,omitempty"`
}
// +k8s:deepcopy-gen=true
// Mirroring holds the Mirroring configuration.
type Mirroring struct {
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`

View File

@ -957,6 +957,22 @@ func (in *InFlightReq) DeepCopy() *InFlightReq {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesIngressMetadata) DeepCopyInto(out *KubernetesIngressMetadata) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesIngressMetadata.
func (in *KubernetesIngressMetadata) DeepCopy() *KubernetesIngressMetadata {
if in == nil {
return nil
}
out := new(KubernetesIngressMetadata)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Message) DeepCopyInto(out *Message) {
*out = *in
@ -1245,6 +1261,27 @@ func (in *Model) DeepCopy() *Model {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ObservabilityMetadata) DeepCopyInto(out *ObservabilityMetadata) {
*out = *in
if in.Ingress != nil {
in, out := &in.Ingress, &out.Ingress
*out = new(KubernetesIngressMetadata)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservabilityMetadata.
func (in *ObservabilityMetadata) DeepCopy() *ObservabilityMetadata {
if in == nil {
return nil
}
out := new(ObservabilityMetadata)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PassTLSClientCert) DeepCopyInto(out *PassTLSClientCert) {
*out = *in
@ -1614,6 +1651,11 @@ func (in *RouterObservabilityConfig) DeepCopyInto(out *RouterObservabilityConfig
*out = new(bool)
**out = **in
}
if in.Metadata != nil {
in, out := &in.Metadata, &out.Metadata
*out = new(ObservabilityMetadata)
(*in).DeepCopyInto(*out)
}
return
}

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"regexp"
"slices"
"strings"
"time"
@ -57,6 +58,27 @@ const (
DefaultUDPTimeout = 3 * time.Second
)
// providerNames is the ordered list of the Traefik provider names.
var providerNames = []string{
gateway.ProviderName,
crd.ProviderName,
ingress.ProviderName,
ingressnginx.ProviderName,
docker.SwarmName,
docker.DockerName,
file.ProviderName,
redis.ProviderName,
knative.ProviderName,
consul.ProviderName,
consulcatalog.ProviderName,
nomad.ProviderName,
etcd.ProviderName,
ecs.ProviderName,
http.ProviderName,
zk.ProviderName,
rest.ProviderName,
}
// Allowed characters in URL following RFC 3986 (https://www.rfc-editor.org/rfc/rfc3986#section-2)
var validBasePath = regexp.MustCompile(`^/[a-zA-Z0-9/_.:~-]*$`)
@ -238,6 +260,7 @@ func (t *Tracing) SetDefaults() {
// Providers contains providers configuration.
type Providers struct {
ProvidersThrottleDuration ptypes.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"`
Precedence []string `description:"Defines the routing precedence between providers." json:"precedence,omitempty" toml:"precedence,omitempty" yaml:"precedence,omitempty" export:"true"`
Docker *docker.Provider `description:"Enables Docker provider." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
Swarm *docker.SwarmProvider `description:"Enables Docker Swarm provider." json:"swarm,omitempty" toml:"swarm,omitempty" yaml:"swarm,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
@ -260,6 +283,11 @@ type Providers struct {
Plugin map[string]PluginConf `description:"Plugins configuration." json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty"`
}
// SetDefaults sets the default values.
func (p *Providers) SetDefaults() {
p.Precedence = providerNames
}
// SetEffectiveConfiguration adds missing configuration parameters derived from existing ones.
// It also takes care of maintaining backwards compatibility.
func (c *Configuration) SetEffectiveConfiguration() {
@ -290,6 +318,10 @@ func (c *Configuration) SetEffectiveConfiguration() {
c.Tracing.ResourceAttributes = c.Tracing.GlobalAttributes
}
for i, providerName := range c.Providers.Precedence {
c.Providers.Precedence[i] = strings.ToLower(providerName)
}
if c.Providers.Docker != nil {
if c.Providers.Docker.HTTPClientTimeout < 0 {
c.Providers.Docker.HTTPClientTimeout = 0
@ -426,6 +458,14 @@ func (c *Configuration) ValidateConfiguration() error {
}
}
if c.Providers != nil {
for _, providerName := range c.Providers.Precedence {
if !slices.Contains(providerNames, providerName) {
return fmt.Errorf("provider %q is not a valid provider name", providerName)
}
}
}
if c.Providers != nil && c.Providers.KubernetesIngressNGINX != nil {
if c.Providers.KubernetesIngressNGINX.WatchNamespace != "" && c.Providers.KubernetesIngressNGINX.WatchNamespaceSelector != "" {
return errors.New("watchNamespace and watchNamespaceSelector options are mutually exclusive")

View File

@ -4,6 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/provider/acme"
)
@ -50,7 +51,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
{
desc: "empty",
conf: &Configuration{
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
},
expected: &Configuration{
EntryPoints: EntryPoints{"http": &EntryPoint{
@ -83,13 +84,13 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
Timeout: 3000000000,
},
}},
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
},
},
{
desc: "ACME simple",
conf: &Configuration{
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@ -131,7 +132,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
Timeout: 3000000000,
},
}},
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@ -147,7 +148,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
{
desc: "ACME deprecation DelayBeforeCheck",
conf: &Configuration{
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@ -190,7 +191,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
Timeout: 3000000000,
},
}},
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@ -210,7 +211,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
{
desc: "ACME deprecation DisablePropagationCheck",
conf: &Configuration{
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@ -253,7 +254,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
Timeout: 3000000000,
},
}},
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@ -378,3 +379,55 @@ func TestValidateConfiguration_BasePath(t *testing.T) {
})
}
}
func TestProvidersPrecedence(t *testing.T) {
testCases := []struct {
desc string
cfg *Configuration
expectedError bool
expected []string
}{
{
desc: "No precedence",
cfg: &Configuration{
Providers: &Providers{
Precedence: providerNames,
},
},
expected: providerNames,
},
{
desc: "Precedence with non existing provider",
cfg: &Configuration{
Providers: &Providers{
Precedence: []string{"unknown"},
},
},
expectedError: true,
},
{
desc: "Precedence with upper case provider",
cfg: &Configuration{
Providers: &Providers{
Precedence: []string{"DOCKER"},
},
},
expected: []string{"docker"},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
test.cfg.SetEffectiveConfiguration()
err := test.cfg.ValidateConfiguration()
if test.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, test.cfg.Providers.Precedence)
}
})
}
}

View File

@ -87,6 +87,17 @@ const (
OTelTraceID = "trace_id"
// OTelSpanID is the OTel-conformant log attribute for the span identifier.
OTelSpanID = "span_id"
// Kubernetes Ingress fields.
// KubernetesIngressNamespace is the namespace of the Kubernetes Ingress resource the router handles.
KubernetesIngressNamespace = "KubernetesIngressNamespace"
// KubernetesIngressName is the name of the Kubernetes Ingress resource the router handles.
KubernetesIngressName = "KubernetesIngressName"
// KubernetesServiceName is the name of the Kubernetes service associated with Ingress the router handles.
KubernetesServiceName = "KubernetesServiceName"
// KubernetesServicePort is the port of the Kubernetes service associated with Ingress the router handles.
KubernetesServicePort = "KubernetesServicePort"
)
// These are written out in the default case when no config is provided to specify keys of interest.
@ -94,11 +105,21 @@ var defaultCoreKeys = [...]string{
StartUTC,
Duration,
RouterName,
ServiceAddr,
ServiceName,
ServiceURL,
ClientAddr,
ClientHost,
ClientPort,
ClientUsername,
GzipRatio,
StartLocal,
Overhead,
RetryAttempts,
TLSVersion,
TLSCipher,
TLSClientSubject,
RequestAddr,
RequestHost,
RequestPort,
RequestMethod,
@ -121,18 +142,6 @@ func init() {
for _, k := range defaultCoreKeys {
allCoreKeys[k] = struct{}{}
}
allCoreKeys[ServiceAddr] = struct{}{}
allCoreKeys[ClientAddr] = struct{}{}
allCoreKeys[RequestAddr] = struct{}{}
allCoreKeys[GzipRatio] = struct{}{}
allCoreKeys[StartLocal] = struct{}{}
allCoreKeys[Overhead] = struct{}{}
allCoreKeys[RetryAttempts] = struct{}{}
allCoreKeys[TLSVersion] = struct{}{}
allCoreKeys[TLSCipher] = struct{}{}
allCoreKeys[TLSClientSubject] = struct{}{}
allCoreKeys[OTelTraceID] = struct{}{}
allCoreKeys[OTelSpanID] = struct{}{}
}
// CoreLogData holds the fields computed from the request/response.

View File

@ -204,6 +204,15 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
},
}
if metadata := observability.GetObservabilityMetadata(req.Context()); metadata != nil {
if metadata.Ingress != nil {
logDataTable.Core[KubernetesIngressNamespace] = metadata.Ingress.Namespace
logDataTable.Core[KubernetesIngressName] = metadata.Ingress.IngressName
logDataTable.Core[KubernetesServiceName] = metadata.Ingress.ServiceName
logDataTable.Core[KubernetesServicePort] = metadata.Ingress.ServicePort
}
}
if span := trace.SpanFromContext(req.Context()); span != nil {
spanContext := span.SpanContext()
if spanContext.HasTraceID() && spanContext.HasSpanID() {

View File

@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
otypes "github.com/traefik/traefik/v3/pkg/observability/types"
@ -452,7 +453,7 @@ func TestLoggerHeaderFields(t *testing.T) {
func TestCommonLogger(t *testing.T) {
logFilePath := filepath.Join(t.TempDir(), logFileNameSuffix)
config := &otypes.AccessLog{FilePath: logFilePath, Format: CommonFormat}
doLogging(t, config, false)
doLogging(t, config, false, false)
logData, err := os.ReadFile(logFilePath)
require.NoError(t, err)
@ -464,7 +465,7 @@ func TestCommonLogger(t *testing.T) {
func TestCommonLoggerWithBufferingSize(t *testing.T) {
logFilePath := filepath.Join(t.TempDir(), logFileNameSuffix)
config := &otypes.AccessLog{FilePath: logFilePath, Format: CommonFormat, BufferingSize: 1024}
doLogging(t, config, false)
doLogging(t, config, false, false)
// wait a bit for the buffer to be written in the file.
time.Sleep(50 * time.Millisecond)
@ -479,7 +480,7 @@ func TestCommonLoggerWithBufferingSize(t *testing.T) {
func TestLoggerGenericCLF(t *testing.T) {
logFilePath := filepath.Join(t.TempDir(), logFileNameSuffix)
config := &otypes.AccessLog{FilePath: logFilePath, Format: GenericCLFFormat}
doLogging(t, config, false)
doLogging(t, config, false, false)
logData, err := os.ReadFile(logFilePath)
require.NoError(t, err)
@ -491,7 +492,7 @@ func TestLoggerGenericCLF(t *testing.T) {
func TestLoggerGenericCLFWithBufferingSize(t *testing.T) {
logFilePath := filepath.Join(t.TempDir(), logFileNameSuffix)
config := &otypes.AccessLog{FilePath: logFilePath, Format: GenericCLFFormat, BufferingSize: 1024}
doLogging(t, config, false)
doLogging(t, config, false, false)
// wait a bit for the buffer to be written in the file.
time.Sleep(50 * time.Millisecond)
@ -541,6 +542,7 @@ func TestLoggerJSON(t *testing.T) {
config *otypes.AccessLog
tls bool
tracing bool
metadata bool
expected map[string]func(t *testing.T, value any)
}{
{
@ -626,6 +628,50 @@ func TestLoggerJSON(t *testing.T) {
OTelSpanID: assertString("0100000000000000"),
},
},
{
desc: "default config with metadata",
config: &otypes.AccessLog{
FilePath: "",
Format: JSONFormat,
},
metadata: true,
expected: map[string]func(t *testing.T, value any){
RequestContentSize: assertFloat64(0),
RequestHost: assertString(testHostname),
RequestAddr: assertString(testHostname),
RequestMethod: assertString(testMethod),
RequestPath: assertString(testPath),
RequestProtocol: assertString(testProto),
RequestScheme: assertString(testScheme),
RequestPort: assertString("-"),
DownstreamStatus: assertFloat64(float64(testStatus)),
DownstreamContentSize: assertFloat64(float64(len(testContent))),
OriginContentSize: assertFloat64(float64(len(testContent))),
OriginStatus: assertFloat64(float64(testStatus)),
RequestRefererHeader: assertString(testReferer),
RequestUserAgentHeader: assertString(testUserAgent),
RouterName: assertString(testRouterName),
ServiceURL: assertString(testServiceName),
ClientUsername: assertString(testUsername),
ClientHost: assertString(testHostname),
ClientPort: assertString(strconv.Itoa(testPort)),
ClientAddr: assertString(fmt.Sprintf("%s:%d", testHostname, testPort)),
"level": assertString("info"),
"msg": assertString(""),
"downstream_Content-Type": assertString("text/plain; charset=utf-8"),
RequestCount: assertFloat64NotZero(),
Duration: assertFloat64NotZero(),
Overhead: assertFloat64NotZero(),
RetryAttempts: assertFloat64(float64(testRetryAttempts)),
"time": assertNotEmpty(),
"StartLocal": assertNotEmpty(),
"StartUTC": assertNotEmpty(),
KubernetesIngressNamespace: assertString("test-namespace"),
KubernetesIngressName: assertString("test-ingress"),
KubernetesServiceName: assertString("test-service"),
KubernetesServicePort: assertString("test-port"),
},
},
{
desc: "default config, with TLS request",
config: &otypes.AccessLog{
@ -788,9 +834,9 @@ func TestLoggerJSON(t *testing.T) {
test.config.FilePath = logFilePath
if test.tls {
doLoggingTLS(t, test.config, test.tracing)
doLoggingTLS(t, test.config, test.tracing, test.metadata)
} else {
doLogging(t, test.config, test.tracing)
doLogging(t, test.config, test.tracing, test.metadata)
}
logData, err := os.ReadFile(logFilePath)
@ -1062,7 +1108,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
file, restoreStdout := captureStdout(t)
defer restoreStdout()
doLogging(t, test.config, false)
doLogging(t, test.config, false, false)
written, err := os.ReadFile(file.Name())
require.NoError(t, err, "unable to read captured stdout from file")
@ -1150,7 +1196,7 @@ func captureStdout(t *testing.T) (out *os.File, restoreStdout func()) {
return file, restoreStdout
}
func doLoggingTLSOpt(t *testing.T, config *otypes.AccessLog, enableTLS, tracing bool) {
func doLoggingTLSOpt(t *testing.T, config *otypes.AccessLog, enableTLS, tracing, metadata bool) {
t.Helper()
logger, err := NewHandler(t.Context(), config)
require.NoError(t, err)
@ -1199,9 +1245,22 @@ func doLoggingTLSOpt(t *testing.T, config *otypes.AccessLog, enableTLS, tracing
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
obs := observability.Observability{
AccessLogsEnabled: true,
}), nil
}
if metadata {
obs.Metadata = &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: "test-namespace",
IngressName: "test-ingress",
ServiceName: "test-service",
ServicePort: "test-port",
},
}
}
return observability.WithObservabilityHandler(next, obs), nil
})
chain = chain.Append(logger.AliceConstructor())
@ -1211,16 +1270,16 @@ func doLoggingTLSOpt(t *testing.T, config *otypes.AccessLog, enableTLS, tracing
handler.ServeHTTP(httptest.NewRecorder(), req)
}
func doLoggingTLS(t *testing.T, config *otypes.AccessLog, tracing bool) {
func doLoggingTLS(t *testing.T, config *otypes.AccessLog, tracing, metadata bool) {
t.Helper()
doLoggingTLSOpt(t, config, true, tracing)
doLoggingTLSOpt(t, config, true, tracing, metadata)
}
func doLogging(t *testing.T, config *otypes.AccessLog, tracing bool) {
func doLogging(t *testing.T, config *otypes.AccessLog, tracing, metadata bool) {
t.Helper()
doLoggingTLSOpt(t, config, false, tracing)
doLoggingTLSOpt(t, config, false, tracing, metadata)
}
func logWriterTestHandlerFunc(rw http.ResponseWriter, r *http.Request) {

View File

@ -93,9 +93,14 @@ func TestNegotiation(t *testing.T) {
expEncoding: gzipName,
},
{
desc: "multi accept header list, prefer gzip",
// github.com/klauspost/compress v1.18.4 and up use zstd
// as preferred compression if all accept headers have
// an equal "q" (implicit "q=1.0").
//
// see https://github.com/klauspost/compress/pull/1121
desc: "multi accept header list, prefer best",
acceptEncHeader: "gzip, br, zstd",
expEncoding: gzipName,
expEncoding: zstdName,
},
}

View File

@ -19,6 +19,8 @@ const (
xForwardedPrefixHeader = "X-Forwarded-Prefix"
)
var replacementRegex = regexp.MustCompile(`\$[0-9]+`)
// RewriteTarget is a middleware used to replace the path of a URL request.
type rewriteTarget struct {
next http.Handler
@ -72,7 +74,7 @@ func (rt *rewriteTarget) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
newTarget = rt.regexp.ReplaceAllString(currentPath, rt.replacement)
} else {
newTarget = rt.replacement
newTarget = replacementRegex.ReplaceAllString(rt.replacement, "")
}
// If the replacement resolves to an absolute URL, issue a 302 redirect.

View File

@ -161,6 +161,16 @@ func TestRewriteTarget(t *testing.T) {
expectedStatusCode: http.StatusFound,
expectedRedirectURL: "https://bar.example.org/foo",
},
{
desc: "regex with full URL replacement - multiple paths, no regex",
path: "/foo/a/b/c",
config: dynamic.RewriteTarget{
Regex: "",
Replacement: "https://bar.example.org/$1",
},
expectedStatusCode: http.StatusFound,
expectedRedirectURL: "https://bar.example.org/",
},
}
for _, test := range testCases {

View File

@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
@ -19,6 +20,7 @@ type Observability struct {
SemConvMetricsEnabled bool
TracingEnabled bool
DetailedTracingEnabled bool
Metadata *dynamic.ObservabilityMetadata
}
// WithObservabilityHandler sets the observability state in the context for the next handler.
@ -64,6 +66,15 @@ func DetailedTracingEnabled(ctx context.Context) bool {
return ok && obs.DetailedTracingEnabled
}
// GetObservabilityMetadata returns the observability metadata.
func GetObservabilityMetadata(ctx context.Context) *dynamic.ObservabilityMetadata {
obs, ok := ctx.Value(observabilityKey).(Observability)
if ok {
return obs.Metadata
}
return nil
}
// SetStatusErrorf flags the span as in error and log an event.
func SetStatusErrorf(ctx context.Context, format string, args ...any) {
if span := trace.SpanFromContext(ctx); span != nil {

View File

@ -64,8 +64,8 @@ func parseHost(addr string) string {
return host
}
// GetCanonizedHost retrieves the canonized host from the given context (previously stored in the request context by the middleware).
func GetCanonizedHost(ctx context.Context) string {
// GetCanonicalHost retrieves the canonical host from the given context (previously stored in the request context by the middleware).
func GetCanonicalHost(ctx context.Context) string {
if val, ok := ctx.Value(canonicalKey).(string); ok {
return val
}

View File

@ -42,7 +42,7 @@ func TestRequestHost(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
host := GetCanonizedHost(r.Context())
host := GetCanonicalHost(r.Context())
assert.Equal(t, test.expected, host)
})

View File

@ -55,7 +55,7 @@ func getHost(req *http.Request) string {
return h
}
h = requestdecorator.GetCanonizedHost(req.Context())
h = requestdecorator.GetCanonicalHost(req.Context())
if h != "" {
return h
}

View File

@ -6,11 +6,11 @@ import (
"regexp"
"slices"
"strings"
"unicode"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v3/pkg/muxer"
)
var httpFuncs = matcherBuilderFuncs{
@ -69,33 +69,33 @@ func method(tree *matchersTree, methods ...string) error {
}
func host(tree *matchersTree, hosts ...string) error {
host := hosts[0]
hostExpr := hosts[0]
if !IsASCII(host) {
return fmt.Errorf("invalid value %q for Host matcher, non-ASCII characters are not allowed", host)
if !muxer.IsASCII(hostExpr) {
return fmt.Errorf("invalid value %q for Host matcher, non-ASCII characters are not allowed", hostExpr)
}
host = strings.ToLower(host)
hostExpr = strings.ToLower(hostExpr)
tree.matcher = func(req *http.Request) bool {
reqHost := requestdecorator.GetCanonizedHost(req.Context())
reqHost := requestdecorator.GetCanonicalHost(req.Context())
if len(reqHost) == 0 {
return false
}
if reqHost == host {
if muxer.DomainMatchHostExpression(reqHost, hostExpr) {
return true
}
flatH := requestdecorator.GetCNAMEFlatten(req.Context())
if len(flatH) > 0 {
return strings.EqualFold(flatH, host)
return muxer.DomainMatchHostExpression(flatH, hostExpr)
}
// Check for match on trailing period on host
if last := len(host) - 1; last >= 0 && host[last] == '.' {
h := host[:last]
if reqHost == h {
if last := len(hostExpr) - 1; last >= 0 && hostExpr[last] == '.' {
h := hostExpr[:last]
if muxer.DomainMatchHostExpression(reqHost, h) {
return true
}
}
@ -103,7 +103,7 @@ func host(tree *matchersTree, hosts ...string) error {
// Check for match on trailing period on request
if last := len(reqHost) - 1; last >= 0 && reqHost[last] == '.' {
h := reqHost[:last]
if h == host {
if muxer.DomainMatchHostExpression(h, hostExpr) {
return true
}
}
@ -117,7 +117,7 @@ func host(tree *matchersTree, hosts ...string) error {
func hostRegexp(tree *matchersTree, hosts ...string) error {
host := hosts[0]
if !IsASCII(host) {
if !muxer.IsASCII(host) {
return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host)
}
@ -127,7 +127,7 @@ func hostRegexp(tree *matchersTree, hosts ...string) error {
}
tree.matcher = func(req *http.Request) bool {
return re.MatchString(requestdecorator.GetCanonizedHost(req.Context())) ||
return re.MatchString(requestdecorator.GetCanonicalHost(req.Context())) ||
re.MatchString(requestdecorator.GetCNAMEFlatten(req.Context()))
}
@ -252,14 +252,3 @@ func queryRegexp(tree *matchersTree, queries ...string) error {
return nil
}
// IsASCII checks if the given string contains only ASCII characters.
func IsASCII(s string) bool {
for i := range len(s) {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}

View File

@ -72,9 +72,9 @@ func TestClientIPMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -147,9 +147,9 @@ func TestMethodMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -256,6 +256,16 @@ func TestHostMatcher(t *testing.T) {
"https://🦭.com": http.StatusNotFound,
},
},
{
desc: "wildcard matcher",
rule: "Host(`*.example.com`)",
expected: map[string]int{
"https://test.example.com": http.StatusOK,
"https://other.example.com": http.StatusOK,
"https://example.com": http.StatusNotFound,
"https://test.otherexample.com": http.StatusNotFound,
},
},
}
for _, test := range testCases {
@ -266,9 +276,9 @@ func TestHostMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -367,9 +377,9 @@ func TestHostRegexpMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -442,9 +452,9 @@ func TestPathMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -536,9 +546,9 @@ func TestPathRegexpMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -609,9 +619,9 @@ func TestPathPrefixMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -697,9 +707,9 @@ func TestHeaderMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -806,9 +816,9 @@ func TestHeaderRegexpMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -896,9 +906,9 @@ func TestQueryMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -1011,9 +1021,9 @@ func TestQueryRegexpMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return

View File

@ -9,6 +9,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v3/pkg/muxer"
)
var httpFuncsV2 = matcherBuilderFuncs{
@ -78,7 +79,7 @@ func pathPrefixV2(tree *matchersTree, paths ...string) error {
func hostV2(tree *matchersTree, hosts ...string) error {
for i, host := range hosts {
if !IsASCII(host) {
if !muxer.IsASCII(host) {
return fmt.Errorf("invalid value %q for \"Host\" matcher, non-ASCII characters are not allowed", host)
}
@ -86,7 +87,7 @@ func hostV2(tree *matchersTree, hosts ...string) error {
}
tree.matcher = func(req *http.Request) bool {
reqHost := requestdecorator.GetCanonizedHost(req.Context())
reqHost := requestdecorator.GetCanonicalHost(req.Context())
if len(reqHost) == 0 {
// If the request is an HTTP/1.0 request, then a Host may not be defined.
if req.ProtoAtLeast(1, 1) {
@ -206,7 +207,7 @@ func hostRegexpV2(tree *matchersTree, hosts ...string) error {
router := mux.NewRouter()
for _, host := range hosts {
if !IsASCII(host) {
if !muxer.IsASCII(host) {
return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host)
}

View File

@ -76,9 +76,9 @@ func TestClientIPV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -154,9 +154,9 @@ func TestMethodV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -280,9 +280,9 @@ func TestHostV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -384,9 +384,9 @@ func TestHostRegexpV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -480,9 +480,9 @@ func TestPathV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -574,9 +574,9 @@ func TestPathPrefixV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -662,9 +662,9 @@ func TestHeadersMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -771,9 +771,9 @@ func TestHeaderRegexpV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -865,9 +865,9 @@ func TestHostRegexp(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.hostExp, "v2", 0, handler)
err = muxer.AddRoute(test.hostExp, "v2", 0, "", handler)
require.NoError(t, err)
results := make(map[string]int)
@ -1534,9 +1534,9 @@ func Test_addRoute(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
} else {

View File

@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"net/url"
"slices"
"sort"
"strings"
@ -24,15 +25,20 @@ type MatcherFunc func(*http.Request) bool
type Muxer struct {
routes routes
parser SyntaxParser
defaultHandler http.Handler
parser SyntaxParser
defaultHandler http.Handler
providersPrecedence []string
}
// NewMuxer returns a new muxer instance.
func NewMuxer(parser SyntaxParser) *Muxer {
func NewMuxer(parser SyntaxParser, providersPrecedence []string) *Muxer {
providersPrecedence = slices.Clone(providersPrecedence)
slices.Reverse(providersPrecedence)
return &Muxer{
parser: parser,
defaultHandler: http.NotFoundHandler(),
parser: parser,
defaultHandler: http.NotFoundHandler(),
providersPrecedence: providersPrecedence,
}
}
@ -71,16 +77,17 @@ func GetRulePriority(rule string) int {
}
// AddRoute add a new route to the router.
func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler http.Handler) error {
func (m *Muxer) AddRoute(rule string, syntax string, priority int, providerName string, handler http.Handler) error {
matchers, err := m.parser.parse(syntax, rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
m.routes = append(m.routes, &route{
handler: handler,
matchers: matchers,
priority: priority,
handler: handler,
matchers: matchers,
priority: priority,
providerPriority: slices.Index(m.providersPrecedence, providerName),
})
sort.Sort(m.routes)
@ -206,7 +213,10 @@ func (r routes) Len() int { return len(r) }
func (r routes) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
// Less implements sort.Interface.
func (r routes) Less(i, j int) bool { return r[i].priority > r[j].priority }
func (r routes) Less(i, j int) bool {
return r[i].priority > r[j].priority ||
(r[i].priority == r[j].priority && r[i].providerPriority > r[j].providerPriority)
}
// route holds the matchers to match HTTP route,
// and the handler that will serve the request.
@ -218,6 +228,8 @@ type route struct {
// priority is used to disambiguate between two (or more) rules that would all match for a given request.
// Computed from the matching rule length, if not user-set.
priority int
// providerPriority is used to disambiguate between two (or more) rules that would all match for a given request and have the same priority.
providerPriority int
}
// matchersTree represents the matchers tree structure.

View File

@ -228,10 +228,10 @@ func TestMuxer(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@ -264,9 +264,10 @@ func TestMuxer(t *testing.T) {
func Test_addRoutePriority(t *testing.T) {
type Case struct {
xFrom string
rule string
priority int
xFrom string
rule string
priority int
providerName string
}
testCases := []struct {
@ -375,6 +376,120 @@ func Test_addRoutePriority(t *testing.T) {
},
expected: "header3",
},
{
desc: "Same priority and rule, kubernetescrd wins over kubernetes",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetes",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetescrd",
},
},
expected: "header2",
},
{
desc: "Same priority and rule, kubernetesgateway wins over kubernetescrd",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetescrd",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetesgateway",
},
},
expected: "header2",
},
{
desc: "Same priority and rule, kubernetesgateway wins over kubernetes",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetesgateway",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetes",
},
},
expected: "header1",
},
{
desc: "Same priority and rule, kubernetescrd wins over kubernetesingressnginx",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetesingressnginx",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetescrd",
},
},
expected: "header2",
},
{
desc: "Same priority and rule, known provider wins over unknown provider",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "unknownprovider",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetes",
},
},
expected: "header2",
},
{
desc: "Higher numeric priority wins regardless of provider",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 20,
providerName: "kubernetesingressnginx",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetesgateway",
},
},
expected: "header1",
},
}
for _, test := range testCases {
@ -383,7 +498,7 @@ func Test_addRoutePriority(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, []string{"kubernetesgateway", "kubernetescrd", "kubernetes", "kubernetesingressnginx"})
for _, route := range test.cases {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -394,7 +509,7 @@ func Test_addRoutePriority(t *testing.T) {
route.priority = GetRulePriority(route.rule)
}
err := muxer.AddRoute(route.rule, "", route.priority, handler)
err := muxer.AddRoute(route.rule, "", route.priority, route.providerName, handler)
require.NoError(t, err, route.rule)
}
@ -517,9 +632,9 @@ func TestEmptyHost(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
require.NoError(t, err)
// RequestDecorator is necessary for the host rule

31
pkg/muxer/muxer.go Normal file
View File

@ -0,0 +1,31 @@
package muxer
import (
"strings"
"unicode"
)
// IsASCII checks if the given string contains only ASCII characters.
func IsASCII(s string) bool {
for i := range len(s) {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}
// DomainMatchHostExpression returns true if the domain matches the host expression.
// The host expression can be a wildcard, in which case it will match any subdomain of the domain.
// For example, if the domain is "example.com" and the host expression is "*.example.com", this function will return true.
// If the host expression is "example.com", this function will also return true.
func DomainMatchHostExpression(domain string, hostExpr string) bool {
if strings.HasPrefix(hostExpr, "*") {
labels := strings.Split(domain, ".")
labels[0] = "*"
return strings.EqualFold(hostExpr, strings.Join(labels, "."))
}
return strings.EqualFold(domain, hostExpr)
}

View File

@ -5,11 +5,11 @@ import (
"regexp"
"slices"
"strings"
"unicode"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/muxer"
)
var tcpFuncs = map[string]func(*matchersTree, ...string) error{
@ -62,36 +62,31 @@ func clientIP(tree *matchersTree, clientIP ...string) error {
return nil
}
var hostOrIP = regexp.MustCompile(`^[[:word:]\.\-\:]+$`)
var hostOrIP = regexp.MustCompile(`^(\*\.)?[[:word:]\.\-\:]+$`)
// hostSNI checks if the SNI Host of the connection match the matcher host.
func hostSNI(tree *matchersTree, hosts ...string) error {
host := hosts[0]
hostExpr := hosts[0]
if host == "*" {
if hostExpr == "*" {
// Since a HostSNI(`*`) rule has been provided as catchAll for non-TLS TCP,
// it allows matching with an empty serverName.
tree.matcher = func(meta ConnData) bool { return true }
return nil
}
if !hostOrIP.MatchString(host) {
return fmt.Errorf("invalid value for HostSNI matcher, %q is not a valid hostname", host)
if !hostOrIP.MatchString(hostExpr) {
return fmt.Errorf("invalid value for HostSNI matcher, %q is not a valid hostname", hostExpr)
}
hostExpr = strings.TrimSuffix(hostExpr, ".")
tree.matcher = func(meta ConnData) bool {
if meta.serverName == "" {
return false
}
if host == meta.serverName {
return true
}
// trim trailing period in case of FQDN
host = strings.TrimSuffix(host, ".")
return host == meta.serverName
return muxer.DomainMatchHostExpression(meta.serverName, hostExpr)
}
return nil
@ -101,7 +96,7 @@ func hostSNI(tree *matchersTree, hosts ...string) error {
func hostSNIRegexp(tree *matchersTree, templates ...string) error {
template := templates[0]
if !isASCII(template) {
if !muxer.IsASCII(template) {
return fmt.Errorf("invalid value for HostSNIRegexp matcher, %q is not a valid hostname", template)
}
@ -116,14 +111,3 @@ func hostSNIRegexp(tree *matchersTree, templates ...string) error {
return nil
}
// isASCII checks if the given string contains only ASCII characters.
func isASCII(s string) bool {
for i := range len(s) {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}

View File

@ -34,10 +34,10 @@ func Test_HostSNICatchAll(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
require.NoError(t, err)
handler, catchAll := muxer.Match(ConnData{
@ -71,11 +71,6 @@ func Test_HostSNI(t *testing.T) {
rule: "HostSNI(`example.com`, `example.org`)",
buildErr: true,
},
{
desc: "Invalid HostSNI matcher (globing sub domain)",
rule: "HostSNI(`*.com`)",
buildErr: true,
},
{
desc: "Invalid HostSNI matcher (non ASCII host)",
rule: "HostSNI(`🦭.com`)",
@ -115,12 +110,6 @@ func Test_HostSNI(t *testing.T) {
serverName: "",
match: true,
},
{
desc: "Matching host with trailing dot",
rule: "HostSNI(`example.com.`)",
serverName: "example.com.",
match: true,
},
{
desc: "Matching host with trailing dot but not in server name",
rule: "HostSNI(`example.com.`)",
@ -139,16 +128,28 @@ func Test_HostSNI(t *testing.T) {
serverName: "foo_bar.example.com",
match: true,
},
{
desc: "Matching hosts with subdomains with wildcard",
rule: "HostSNI(`*.example.com`)",
serverName: "foo.example.com",
match: true,
},
{
desc: "Matching hosts with subdomains with wildcard",
rule: "HostSNI(`*.*.example.com`)",
serverName: "toto.foo.example.com",
buildErr: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
if test.buildErr {
require.Error(t, err)
return
@ -227,10 +228,10 @@ func Test_HostSNIRegexp(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
if test.buildErr {
require.Error(t, err)
return
@ -298,10 +299,10 @@ func Test_ClientIP(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
if test.buildErr {
require.Error(t, err)
return
@ -361,10 +362,10 @@ func Test_ALPN(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
if test.buildErr {
require.Error(t, err)
return

View File

@ -21,6 +21,8 @@ var tcpFuncsV2 = map[string]func(*matchersTree, ...string) error{
"HostSNIRegexp": hostSNIRegexpV2,
}
var hostOrIPv2 = regexp.MustCompile(`^[[:word:]\.\-\:]+$`)
func clientIPV2(tree *matchersTree, clientIPs ...string) error {
checker, err := ip.NewChecker(clientIPs)
if err != nil {
@ -80,7 +82,7 @@ func hostSNIV2(tree *matchersTree, hosts ...string) error {
continue
}
if !hostOrIP.MatchString(host) {
if !hostOrIPv2.MatchString(host) {
return fmt.Errorf("invalid value for \"HostSNI\" matcher, %q is not a valid hostname or IP", host)
}

View File

@ -470,10 +470,10 @@ func Test_addTCPRouteV2(t *testing.T) {
require.NoError(t, err)
})
router, err := NewMuxer()
router, err := NewMuxer(nil)
require.NoError(t, err)
err = router.AddRoute(test.rule, "v2", 0, handler)
err = router.AddRoute(test.rule, "v2", 0, "", handler)
if test.routeErr {
require.Error(t, err)
return
@ -606,10 +606,10 @@ func Test_HostSNICatchAllV2(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "v2", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "v2", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
require.NoError(t, err)
handler, catchAll := muxer.Match(ConnData{

View File

@ -3,6 +3,7 @@ package tcp
import (
"fmt"
"net"
"slices"
"sort"
"strings"
@ -27,12 +28,10 @@ func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) (
return ConnData{}, fmt.Errorf("error while parsing remote address %q: %w", conn.RemoteAddr().String(), err)
}
// as per https://datatracker.ietf.org/doc/html/rfc6066:
// > The hostname is represented as a byte string using ASCII encoding without a trailing dot.
// so there is no need to trim a potential trailing dot
serverName = types.CanonicalDomain(serverName)
return ConnData{
// As per https://datatracker.ietf.org/doc/html/rfc6066:
// > The hostname is represented as a byte string using ASCII encoding without a trailing dot.
// so there is no need to trim a potential trailing dot
serverName: types.CanonicalDomain(serverName),
remoteIP: remoteIP,
alpnProtos: alpnProtos,
@ -41,13 +40,15 @@ func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) (
// Muxer defines a muxer that handles TCP routing with rules.
type Muxer struct {
routes routes
parser predicate.Parser
parserV2 predicate.Parser
routes routes
parser predicate.Parser
parserV2 predicate.Parser
providersPrecedence []string
}
// NewMuxer returns a TCP muxer.
func NewMuxer() (*Muxer, error) {
func NewMuxer(providersPrecedence []string) (*Muxer, error) {
var matcherNames []string
for matcherName := range tcpFuncs {
matcherNames = append(matcherNames, matcherName)
@ -68,9 +69,13 @@ func NewMuxer() (*Muxer, error) {
return nil, fmt.Errorf("error while creating v2 rules parser: %w", err)
}
providersPrecedence = slices.Clone(providersPrecedence)
slices.Reverse(providersPrecedence)
return &Muxer{
parser: parser,
parserV2: parserV2,
parser: parser,
parserV2: parserV2,
providersPrecedence: providersPrecedence,
}, nil
}
@ -120,7 +125,7 @@ func GetRulePriority(rule string) int {
// AddRoute adds a new route, associated to the given handler, at the given
// priority, to the muxer.
func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler tcp.Handler) error {
func (m *Muxer) AddRoute(rule string, syntax string, priority int, providerName string, handler tcp.Handler) error {
var parse any
var err error
var matcherFuncs map[string]func(*matchersTree, ...string) error
@ -161,10 +166,11 @@ func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler tcp.H
}
newRoute := &route{
handler: handler,
matchers: matchers,
catchAll: catchAll,
priority: priority,
handler: handler,
matchers: matchers,
catchAll: catchAll,
priority: priority,
providerPriority: slices.Index(m.providersPrecedence, providerName),
}
m.routes = append(m.routes, newRoute)
@ -217,7 +223,10 @@ func (r routes) Len() int { return len(r) }
func (r routes) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
// Less implements sort.Interface.
func (r routes) Less(i, j int) bool { return r[i].priority > r[j].priority }
func (r routes) Less(i, j int) bool {
return r[i].priority > r[j].priority ||
(r[i].priority == r[j].priority && r[i].providerPriority > r[j].providerPriority)
}
// route holds the matchers to match TCP route,
// and the handler that will serve the connection.
@ -232,6 +241,8 @@ type route struct {
// all match for a given request.
// Computed from the matching rule length, if not user-set.
priority int
// providerPriority is used to disambiguate between two (or more) rules that would all match for a given request and have the same priority.
providerPriority int
}
// matchersTree represents the matchers tree structure.

View File

@ -272,10 +272,10 @@ func Test_addTCPRoute(t *testing.T) {
require.NoError(t, err)
})
router, err := NewMuxer()
router, err := NewMuxer(nil)
require.NoError(t, err)
err = router.AddRoute(test.rule, "", 0, handler)
err = router.AddRoute(test.rule, "", 0, "", handler)
if test.routeErr {
require.Error(t, err)
return
@ -388,47 +388,66 @@ func TestParseHostSNI(t *testing.T) {
}
func Test_Priority(t *testing.T) {
type rule struct {
rule string
priority int
provider string
}
testCases := []struct {
desc string
rules map[string]int
serverName string
expectedRule string
desc string
rules []rule
serverName string
expectedRule string
expectedProvider string
}{
{
desc: "One matching rule, calculated priority",
rules: map[string]int{
"HostSNI(`example.com`)": 0,
"HostSNI(`example.org`)": 0,
desc: "One matching rule, same priority",
rules: []rule{
{rule: "HostSNI(`example.com`)"},
{rule: "HostSNI(`example.org`)"},
},
expectedRule: "HostSNI(`example.com`)",
serverName: "example.com",
expectedRule: "HostSNI(`example.com`)",
},
{
desc: "One matching rule, custom priority",
rules: map[string]int{
"HostSNI(`example.org`)": 0,
"HostSNI(`example.com`)": 10000,
rules: []rule{
{rule: "HostSNI(`example.com`)", priority: 10000},
{rule: "HostSNI(`example.org`)"},
},
expectedRule: "HostSNI(`example.org`)",
serverName: "example.org",
expectedRule: "HostSNI(`example.org`)",
},
{
desc: "Two matching rules, calculated priority",
rules: map[string]int{
"HostSNI(`example.org`)": 0,
"HostSNI(`example.com`)": 0,
desc: "Same rule and priority, kubernetescrd wins over kubernetes",
rules: []rule{
{rule: "HostSNI(`example.org`)", provider: "kubernetescrd"},
{rule: "HostSNI(`example.org`)", provider: "kubernetes"},
},
expectedRule: "HostSNI(`example.org`)",
serverName: "example.org",
serverName: "example.org",
expectedRule: "HostSNI(`example.org`)",
expectedProvider: "kubernetescrd",
},
{
desc: "Two matching rules, custom priority",
rules: map[string]int{
"HostSNI(`example.com`)": 10000,
"HostSNI(`example.org`)": 0,
desc: "Same rule and priority, known provider wins over unknown provider",
rules: []rule{
{rule: "HostSNI(`example.org`)", provider: "kubernetescrd"},
{rule: "HostSNI(`example.org`)", provider: "foo"},
},
expectedRule: "HostSNI(`example.com`)",
serverName: "example.com",
serverName: "example.org",
expectedRule: "HostSNI(`example.org`)",
expectedProvider: "kubernetescrd",
},
{
desc: "Same rule, higher numeric priority wins regardless of provider",
rules: []rule{
{rule: "HostSNI(`example.org`)", priority: 10, provider: "kubernetescrd"},
{rule: "HostSNI(`example.org`)", priority: 20, provider: "kubernetes"},
},
serverName: "example.org",
expectedRule: "HostSNI(`example.org`)",
expectedProvider: "kubernetes",
},
}
@ -436,13 +455,17 @@ func Test_Priority(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer([]string{"kubernetescrd", "kubernetes"})
require.NoError(t, err)
matchedRule := ""
for rule, priority := range test.rules {
err := muxer.AddRoute(rule, "", priority, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
matchedRule = rule
var (
matchedRule string
matchedProvider string
)
for _, r := range test.rules {
err := muxer.AddRoute(r.rule, "", r.priority, r.provider, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
matchedRule = r.rule
matchedProvider = r.provider
}))
require.NoError(t, err)
}
@ -454,6 +477,7 @@ func Test_Priority(t *testing.T) {
handler.ServeTCP(nil)
assert.Equal(t, test.expectedRule, matchedRule)
assert.Equal(t, test.expectedProvider, matchedProvider)
})
}
}

View File

@ -27,8 +27,8 @@ import (
// defaultTemplateRule is the default template for the default rule.
const defaultTemplateRule = "Host(`{{ normalize .Name }}`)"
// providerName is the Consul Catalog provider name.
const providerName = "consulcatalog"
// ProviderName is the Consul Catalog provider name.
const ProviderName = "consulcatalog"
var _ provider.Provider = (*Provider)(nil)
@ -58,7 +58,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
if len(p.Namespaces) == 0 {
return []*Provider{{
Configuration: p.Configuration,
name: providerName,
name: ProviderName,
}}
}
@ -66,7 +66,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
for _, namespace := range p.Namespaces {
providers = append(providers, &Provider{
Configuration: p.Configuration,
name: providerName + "-" + namespace,
name: ProviderName + "-" + namespace,
namespace: namespace,
})
}
@ -145,7 +145,7 @@ func (p *Provider) Init() error {
// In case they didn't initialize Provider with BuildProviders.
if p.name == "" {
p.name = providerName
p.name = ProviderName
}
return nil

View File

@ -21,7 +21,8 @@ import (
"github.com/traefik/traefik/v3/pkg/safe"
)
const dockerName = "docker"
// DockerName is the docker provider name.
const DockerName = "docker"
var _ provider.Provider = (*Provider)(nil)
@ -53,7 +54,7 @@ func (p *Provider) Init() error {
// Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, dockerName).Logger()
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, DockerName).Logger()
ctxLog := logger.WithContext(routineCtx)
operation := func() error {
@ -61,7 +62,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
ctx, cancel := context.WithCancel(ctxLog)
defer cancel()
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, dockerName).Logger().WithContext(ctx)
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, DockerName).Logger().WithContext(ctx)
dockerClient, err := p.createClient(ctxLog)
if err != nil {
@ -88,7 +89,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
configuration := builder.build(ctxLog, dockerDataList)
configurationChan <- dynamic.Message{
ProviderName: dockerName,
ProviderName: DockerName,
Configuration: configuration,
}
@ -111,7 +112,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
configuration := builder.build(ctx, containers)
if configuration != nil {
message := dynamic.Message{
ProviderName: dockerName,
ProviderName: DockerName,
Configuration: configuration,
}
select {

View File

@ -22,7 +22,8 @@ import (
"github.com/traefik/traefik/v3/pkg/safe"
)
const swarmName = "swarm"
// SwarmName is the swarm provider name.
const SwarmName = "swarm"
var _ provider.Provider = (*SwarmProvider)(nil)
@ -57,7 +58,7 @@ func (p *SwarmProvider) Init() error {
// Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, swarmName).Logger()
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, SwarmName).Logger()
ctxLog := logger.WithContext(routineCtx)
operation := func() error {
@ -65,7 +66,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
ctx, cancel := context.WithCancel(ctxLog)
defer cancel()
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, swarmName).Logger().WithContext(ctx)
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, SwarmName).Logger().WithContext(ctx)
dockerClient, err := p.createClient(ctx)
if err != nil {
@ -92,7 +93,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
configuration := builder.build(ctxLog, dockerDataList)
configurationChan <- dynamic.Message{
ProviderName: swarmName,
ProviderName: SwarmName,
Configuration: configuration,
}
if p.Watch {
@ -102,7 +103,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
ticker := time.NewTicker(time.Duration(p.RefreshSeconds))
pool.GoCtx(func(ctx context.Context) {
logger := log.Ctx(ctx).With().Str(logs.ProviderName, swarmName).Logger()
logger := log.Ctx(ctx).With().Str(logs.ProviderName, SwarmName).Logger()
ctx = logger.WithContext(ctx)
defer close(errChan)
@ -119,7 +120,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
configuration := builder.build(ctx, services)
if configuration != nil {
configurationChan <- dynamic.Message{
ProviderName: swarmName,
ProviderName: SwarmName,
Configuration: configuration,
}
}

View File

@ -29,6 +29,9 @@ import (
"github.com/traefik/traefik/v3/pkg/safe"
)
// ProviderName is the ECS provider name.
const ProviderName = "ecs"
// Provider holds configurations of the provider.
type Provider struct {
Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"`
@ -107,7 +110,7 @@ func (p *Provider) Init() error {
// Provide configuration to traefik from ECS.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, "ecs").Logger()
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(routineCtx)
operation := func() error {

View File

@ -26,7 +26,8 @@ import (
"github.com/traefik/traefik/v3/pkg/types"
)
const providerName = "file"
// ProviderName is the file provider name.
const ProviderName = "file"
var _ provider.Provider = (*Provider)(nil)
@ -52,7 +53,7 @@ func (p *Provider) Init() error {
// Provide allows the file provider to provide configurations to traefik
// using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
if p.Watch {
var watchItems []string
@ -174,7 +175,7 @@ func (p *Provider) addWatcher(pool *safe.Pool, items []string, configurationChan
// Process events
pool.GoCtx(func(ctx context.Context) {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
defer watcher.Close()
for {
select {
@ -218,7 +219,7 @@ func (p *Provider) applyConfiguration(configurationChan chan<- dynamic.Message)
// buildConfiguration loads configuration either from file or a directory
// specified by 'Filename'/'Directory' and returns a 'Configuration' object.
func (p *Provider) buildConfiguration() (*dynamic.Configuration, error) {
ctx := log.With().Str(logs.ProviderName, providerName).Logger().WithContext(context.Background())
ctx := log.With().Str(logs.ProviderName, ProviderName).Logger().WithContext(context.Background())
if len(p.Directory) > 0 {
configurations, err := p.collectFileConfigs(ctx, p.Directory, "")

View File

@ -25,6 +25,9 @@ import (
var _ provider.Provider = (*Provider)(nil)
// ProviderName is the http provider name.
const ProviderName = "http"
const defaultMaxResponseBodySize = -1
// Provider is a provider.Provider implementation that queries an HTTP(s) endpoint for a configuration.
@ -78,7 +81,7 @@ func (p *Provider) Init() error {
// Provide allows the provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, "http").Logger()
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(routineCtx)
operation := func() error {

View File

@ -26,10 +26,6 @@ THE SOFTWARE.
package v1alpha1
import (
dynamic "github.com/traefik/traefik/v3/pkg/config/dynamic"
)
// RouteApplyConfiguration represents a declarative configuration of the Route type for use
// with apply.
//
@ -58,7 +54,7 @@ type RouteApplyConfiguration struct {
Middlewares []MiddlewareRefApplyConfiguration `json:"middlewares,omitempty"`
// Observability defines the observability configuration for a router.
// More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/routing/observability/
Observability *dynamic.RouterObservabilityConfig `json:"observability,omitempty"`
Observability *RouterObservabilityConfigApplyConfiguration `json:"observability,omitempty"`
}
// RouteApplyConfiguration constructs a declarative configuration of the Route type for use with
@ -128,7 +124,7 @@ func (b *RouteApplyConfiguration) WithMiddlewares(values ...*MiddlewareRefApplyC
// WithObservability sets the Observability field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Observability field is set to the value of the last call.
func (b *RouteApplyConfiguration) WithObservability(value dynamic.RouterObservabilityConfig) *RouteApplyConfiguration {
b.Observability = &value
func (b *RouteApplyConfiguration) WithObservability(value *RouterObservabilityConfigApplyConfiguration) *RouteApplyConfiguration {
b.Observability = value
return b
}

View File

@ -0,0 +1,85 @@
/*
The MIT License (MIT)
Copyright (c) 2016-2020 Containous SAS; 2020-2026 Traefik Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v1alpha1
import (
types "github.com/traefik/traefik/v3/pkg/observability/types"
)
// RouterObservabilityConfigApplyConfiguration represents a declarative configuration of the RouterObservabilityConfig type for use
// with apply.
//
// RouterObservabilityConfig holds the observability configuration for a router.
// More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/routing/observability/
type RouterObservabilityConfigApplyConfiguration struct {
// AccessLogs enables access logs for this router.
AccessLogs *bool `json:"accessLogs,omitempty"`
// Metrics enables metrics for this router.
Metrics *bool `json:"metrics,omitempty"`
// Tracing enables tracing for this router.
Tracing *bool `json:"tracing,omitempty"`
// TraceVerbosity defines the verbosity level of the tracing for this router.
TraceVerbosity *types.TracingVerbosity `json:"traceVerbosity,omitempty"`
}
// RouterObservabilityConfigApplyConfiguration constructs a declarative configuration of the RouterObservabilityConfig type for use with
// apply.
func RouterObservabilityConfig() *RouterObservabilityConfigApplyConfiguration {
return &RouterObservabilityConfigApplyConfiguration{}
}
// WithAccessLogs sets the AccessLogs field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the AccessLogs field is set to the value of the last call.
func (b *RouterObservabilityConfigApplyConfiguration) WithAccessLogs(value bool) *RouterObservabilityConfigApplyConfiguration {
b.AccessLogs = &value
return b
}
// WithMetrics sets the Metrics field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Metrics field is set to the value of the last call.
func (b *RouterObservabilityConfigApplyConfiguration) WithMetrics(value bool) *RouterObservabilityConfigApplyConfiguration {
b.Metrics = &value
return b
}
// WithTracing sets the Tracing field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Tracing field is set to the value of the last call.
func (b *RouterObservabilityConfigApplyConfiguration) WithTracing(value bool) *RouterObservabilityConfigApplyConfiguration {
b.Tracing = &value
return b
}
// WithTraceVerbosity sets the TraceVerbosity field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the TraceVerbosity field is set to the value of the last call.
func (b *RouterObservabilityConfigApplyConfiguration) WithTraceVerbosity(value types.TracingVerbosity) *RouterObservabilityConfigApplyConfiguration {
b.TraceVerbosity = &value
return b
}

View File

@ -118,6 +118,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &traefikiov1alpha1.RootCAApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("Route"):
return &traefikiov1alpha1.RouteApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("RouterObservabilityConfig"):
return &traefikiov1alpha1.RouterObservabilityConfigApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("RouteTCP"):
return &traefikiov1alpha1.RouteTCPApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("RouteUDP"):

View File

@ -44,7 +44,8 @@ const (
)
const (
providerName = "kubernetescrd"
// ProviderName is the Kubernetes CRD provider name.
ProviderName = "kubernetescrd"
providerNamespaceSeparator = "@"
)
@ -76,7 +77,7 @@ func (p *Provider) Init() error {
// Provide allows the k8s provider to provide configurations to traefik
// using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(context.Background())
k8sClient, err := p.newK8sClient(ctxLog)
@ -131,7 +132,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: providerName,
ProviderName: ProviderName,
Configuration: conf,
}
}
@ -166,7 +167,7 @@ func (p *Provider) FillExtensionBuilderRegistry(registry gateway.ExtensionBuilde
return "", nil, fmt.Errorf("namespace %q is not allowed", namespace)
}
return makeID(namespace, name) + providerNamespaceSeparator + providerName, nil, nil
return makeID(namespace, name) + providerNamespaceSeparator + ProviderName, nil, nil
})
registry.RegisterBackendFuncs(traefikv1alpha1.GroupName, "TraefikService", func(name, namespace string) (string, *dynamic.Service, error) {
@ -174,7 +175,7 @@ func (p *Provider) FillExtensionBuilderRegistry(registry gateway.ExtensionBuilde
return "", nil, fmt.Errorf("namespace %q is not allowed", namespace)
}
return makeID(namespace, name) + providerNamespaceSeparator + providerName, nil, nil
return makeID(namespace, name) + providerNamespaceSeparator + ProviderName, nil, nil
})
}

View File

@ -122,14 +122,22 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
}
r := &dynamic.Router{
Middlewares: mds,
Priority: route.Priority,
RuleSyntax: route.Syntax,
EntryPoints: ingressRoute.Spec.EntryPoints,
Rule: route.Match,
Service: serviceName,
Observability: route.Observability,
ParentRefs: parentRouterNames,
Middlewares: mds,
Priority: route.Priority,
RuleSyntax: route.Syntax,
EntryPoints: ingressRoute.Spec.EntryPoints,
Rule: route.Match,
Service: serviceName,
ParentRefs: parentRouterNames,
}
if route.Observability != nil {
r.Observability = &dynamic.RouterObservabilityConfig{
AccessLogs: route.Observability.AccessLogs,
Metrics: route.Observability.Metrics,
Tracing: route.Observability.Tracing,
TraceVerbosity: route.Observability.TraceVerbosity,
}
}
if ingressRoute.Spec.TLS != nil {
@ -178,7 +186,7 @@ func makeMiddlewareKeys(ctx context.Context, namespace string, middlewares []tra
for _, mi := range middlewares {
name := mi.Name
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+providerName) {
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross namespace references.
@ -521,7 +529,7 @@ func (c configBuilder) makeServersTransportKey(parentNamespace string, serversTr
return "", nil
}
if !c.allowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+providerName) {
if !c.allowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross namespace references.
@ -543,7 +551,7 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L
}
// If the service uses explicitly the provider suffix
sanitizedName := strings.TrimSuffix(svc.Name, providerNamespaceSeparator+providerName)
sanitizedName := strings.TrimSuffix(svc.Name, providerNamespaceSeparator+ProviderName)
service, exists, err := c.client.GetService(namespace, sanitizedName)
if err != nil {
return nil, err
@ -799,7 +807,7 @@ func fullServiceName(ctx context.Context, namespace string, service traefikv1alp
}
name, pName := splitSvcNameProvider(service.Name)
if pName == providerName {
if pName == ProviderName {
return provider.Normalize(fmt.Sprintf("%s-%s", namespace, name))
}

View File

@ -339,7 +339,7 @@ func (p *Provider) makeTCPServersTransportKey(parentNamespace string, serversTra
return "", nil
}
if !p.AllowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+providerName) {
if !p.AllowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross namespace references.

View File

@ -2,6 +2,7 @@ package v1alpha1
import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
otypes "github.com/traefik/traefik/v3/pkg/observability/types"
"github.com/traefik/traefik/v3/pkg/types"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
@ -54,7 +55,22 @@ type Route struct {
Middlewares []MiddlewareRef `json:"middlewares,omitempty"`
// Observability defines the observability configuration for a router.
// More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/routing/observability/
Observability *dynamic.RouterObservabilityConfig `json:"observability,omitempty"`
Observability *RouterObservabilityConfig `json:"observability,omitempty"`
}
// RouterObservabilityConfig holds the observability configuration for a router.
// More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/routing/observability/
type RouterObservabilityConfig struct {
// AccessLogs enables access logs for this router.
AccessLogs *bool `json:"accessLogs,omitempty" toml:"accessLogs,omitempty" yaml:"accessLogs,omitempty" export:"true"`
// Metrics enables metrics for this router.
Metrics *bool `json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
// Tracing enables tracing for this router.
Tracing *bool `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
// TraceVerbosity defines the verbosity level of the tracing for this router.
// +kubebuilder:validation:Enum=minimal;detailed
// +kubebuilder:default=minimal
TraceVerbosity otypes.TracingVerbosity `json:"traceVerbosity,omitempty" toml:"traceVerbosity,omitempty" yaml:"traceVerbosity,omitempty" export:"true"`
}
// TLS holds the TLS configuration.

View File

@ -1382,7 +1382,7 @@ func (in *Route) DeepCopyInto(out *Route) {
}
if in.Observability != nil {
in, out := &in.Observability, &out.Observability
*out = new(dynamic.RouterObservabilityConfig)
*out = new(RouterObservabilityConfig)
(*in).DeepCopyInto(*out)
}
return
@ -1449,6 +1449,37 @@ func (in *RouteUDP) DeepCopy() *RouteUDP {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RouterObservabilityConfig) DeepCopyInto(out *RouterObservabilityConfig) {
*out = *in
if in.AccessLogs != nil {
in, out := &in.AccessLogs, &out.AccessLogs
*out = new(bool)
**out = **in
}
if in.Metrics != nil {
in, out := &in.Metrics, &out.Metrics
*out = new(bool)
**out = **in
}
if in.Tracing != nil {
in, out := &in.Tracing, &out.Tracing
*out = new(bool)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouterObservabilityConfig.
func (in *RouterObservabilityConfig) DeepCopy() *RouterObservabilityConfig {
if in == nil {
return nil
}
out := new(RouterObservabilityConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServerHealthCheck) DeepCopyInto(out *ServerHealthCheck) {
*out = *in

View File

@ -70,6 +70,12 @@ spec:
- group: core
kind: ConfigMap
name: ca-file-2
- group: ""
kind: Secret
name: ca-file
- group: core
kind: Secret
name: ca-file-2
---
apiVersion: v1
@ -88,3 +94,21 @@ metadata:
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==

View File

@ -0,0 +1,78 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret2
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:
- name: https
protocol: HTTPS
port: 443
tls:
certificateRefs:
- kind: Secret
name: supersecret
group: ""
- kind: Secret
name: supersecret2
group: ""
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:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""

View File

@ -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:
- name: https
protocol: HTTPS
port: 443
tls:
certificateRefs:
- kind: Secret
name: supersecret
group: ""
- kind: Secret
name: missing-secret
group: ""
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:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""

View File

@ -585,38 +585,55 @@ 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" {
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != "ConfigMap" && caCertRef.Kind != "Secret") {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidKind),
Message: "Only ConfigMaps are supported",
Message: "Only ConfigMaps and Secrets are supported",
}
}
configMap, err := p.client.GetConfigMap(namespace, string(caCertRef.Name))
if err != nil {
var caCRT string
switch caCertRef.Kind {
case "ConfigMap":
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 "Secret":
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("getting configmap %s/%s: %s", namespace, string(caCertRef.Name), err),
}
}
caCRT, ok := configMap.Data["ca.crt"]
if !ok || 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("configmap %s/%s does not have a ca.crt", namespace, string(caCertRef.Name)),
Message: fmt.Sprintf("%s %s/%s does not have a ca.crt", caCertRef.Kind, namespace, string(caCertRef.Name)),
}
}

View File

@ -34,7 +34,8 @@ import (
)
const (
providerName = "kubernetesgateway"
// ProviderName is the Kubernetes Gateway API provider name.
ProviderName = "kubernetesgateway"
controllerName = "traefik.io/gateway-controller"
@ -166,7 +167,7 @@ func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) {
// Init the provider.
func (p *Provider) Init() error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
var err error
p.client, err = p.newK8sClient(logger.WithContext(context.Background()))
@ -179,7 +180,7 @@ func (p *Provider) Init() error {
// Provide allows the k8s provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(context.Background())
pool.GoCtx(func(ctxPool context.Context) {
@ -221,7 +222,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: providerName,
ProviderName: ProviderName,
Configuration: conf,
}
}
@ -427,7 +428,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
}
func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gateway, conf *dynamic.Configuration) []gatewayListener {
tlsConfigs := make(map[string]*tls.CertAndStores)
tlsCerts := make(map[string]*tls.CertAndStores)
allocatedListeners := make(map[string]struct{})
gatewayListeners := make([]gatewayListener, len(gateway.Spec.Listeners))
@ -528,30 +529,22 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
// TLS
if listener.Protocol == gatev1.HTTPSProtocolType || listener.Protocol == gatev1.TLSProtocolType {
if listener.TLS == nil || (len(listener.TLS.CertificateRefs) == 0 && listener.TLS.Mode != nil && *listener.TLS.Mode != gatev1.TLSModePassthrough) {
// update "Detached" status with "UnsupportedProtocol" reason
if listener.TLS == nil {
gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: "InvalidTLSConfiguration", // TODO check the spec if a proper reason is introduced at some point
Message: fmt.Sprintf("No TLS configuration for Gateway Listener %s:%d and protocol %q",
listener.Name, listener.Port, listener.Protocol),
Message: fmt.Sprintf("No TLS configuration for Gateway Listener %s:%d and protocol %q", listener.Name, listener.Port, listener.Protocol),
})
continue
}
var tlsModeType gatev1.TLSModeType
if listener.TLS.Mode != nil {
tlsModeType = *listener.TLS.Mode
}
isTLSPassthrough := tlsModeType == gatev1.TLSModePassthrough
tlsMode := ptr.Deref(listener.TLS.Mode, gatev1.TLSModeTerminate)
isTLSPassthrough := tlsMode == gatev1.TLSModePassthrough
if isTLSPassthrough && len(listener.TLS.CertificateRefs) > 0 {
// https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayTLSConfig
log.Ctx(ctx).Warn().Msg("In case of Passthrough TLS mode, no TLS settings take effect as the TLS session from the client is NOT terminated at the Gateway")
}
@ -559,7 +552,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
// Protocol TLS -> Passthrough -> TLSRoute
// Protocol TLS -> Terminate -> TLSRoute
// Protocol HTTPS -> Terminate -> HTTPRoute
if listener.Protocol == gatev1.HTTPSProtocolType && isTLSPassthrough {
if isTLSPassthrough && listener.Protocol == gatev1.HTTPSProtocolType {
gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionAccepted),
Status: metav1.ConditionFalse,
@ -568,13 +561,11 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
Reason: string(gatev1.ListenerReasonUnsupportedProtocol),
Message: "HTTPS protocol is not supported with TLS mode Passthrough",
})
continue
}
if !isTLSPassthrough {
if len(listener.TLS.CertificateRefs) == 0 {
// update "ResolvedRefs" status true with "InvalidCertificateRef" reason
gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
@ -583,74 +574,73 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
Reason: string(gatev1.ListenerReasonInvalidCertificateRef),
Message: "One TLS CertificateRef is required in Terminate mode",
})
continue
}
// TODO Should we support multiple certificates?
certificateRef := listener.TLS.CertificateRefs[0]
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) {
errCertConditions = append(errCertConditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonInvalidCertificateRef),
Message: fmt.Sprintf("Unsupported TLS CertificateRef group/kind: %s/%s", groupToString(certificateRef.Group), kindToString(certificateRef.Kind)),
})
continue
}
if certificateRef.Kind == nil || *certificateRef.Kind != "Secret" ||
certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) {
// update "ResolvedRefs" status true with "InvalidCertificateRef" reason
gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonInvalidCertificateRef),
Message: fmt.Sprintf("Unsupported TLS CertificateRef group/kind: %s/%s", groupToString(certificateRef.Group), kindToString(certificateRef.Kind)),
})
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 {
errCertConditions = append(errCertConditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot reference CertificateRef %s/%s: %s", certificateNamespace, certificateRef.Name, err),
})
continue
}
continue
}
certificateNamespace := gateway.Namespace
if certificateRef.Namespace != nil && string(*certificateRef.Namespace) != gateway.Namespace {
certificateNamespace = string(*certificateRef.Namespace)
}
if err := p.isReferenceGranted(kindGateway, gateway.Namespace, groupCore, "Secret", string(certificateRef.Name), certificateNamespace); err != nil {
gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load CertificateRef %s/%s: %s", certificateNamespace, certificateRef.Name, err),
})
continue
}
configKey := certificateNamespace + "/" + string(certificateRef.Name)
if _, tlsExists := tlsConfigs[configKey]; !tlsExists {
tlsConf, err := p.getTLS(certificateRef.Name, certificateNamespace)
if err != nil {
// update "ResolvedRefs" status false with "InvalidCertificateRef" reason
// update "Programmed" status false with "Invalid" reason
gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions,
metav1.Condition{
configKey := certificateNamespace + "/" + string(certificateRef.Name)
if _, tlsExists := listenerTLSCerts[configKey]; !tlsExists {
tlsCert, err := p.getTLSCert(certificateRef.Name, certificateNamespace)
if err != nil {
errCertConditions = append(errCertConditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonInvalidCertificateRef),
Message: fmt.Sprintf("Error while retrieving certificate: %v", err),
},
metav1.Condition{
Type: string(gatev1.ListenerConditionProgrammed),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonInvalid),
Message: fmt.Sprintf("Error while retrieving certificate: %v", err),
},
)
continue
Message: fmt.Sprintf("Cannot load CertificateRef %s/%s: %s", certificateNamespace, certificateRef.Name, err),
})
continue
}
listenerTLSCerts[configKey] = tlsCert
}
}
if len(errCertConditions) > 0 {
gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, errCertConditions...)
gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionProgrammed),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonInvalid),
Message: "Invalid CertificateRefs",
})
continue
}
// Only copy if the certificate TLS config is not already known.
for key, listenerTLSCert := range listenerTLSCerts {
if _, ok := tlsCerts[key]; !ok {
tlsCerts[key] = listenerTLSCert
}
tlsConfigs[configKey] = tlsConf
}
}
}
@ -658,8 +648,8 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
gatewayListeners[i].Attached = true
}
if len(tlsConfigs) > 0 {
conf.TLS.Certificates = append(conf.TLS.Certificates, getTLSConfig(tlsConfigs)...)
if len(tlsCerts) > 0 {
conf.TLS.Certificates = append(conf.TLS.Certificates, getTLSConfig(tlsCerts)...)
}
return gatewayListeners
@ -830,7 +820,7 @@ func (p *Provider) isReferenceGranted(fromKind, fromNamespace, toGroup, toKind,
return nil
}
func (p *Provider) getTLS(secretName gatev1.ObjectName, namespace string) (*tls.CertAndStores, error) {
func (p *Provider) getTLSCert(secretName gatev1.ObjectName, namespace string) (*tls.CertAndStores, error) {
secret, err := p.client.GetSecret(namespace, string(secretName))
if err != nil {
return nil, fmt.Errorf("getting secret: %w", err)

View File

@ -782,6 +782,110 @@ func TestLoadHTTPRoutes(t *testing.T) {
},
},
},
{
desc: "HTTPRoute with multiple TLS certificateRefs",
paths: []string{"services.yml", "httproute/with_multiple_certificaterefs.yml"},
entryPoints: map[string]Entrypoint{"websecure": {
Address: ":443",
}},
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{
"httproute-default-http-app-1-gw-default-my-gateway-ep-websecure-0-af329269dd38031b03e3": {
EntryPoints: []string{"websecure"},
Service: "httproute-default-http-app-1-gw-default-my-gateway-ep-websecure-0-af329269dd38031b03e3-wrr",
Rule: "Host(\"foo.com\") && Path(\"/bar\")",
Priority: 100008,
RuleSyntax: "default",
TLS: &dynamic.RouterTLSConfig{},
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"httproute-default-http-app-1-gw-default-my-gateway-ep-websecure-0-af329269dd38031b03e3-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-http-80",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-http-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(listenerCert),
KeyFile: types.FileOrContent(listenerKey),
},
},
},
},
},
},
{
desc: "HTTPRoute with multiple certificateRefs, one missing",
paths: []string{"services.yml", "httproute/with_multiple_certificaterefs_one_missing.yml"},
entryPoints: map[string]Entrypoint{"websecure": {
Address: ":443",
}},
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{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple HTTPRoute, with multiple hosts",
paths: []string{"services.yml", "httproute/with_multiple_host.yml"},
@ -2356,6 +2460,8 @@ func TestLoadHTTPRoutes(t *testing.T) {
RootCAs: []types.FileOrContent{
"CA1",
"CA2",
"CA1-secret",
"CA2-secret",
},
},
},

View File

@ -21,6 +21,7 @@ type IngressConfig struct {
AuthResponseHeaders *string `annotation:"nginx.ingress.kubernetes.io/auth-response-headers"`
AuthSnippet *string `annotation:"nginx.ingress.kubernetes.io/auth-snippet"`
AuthMethod *string `annotation:"nginx.ingress.kubernetes.io/auth-method"`
EnableGlobalAuth *bool `annotation:"nginx.ingress.kubernetes.io/enable-global-auth"`
AuthTLSSecret *string `annotation:"nginx.ingress.kubernetes.io/auth-tls-secret"`
AuthTLSVerifyClient *string `annotation:"nginx.ingress.kubernetes.io/auth-tls-verify-client"`
@ -80,11 +81,14 @@ type IngressConfig struct {
CORSAllowOrigin *[]string `annotation:"nginx.ingress.kubernetes.io/cors-allow-origin"`
CORSMaxAge *int `annotation:"nginx.ingress.kubernetes.io/cors-max-age"`
EnableAccessLog *bool `annotation:"nginx.ingress.kubernetes.io/enable-access-log"`
WhitelistSourceRange *string `annotation:"nginx.ingress.kubernetes.io/whitelist-source-range"`
AllowlistSourceRange *string `annotation:"nginx.ingress.kubernetes.io/allowlist-source-range"`
LimitRPM *int `annotation:"nginx.ingress.kubernetes.io/limit-rpm"`
LimitRPS *int `annotation:"nginx.ingress.kubernetes.io/limit-rps"`
LimitRPM *int `annotation:"nginx.ingress.kubernetes.io/limit-rpm"`
LimitRPS *int `annotation:"nginx.ingress.kubernetes.io/limit-rps"`
LimitBurstMultiplier *int `annotation:"nginx.ingress.kubernetes.io/limit-burst-multiplier"`
CustomHeaders *string `annotation:"nginx.ingress.kubernetes.io/custom-headers"`
UpstreamVhost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"`

View File

@ -0,0 +1,66 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-access-log-enabled
namespace: default
annotations:
nginx.ingress.kubernetes.io/enable-access-log: "true"
spec:
ingressClassName: nginx
rules:
- host: accesslog-enabled.localhost
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-access-log-disabled
namespace: default
annotations:
nginx.ingress.kubernetes.io/enable-access-log: "false"
spec:
ingressClassName: nginx
rules:
- host: accesslog-disabled.localhost
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-access-log-default
namespace: default
spec:
ingressClassName: nginx
rules:
- host: accesslog-default.localhost
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: whoami
port:
number: 80

Some files were not shown because too many files have changed in this diff Show More