mirror of
https://github.com/traefik/traefik.git
synced 2026-05-04 20:06:21 +02:00
Merge branch v3.7 into master
This commit is contained in:
commit
786f7192e1
2
.github/workflows/template-webui.yaml
vendored
2
.github/workflows/template-webui.yaml
vendored
@ -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:
|
||||
|
||||
43
CHANGELOG.md
43
CHANGELOG.md
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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" %}
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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)"
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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> | |
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
2
go.mod
@ -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
4
go.sum
@ -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=
|
||||
|
||||
36
integration/fixtures/https/https_wildcard_host.toml
Normal file
36
integration/fixtures/https/https_wildcard_host.toml
Normal 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"
|
||||
64
integration/fixtures/https/https_wildcard_tls_options.toml
Normal file
64
integration/fixtures/https/https_wildcard_tls_options.toml
Normal 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"
|
||||
48
integration/fixtures/providers-precedence.toml
Normal file
48
integration/fixtures/providers-precedence.toml
Normal 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 }}"
|
||||
65
integration/fixtures/tcp/wildcard-hostsni-tls-options.toml
Normal file
65
integration/fixtures/tcp/wildcard-hostsni-tls-options.toml
Normal 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"
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
|
||||
9
integration/resources/compose/providers-precedence.yml
Normal file
9
integration/resources/compose/providers-precedence.yml
Normal 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
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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": [
|
||||
|
||||
48
integration/testdata/rawdata-ingress.json
vendored
48
integration/testdata/rawdata-ingress.json
vendored
@ -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": [
|
||||
|
||||
12
integration/testdata/rawdata-ingressclass.json
vendored
12
integration/testdata/rawdata-ingressclass.json
vendored
@ -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": [
|
||||
|
||||
@ -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
172
pkg/api/certificate.go
Normal 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
|
||||
}
|
||||
@ -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{
|
||||
|
||||
99
pkg/api/handler_certificate.go
Normal file
99
pkg/api/handler_certificate.go
Normal 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
|
||||
}
|
||||
603
pkg/api/handler_certificate_test.go
Normal file
603
pkg/api/handler_certificate_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 §ion{
|
||||
Total: len(x509Certs),
|
||||
Warnings: countWarnings,
|
||||
Errors: countErrors,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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{},
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
31
pkg/muxer/muxer.go
Normal 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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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, "")
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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"):
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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==
|
||||
|
||||
@ -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: ""
|
||||
@ -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: ""
|
||||
@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user