Merge branch 'v3.0' of github.com:traefik/traefik

Signed-off-by: baalajimaestro <me@baalajimaestro.me>
This commit is contained in:
baalajimaestro 2024-01-29 10:43:41 +05:30
commit ca45770bbd
Signed by: baalajimaestro
GPG key ID: F93C394FE9BBAFD5
75 changed files with 5654 additions and 269 deletions

1
.gitignore vendored
View file

@ -19,3 +19,4 @@ plugins-storage/
plugins-local/
traefik_changelog.md
integration/tailscale.secret
integration/conformance-reports/

View file

@ -280,3 +280,9 @@ issues:
text: 'unusedwrite: unused write to field'
linters:
- govet
- path: pkg/cli/deprecation.go
linters:
- goconst
- path: pkg/cli/loader_file.go
linters:
- goconst

View file

@ -99,6 +99,17 @@ test-unit:
test-integration: binary
GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -test.timeout=20m -failfast -v $(TESTFLAGS)
## Run the conformance tests
.PHONY: test-gateway-api-conformance
test-gateway-api-conformance: binary
GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -test.run K8sConformanceSuite -k8sConformance=true $(TESTFLAGS)
## TODO: Need to be fixed to work in all situations.
## Run the conformance tests
.PHONY: test-gateway-api-conformance-ci
test-gateway-api-conformance-ci:
GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -test.run K8sConformanceSuite -k8sConformance=true $(TESTFLAGS)
## Pull all Docker images to avoid timeout during integration tests
.PHONY: pull-images
pull-images:

View file

@ -53,7 +53,7 @@ func main() {
// traefik config inits
tConfig := cmd.NewTraefikConfiguration()
loaders := []cli.ResourceLoader{&tcli.FileLoader{}, &tcli.FlagLoader{}, &tcli.EnvLoader{}}
loaders := []cli.ResourceLoader{&tcli.DeprecationLoader{}, &tcli.FileLoader{}, &tcli.FlagLoader{}, &tcli.EnvLoader{}}
cmdTraefik := &cli.Command{
Name: "traefik",

View file

@ -19,7 +19,7 @@ version: '3'
services:
reverse-proxy:
# The official v2 Traefik docker image
# The official v3 Traefik docker image
image: traefik:v3.0
# Enables the web UI and tells Traefik to listen to docker
command: --api.insecure=true --providers.docker

View file

@ -19,6 +19,8 @@ and how it now looks like in v3.
### Docker & Docker Swarm
#### SwarmMode
In v3, the provider Docker has been split into 2 providers:
- Docker provider (without Swarm support)
@ -43,7 +45,7 @@ In v3, the provider Docker has been split into 2 providers:
This configuration is now unsupported and would prevent Traefik to start.
#### Remediation
##### Remediation
In v3, the `swarmMode` should not be used with the Docker provider, and, to use Swarm, the Swarm provider should be used instead.
@ -64,7 +66,35 @@ In v3, the `swarmMode` should not be used with the Docker provider, and, to use
--providers.swarm.endpoint=tcp://127.0.0.1:2377
```
### HTTP3 Experimental Configuration
#### TLS.CAOptional
Docker provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType).
??? example "An example usage of the TLS.CAOptional option"
```yaml tab="File (YAML)"
providers:
docker:
tls:
caOptional: true
```
```toml tab="File (TOML)"
[providers.docker.tls]
caOptional=true
```
```bash tab="CLI"
--providers.docker.tls.caOptional=true
```
##### Remediation
The `tls.caOptional` option should be removed from the Docker provider static configuration.
### Experimental Configuration
#### HTTP3
In v3, HTTP/3 is no longer an experimental feature.
It can be enabled on entry points without the associated `experimental.http3` option, which is now removed.
@ -86,12 +116,14 @@ It is now unsupported and would prevent Traefik to start.
--experimental.http3=true
```
#### Remediation
##### Remediation
The `http3` option should be removed from the static configuration experimental section.
### Consul provider
#### namespace
The Consul provider `namespace` option was deprecated in v2 and is now removed in v3.
It is now unsupported and would prevent Traefik to start.
@ -111,7 +143,7 @@ It is now unsupported and would prevent Traefik to start.
--consul.namespace=foobar
```
#### Remediation
##### Remediation
In v3, the `namespaces` option should be used instead of the `namespace` option.
@ -132,8 +164,36 @@ In v3, the `namespaces` option should be used instead of the `namespace` option.
--consul.namespaces=foobar
```
#### TLS.CAOptional
Consul provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType).
??? example "An example usage of the TLS.CAOptional option"
```yaml tab="File (YAML)"
providers:
consul:
tls:
caOptional: true
```
```toml tab="File (TOML)"
[providers.consul.tls]
caOptional=true
```
```bash tab="CLI"
--providers.consul.tls.caOptional=true
```
##### Remediation
The `tls.caOptional` option should be removed from the Consul provider static configuration.
### ConsulCatalog provider
#### namespace
The ConsulCatalog provider `namespace` option was deprecated in v2 and is now removed in v3.
It is now unsupported and would prevent Traefik to start.
@ -153,7 +213,7 @@ It is now unsupported and would prevent Traefik to start.
--consulCatalog.namespace=foobar
```
#### Remediation
##### Remediation
In v3, the `namespaces` option should be used instead of the `namespace` option.
@ -174,8 +234,37 @@ In v3, the `namespaces` option should be used instead of the `namespace` option.
--consulCatalog.namespaces=foobar
```
#### Endpoint.TLS.CAOptional
ConsulCatalog provider `endpoint.tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType).
??? example "An example usage of the Endpoint.TLS.CAOptional option"
```yaml tab="File (YAML)"
providers:
consulCatalog:
endpoint:
tls:
caOptional: true
```
```toml tab="File (TOML)"
[providers.consulCatalog.endpoint.tls]
caOptional=true
```
```bash tab="CLI"
--providers.consulCatalog.endpoint.tls.caOptional=true
```
##### Remediation
The `endpoint.tls.caOptional` option should be removed from the ConsulCatalog provider static configuration.
### Nomad provider
#### namespace
The Nomad provider `namespace` option was deprecated in v2 and is now removed in v3.
It is now unsupported and would prevent Traefik to start.
@ -195,7 +284,7 @@ It is now unsupported and would prevent Traefik to start.
--nomad.namespace=foobar
```
#### Remediation
##### Remediation
In v3, the `namespaces` option should be used instead of the `namespace` option.
@ -216,6 +305,33 @@ In v3, the `namespaces` option should be used instead of the `namespace` option.
--nomad.namespaces=foobar
```
#### Endpoint.TLS.CAOptional
Nomad provider `endpoint.tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType).
??? example "An example usage of the Endpoint.TLS.CAOptional option"
```yaml tab="File (YAML)"
providers:
nomad:
endpoint:
tls:
caOptional: true
```
```toml tab="File (TOML)"
[providers.nomad.endpoint.tls]
caOptional=true
```
```bash tab="CLI"
--providers.nomad.endpoint.tls.caOptional=true
```
##### Remediation
The `endpoint.tls.caOptional` option should be removed from the Nomad provider static configuration.
### Rancher v1 Provider
In v3, the Rancher v1 provider has been removed because Rancher v1 is [no longer actively maintaned](https://rancher.com/docs/os/v1.x/en/support/),
@ -271,6 +387,90 @@ This configuration is now unsupported and would prevent Traefik to start.
All Marathon provider related configuration should be removed from the static configuration.
### HTTP Provider
#### TLS.CAOptional
HTTP provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType).
??? example "An example usage of the TLS.CAOptional option"
```yaml tab="File (YAML)"
providers:
http:
tls:
caOptional: true
```
```toml tab="File (TOML)"
[providers.http.tls]
caOptional=true
```
```bash tab="CLI"
--providers.http.tls.caOptional=true
```
##### Remediation
The `tls.caOptional` option should be removed from the HTTP provider static configuration.
### ETCD Provider
#### TLS.CAOptional
ETCD provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType).
??? example "An example usage of the TLS.CAOptional option"
```yaml tab="File (YAML)"
providers:
etcd:
tls:
caOptional: true
```
```toml tab="File (TOML)"
[providers.etcd.tls]
caOptional=true
```
```bash tab="CLI"
--providers.etcd.tls.caOptional=true
```
##### Remediation
The `tls.caOptional` option should be removed from the ETCD provider static configuration.
### Redis Provider
#### TLS.CAOptional
Redis provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType).
??? example "An example usage of the TLS.CAOptional option"
```yaml tab="File (YAML)"
providers:
redis:
tls:
caOptional: true
```
```toml tab="File (TOML)"
[providers.redis.tls]
caOptional=true
```
```bash tab="CLI"
--providers.redis.tls.caOptional=true
```
##### Remediation
The `tls.caOptional` option should be removed from the Redis provider static configuration.
### InfluxDB v1
InfluxDB v1.x maintenance [ended in 2021](https://www.influxdata.com/blog/influxdb-oss-and-enterprise-roadmap-update-from-influxdays-emea/).
@ -326,21 +526,16 @@ All Pilot related configuration should be removed from the static configuration.
## Dynamic configuration
### IPWhiteList
### Router Rule Matchers
In v3, we renamed the `IPWhiteList` middleware to `IPAllowList` without changing anything to the configuration.
In v3, a new rule matchers syntax has been introduced for HTTP and TCP routers.
The default rule matchers syntax is now the v3 one, but for backward compatibility this can be configured.
The v2 rule matchers syntax is deprecated and its support will be removed in the next major version.
For this reason, we encourage migrating to the new syntax.
### Deprecated Options Removal
#### New V3 Syntax Notable Changes
- The `tracing.datadog.globaltag` option has been removed.
- The `tls.caOptional` option has been removed from the ForwardAuth middleware, as well as from the HTTP, Consul, Etcd, Redis, ZooKeeper, Consul Catalog, and Docker providers.
- `sslRedirect`, `sslTemporaryRedirect`, `sslHost`, `sslForceHost` and `featurePolicy` options of the Headers middleware have been removed.
- The `forceSlash` option of the StripPrefix middleware has been removed.
- The `preferServerCipherSuites` option has been removed.
### Matchers
In v3, the `Headers` and `HeadersRegexp` matchers have been renamed to `Header` and `HeaderRegexp` respectively.
The `Headers` and `HeadersRegexp` matchers have been renamed to `Header` and `HeaderRegexp` respectively.
`PathPrefix` no longer uses regular expressions to match path prefixes.
@ -355,6 +550,87 @@ and should be explicitly combined using logical operators to mimic previous beha
`HostHeader` has been removed, use `Host` instead.
#### Remediation
##### Configure the Default Syntax In Static Configuration
The default rule matchers syntax is the expected syntax for any router that is not self opt-out from this default value.
It can be configured in the static configuration.
??? example "An example configuration for the default rule matchers syntax"
```yaml tab="File (YAML)"
# static configuration
core:
defaultRuleSyntax: v2
```
```toml tab="File (TOML)"
# static configuration
[core]
defaultRuleSyntax="v2"
```
```bash tab="CLI"
# static configuration
--core.defaultRuleSyntax=v2
```
##### Configure the Syntax Per Router
The rule syntax can also be configured on a per-router basis.
This allows to have heterogeneous router configurations and ease migration.
??? example "An example router with syntax configuration"
```yaml tab="Docker & Swarm"
labels:
- "traefik.http.routers.test.ruleSyntax=v2"
```
```yaml tab="Kubernetes"
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: test.route
namespace: default
spec:
routes:
- match: PathPrefix(`/foo`, `/bar`)
syntax: v2
kind: Rule
```
```yaml tab="Consul Catalog"
- "traefik.http.routers.test.ruleSyntax=v2"
```
```yaml tab="File (YAML)"
http:
routers:
test:
ruleSyntax: v2
```
```toml tab="File (TOML)"
[http.routers]
[http.routers.test]
ruleSyntax = "v2"
```
### IPWhiteList
In v3, we renamed the `IPWhiteList` middleware to `IPAllowList` without changing anything to the configuration.
### Deprecated Options Removal
- The `tracing.datadog.globaltag` option has been removed.
- The `tls.caOptional` option has been removed from the ForwardAuth middleware, as well as from the HTTP, Consul, Etcd, Redis, ZooKeeper, Consul Catalog, and Docker providers.
- `sslRedirect`, `sslTemporaryRedirect`, `sslHost`, `sslForceHost` and `featurePolicy` options of the Headers middleware have been removed.
- The `forceSlash` option of the StripPrefix middleware has been removed.
- The `preferServerCipherSuites` option has been removed.
### TCP LoadBalancer `terminationDelay` option
The TCP LoadBalancer `terminationDelay` option has been removed.
@ -415,3 +691,5 @@ Here are two possible transition strategies:
For legacy stacks that cannot immediately upgrade to the latest vendor agents supporting OTLP ingestion,
using OpenTelemetry (OTel) collectors with appropriate exporters configuration is a viable solution.
This allows continued compatibility with the existing infrastructure.
Please check the [OpenTelemetry Tracing provider documention](../observability/tracing/opentelemetry.md) for more information.

View file

@ -132,6 +132,7 @@
- "traefik.http.routers.router0.middlewares=foobar, foobar"
- "traefik.http.routers.router0.priority=42"
- "traefik.http.routers.router0.rule=foobar"
- "traefik.http.routers.router0.rulesyntax=foobar"
- "traefik.http.routers.router0.service=foobar"
- "traefik.http.routers.router0.tls=true"
- "traefik.http.routers.router0.tls.certresolver=foobar"
@ -144,6 +145,7 @@
- "traefik.http.routers.router1.middlewares=foobar, foobar"
- "traefik.http.routers.router1.priority=42"
- "traefik.http.routers.router1.rule=foobar"
- "traefik.http.routers.router1.rulesyntax=foobar"
- "traefik.http.routers.router1.service=foobar"
- "traefik.http.routers.router1.tls=true"
- "traefik.http.routers.router1.tls.certresolver=foobar"
@ -176,6 +178,7 @@
- "traefik.http.services.service02.loadbalancer.sticky.cookie.secure=true"
- "traefik.http.services.service02.loadbalancer.server.port=foobar"
- "traefik.http.services.service02.loadbalancer.server.scheme=foobar"
- "traefik.http.services.service02.loadbalancer.server.weight=42"
- "traefik.tcp.middlewares.tcpmiddleware01.ipallowlist.sourcerange=foobar, foobar"
- "traefik.tcp.middlewares.tcpmiddleware02.ipwhitelist.sourcerange=foobar, foobar"
- "traefik.tcp.middlewares.tcpmiddleware03.inflightconn.amount=42"
@ -183,6 +186,7 @@
- "traefik.tcp.routers.tcprouter0.middlewares=foobar, foobar"
- "traefik.tcp.routers.tcprouter0.priority=42"
- "traefik.tcp.routers.tcprouter0.rule=foobar"
- "traefik.tcp.routers.tcprouter0.rulesyntax=foobar"
- "traefik.tcp.routers.tcprouter0.service=foobar"
- "traefik.tcp.routers.tcprouter0.tls=true"
- "traefik.tcp.routers.tcprouter0.tls.certresolver=foobar"
@ -196,6 +200,7 @@
- "traefik.tcp.routers.tcprouter1.middlewares=foobar, foobar"
- "traefik.tcp.routers.tcprouter1.priority=42"
- "traefik.tcp.routers.tcprouter1.rule=foobar"
- "traefik.tcp.routers.tcprouter1.rulesyntax=foobar"
- "traefik.tcp.routers.tcprouter1.service=foobar"
- "traefik.tcp.routers.tcprouter1.tls=true"
- "traefik.tcp.routers.tcprouter1.tls.certresolver=foobar"

View file

@ -7,6 +7,7 @@
middlewares = ["foobar", "foobar"]
service = "foobar"
rule = "foobar"
ruleSyntax = "foobar"
priority = 42
[http.routers.Router0.tls]
options = "foobar"
@ -24,6 +25,7 @@
middlewares = ["foobar", "foobar"]
service = "foobar"
rule = "foobar"
ruleSyntax = "foobar"
priority = 42
[http.routers.Router1.tls]
options = "foobar"
@ -56,9 +58,11 @@
[[http.services.Service02.loadBalancer.servers]]
url = "foobar"
weight = 42
[[http.services.Service02.loadBalancer.servers]]
url = "foobar"
weight = 42
[http.services.Service02.loadBalancer.healthCheck]
scheme = "foobar"
mode = "foobar"
@ -353,6 +357,7 @@
middlewares = ["foobar", "foobar"]
service = "foobar"
rule = "foobar"
ruleSyntax = "foobar"
priority = 42
[tcp.routers.TCPRouter0.tls]
passthrough = true
@ -371,6 +376,7 @@
middlewares = ["foobar", "foobar"]
service = "foobar"
rule = "foobar"
ruleSyntax = "foobar"
priority = 42
[tcp.routers.TCPRouter1.tls]
passthrough = true

View file

@ -11,6 +11,7 @@ http:
- foobar
service: foobar
rule: foobar
ruleSyntax: foobar
priority: 42
tls:
options: foobar
@ -33,6 +34,7 @@ http:
- foobar
service: foobar
rule: foobar
ruleSyntax: foobar
priority: 42
tls:
options: foobar
@ -63,7 +65,9 @@ http:
maxAge: 42
servers:
- url: foobar
weight: 42
- url: foobar
weight: 42
healthCheck:
scheme: foobar
mode: foobar
@ -409,6 +413,7 @@ tcp:
- foobar
service: foobar
rule: foobar
ruleSyntax: foobar
priority: 42
tls:
passthrough: true
@ -432,6 +437,7 @@ tcp:
- foobar
service: foobar
rule: foobar
ruleSyntax: foobar
priority: 42
tls:
passthrough: true

View file

@ -195,6 +195,10 @@ spec:
- name
type: object
type: array
syntax:
description: 'Syntax defines the router''s rule syntax. More
info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax'
type: string
required:
- kind
- match
@ -402,6 +406,10 @@ spec:
- port
type: object
type: array
syntax:
description: 'Syntax defines the router''s rule syntax. More
info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax_1'
type: string
required:
- match
type: object

View file

@ -158,6 +158,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/routers/Router0/middlewares/1` | `foobar` |
| `traefik/http/routers/Router0/priority` | `42` |
| `traefik/http/routers/Router0/rule` | `foobar` |
| `traefik/http/routers/Router0/ruleSyntax` | `foobar` |
| `traefik/http/routers/Router0/service` | `foobar` |
| `traefik/http/routers/Router0/tls/certResolver` | `foobar` |
| `traefik/http/routers/Router0/tls/domains/0/main` | `foobar` |
@ -173,6 +174,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/routers/Router1/middlewares/1` | `foobar` |
| `traefik/http/routers/Router1/priority` | `42` |
| `traefik/http/routers/Router1/rule` | `foobar` |
| `traefik/http/routers/Router1/ruleSyntax` | `foobar` |
| `traefik/http/routers/Router1/service` | `foobar` |
| `traefik/http/routers/Router1/tls/certResolver` | `foobar` |
| `traefik/http/routers/Router1/tls/domains/0/main` | `foobar` |
@ -238,7 +240,9 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/services/Service02/loadBalancer/passHostHeader` | `true` |
| `traefik/http/services/Service02/loadBalancer/responseForwarding/flushInterval` | `42s` |
| `traefik/http/services/Service02/loadBalancer/servers/0/url` | `foobar` |
| `traefik/http/services/Service02/loadBalancer/servers/0/weight` | `42` |
| `traefik/http/services/Service02/loadBalancer/servers/1/url` | `foobar` |
| `traefik/http/services/Service02/loadBalancer/servers/1/weight` | `42` |
| `traefik/http/services/Service02/loadBalancer/serversTransport` | `foobar` |
| `traefik/http/services/Service02/loadBalancer/sticky/cookie/httpOnly` | `true` |
| `traefik/http/services/Service02/loadBalancer/sticky/cookie/maxAge` | `42` |
@ -273,6 +277,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/tcp/routers/TCPRouter0/middlewares/1` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/priority` | `42` |
| `traefik/tcp/routers/TCPRouter0/rule` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/ruleSyntax` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/service` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/tls/certResolver` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/tls/domains/0/main` | `foobar` |
@ -289,6 +294,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/tcp/routers/TCPRouter1/middlewares/1` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/priority` | `42` |
| `traefik/tcp/routers/TCPRouter1/rule` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/ruleSyntax` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/service` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/tls/certResolver` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/tls/domains/0/main` | `foobar` |

View file

@ -195,6 +195,10 @@ spec:
- name
type: object
type: array
syntax:
description: 'Syntax defines the router''s rule syntax. More
info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax'
type: string
required:
- kind
- match

View file

@ -129,6 +129,10 @@ spec:
- port
type: object
type: array
syntax:
description: 'Syntax defines the router''s rule syntax. More
info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax_1'
type: string
required:
- match
type: object

View file

@ -105,6 +105,9 @@ Activate TLS-ALPN-01 Challenge. (Default: ```true```)
`--certificatesresolvers.<name>.tailscale`:
Enables Tailscale certificate resolution. (Default: ```true```)
`--core.defaultrulesyntax`:
Defines the rule parser default syntax (v2 or v3) (Default: ```v3```)
`--entrypoints.<name>`:
Entry points definition. (Default: ```false```)

View file

@ -105,6 +105,9 @@ Activate TLS-ALPN-01 Challenge. (Default: ```true```)
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_TAILSCALE`:
Enables Tailscale certificate resolution. (Default: ```true```)
`TRAEFIK_CORE_DEFAULTRULESYNTAX`:
Defines the rule parser default syntax (v2 or v3) (Default: ```v3```)
`TRAEFIK_ENTRYPOINTS_<NAME>`:
Entry points definition. (Default: ```false```)

View file

@ -453,5 +453,8 @@
[experimental.localPlugins.LocalDescriptor1]
moduleName = "foobar"
[core]
defaultRuleSyntax = "foobar"
[spiffe]
workloadAPIAddr = "foobar"

View file

@ -486,5 +486,7 @@ experimental:
LocalDescriptor1:
moduleName: foobar
kubernetesGateway: true
core:
defaultRuleSyntax: foobar
spiffe:
workloadAPIAddr: foobar

View file

@ -515,6 +515,60 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
In this configuration, the priority is configured to allow `Router-2` to handle requests with the `foobar.traefik.com` host.
### RuleSyntax
In Traefik v3 a new rule syntax has been introduced ([migration guide](../../migration/v2-to-v3.md#router-rule-matchers)).
`ruleSyntax` option allows to configure the rule syntax to be used for parsing the rule on a per-router basis.
This allows to have heterogeneous router configurations and ease migration.
??? example "Set rule syntax -- using the [File Provider](../../providers/file.md)"
```yaml tab="File (YAML)"
## Dynamic configuration
http:
routers:
Router-v3:
rule: HostRegexp(`[a-z]+\\.traefik\\.com`)
ruleSyntax: v3
Router-v2:
rule: HostRegexp(`{subdomain:[a-z]+}.traefik.com`)
ruleSyntax: v2
```
```toml tab="File (TOML)"
## Dynamic configuration
[http.routers]
[http.routers.Router-v3]
rule = "HostRegexp(`[a-z]+\\.traefik\\.com`)"
ruleSyntax = v3
[http.routers.Router-v2]
rule = "HostRegexp(`{subdomain:[a-z]+}.traefik.com`)"
ruleSyntax = v2
```
```yaml tab="Kubernetes traefik.io/v1alpha1"
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: test.route
namespace: default
spec:
routes:
# route v3
- match: HostRegexp(`[a-z]+\\.traefik\\.com`)
syntax: v3
kind: Rule
# route v2
- match: HostRegexp(`{subdomain:[a-z]+}.traefik.com`)
syntax: v2
kind: Rule
```
In this configuration, the ruleSyntax is configured to allow `Router-v2` to use v2 syntax,
while for `Router-v3` it is configured to use v3 syntax.
### Middlewares
You can attach a list of [middlewares](../../middlewares/overview.md) to each HTTP router.
@ -1161,6 +1215,60 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
In this configuration, the priority is configured so that `Router-1` will handle requests from `192.168.0.12`.
### RuleSyntax
In Traefik v3 a new rule syntax has been introduced ([migration guide](../../migration/v2-to-v3.md#router-rule-matchers)).
`ruleSyntax` option allows to configure the rule syntax to be used for parsing the rule on a per-router basis.
This allows to have heterogeneous router configurations and ease migration.
??? example "Set rule syntax -- using the [File Provider](../../providers/file.md)"
```yaml tab="File (YAML)"
## Dynamic configuration
tcp:
routers:
Router-v3:
rule: ClientIP(`192.168.0.11`) || ClientIP(`192.168.0.12`)
ruleSyntax: v3
Router-v2:
rule: ClientIP(`192.168.0.11`, `192.168.0.12`)
ruleSyntax: v2
```
```toml tab="File (TOML)"
## Dynamic configuration
[tcp.routers]
[tcp.routers.Router-v3]
rule = "ClientIP(`192.168.0.11`) || ClientIP(`192.168.0.12`)"
ruleSyntax = v3
[tcp.routers.Router-v2]
rule = "ClientIP(`192.168.0.11`, `192.168.0.12`)"
ruleSyntax = v2
```
```yaml tab="Kubernetes traefik.io/v1alpha1"
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: test.route
namespace: default
spec:
routes:
# route v3
- match: ClientIP(`192.168.0.11`) || ClientIP(`192.168.0.12`)
syntax: v3
kind: Rule
# route v2
- match: ClientIP(`192.168.0.11`, `192.168.0.12`)
syntax: v2
kind: Rule
```
In this configuration, the ruleSyntax is configured to allow `Router-v2` to use v2 syntax,
while for `Router-v3` it is configured to use v3 syntax.
### Middlewares
You can attach a list of [middlewares](../../middlewares/overview.md) to each TCP router.

View file

@ -143,6 +143,36 @@ The `url` option point to a specific instance.
url = "http://private-ip-server-1/"
```
The `weight` option allows for weighted load balancing on the servers.
??? example "A Service with Two Servers with Weight -- Using the [File Provider](../../providers/file.md)"
```yaml tab="YAML"
## Dynamic configuration
http:
services:
my-service:
loadBalancer:
servers:
- url: "http://private-ip-server-1/"
weight: 2
- url: "http://private-ip-server-2/"
weight: 1
```
```toml tab="TOML"
## Dynamic configuration
[http.services]
[http.services.my-service.loadBalancer]
[[http.services.my-service.loadBalancer.servers]]
url = "http://private-ip-server-1/"
weight = 2
[[http.services.my-service.loadBalancer.servers]]
url = "http://private-ip-server-2/"
weight = 1
```
#### Load-balancing
For now, only round robin load balancing is supported:

12
go.mod
View file

@ -28,7 +28,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-retryablehttp v0.7.4
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/nomad/api v0.0.0-20231213195942-64e3dca9274b
github.com/hashicorp/nomad/api v0.0.0-20240122103822-8a4bd61caf74
github.com/http-wasm/http-wasm-host-go v0.5.2
github.com/influxdata/influxdb-client-go/v2 v2.7.0
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d
@ -85,12 +85,13 @@ require (
golang.org/x/tools v0.14.0
google.golang.org/grpc v1.59.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.28.3
k8s.io/api v0.28.4
k8s.io/apiextensions-apiserver v0.28.3
k8s.io/apimachinery v0.28.3
k8s.io/client-go v0.28.3
k8s.io/apimachinery v0.28.4
k8s.io/client-go v0.28.4
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
mvdan.cc/xurls/v2 v2.5.0
sigs.k8s.io/controller-runtime v0.16.3
sigs.k8s.io/gateway-api v1.0.0
)
@ -162,6 +163,7 @@ require (
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
github.com/exoscale/egoscale v0.100.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
@ -236,6 +238,7 @@ require (
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@ -254,7 +257,6 @@ require (
github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/ginkgo/v2 v2.11.0 // indirect
github.com/onsi/gomega v1.27.10 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/opencontainers/runc v1.1.5 // indirect

28
go.sum
View file

@ -114,6 +114,8 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
@ -302,6 +304,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc=
github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/exoscale/egoscale v0.100.1 h1:iXsV1Ei7daqe/6FYSCSDyrFs1iUG1l1X9qNh2uMw6z0=
github.com/exoscale/egoscale v0.100.1/go.mod h1:BAb9p4rmyU+Wl400CJZO5270H2sXtdsZjLcm5xMKkz4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@ -358,6 +362,8 @@ github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo=
github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@ -592,8 +598,8 @@ github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
github.com/hashicorp/nomad/api v0.0.0-20231213195942-64e3dca9274b h1:R1UDhkwGltpSPY9bCBBxIMQd+NY9BkN0vFHnJo/8o8w=
github.com/hashicorp/nomad/api v0.0.0-20231213195942-64e3dca9274b/go.mod h1:ijDwa6o1uG1jFSq6kERiX2PamKGpZzTmo0XOFNeFZgw=
github.com/hashicorp/nomad/api v0.0.0-20240122103822-8a4bd61caf74 h1:Q+WuGTnZkL2cJ7yNsg4Go4GNnRkcahGLiQP/WD41TTA=
github.com/hashicorp/nomad/api v0.0.0-20240122103822-8a4bd61caf74/go.mod h1:ijDwa6o1uG1jFSq6kERiX2PamKGpZzTmo0XOFNeFZgw=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
@ -796,6 +802,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
@ -1485,6 +1493,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
@ -1619,14 +1629,14 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM=
k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc=
k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY=
k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0=
k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08=
k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc=
k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4=
k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo=
k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8=
k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg=
k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY=
k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4=
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
@ -1640,6 +1650,8 @@ nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4=
sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0=
sigs.k8s.io/gateway-api v1.0.0 h1:iPTStSv41+d9p0xFydll6d7f7MOBGuqXM6p2/zVYMAs=
sigs.k8s.io/gateway-api v1.0.0/go.mod h1:4cUgr0Lnp5FZ0Cdq8FdRwCvpiWws7LVhLHGIudLlf4c=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
"io"
"net/http"
"strings"
"testing"
"time"
@ -55,6 +56,56 @@ func (s *DockerSuite) TestSimpleConfiguration() {
require.NoError(s.T(), err)
}
func (s *DockerSuite) TestWRRServer() {
tempObjects := struct {
DockerHost string
DefaultRule string
}{
DockerHost: s.getDockerHost(),
DefaultRule: "Host(`{{ normalize .Name }}.docker.localhost`)",
}
file := s.adaptFile("fixtures/docker/simple.toml", tempObjects)
s.composeUp()
s.traefikCmd(withConfigFile(file))
whoami1IP := s.getComposeServiceIP("wrr-server")
whoami2IP := s.getComposeServiceIP("wrr-server2")
// Expected a 404 as we did not configure anything
err := try.GetRequest("http://127.0.0.1:8000/", 500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound))
require.NoError(s.T(), err)
err = try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains("wrr-server"))
require.NoError(s.T(), err)
repartition := map[string]int{}
for i := 0; i < 4; i++ {
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil)
req.Host = "my.wrr.host"
require.NoError(s.T(), err)
response, err := http.DefaultClient.Do(req)
require.NoError(s.T(), err)
assert.Equal(s.T(), http.StatusOK, response.StatusCode)
body, err := io.ReadAll(response.Body)
require.NoError(s.T(), err)
if strings.Contains(string(body), whoami1IP) {
repartition[whoami1IP]++
}
if strings.Contains(string(body), whoami2IP) {
repartition[whoami2IP]++
}
}
assert.Equal(s.T(), 3, repartition[whoami1IP])
assert.Equal(s.T(), 1, repartition[whoami2IP])
}
func (s *DockerSuite) TestDefaultDockerContainers() {
tempObjects := struct {
DockerHost string

View file

@ -195,6 +195,10 @@ spec:
- name
type: object
type: array
syntax:
description: 'Syntax defines the router''s rule syntax. More
info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax'
type: string
required:
- kind
- match
@ -402,6 +406,10 @@ spec:
- port
type: object
type: array
syntax:
description: 'Syntax defines the router''s rule syntax. More
info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax_1'
type: string
required:
- match
type: object

View file

@ -0,0 +1,21 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[api]
insecure = true
[experimental]
kubernetesGateway = true
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.websecure]
address = ":443"
[providers.kubernetesGateway]

View file

@ -0,0 +1,41 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[core]
defaultRuleSyntax = "v2"
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.web]
address = ":8000"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.routers]
[http.routers.router1]
service = "service1"
rule = "PathPrefix(`/foo`, `/bar`)"
[http.routers.router2]
service = "service1"
rule = "QueryRegexp(`foo`, `bar`)"
[http.routers.router3]
service = "service1"
rule = "PathPrefix(`/foo`, `/bar`)"
ruleSyntax = "v3"
[http.services]
[http.services.service1]
[http.services.service1.loadBalancer]
[http.services.service1.loadBalancer.servers]

View file

@ -0,0 +1,38 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.web]
address = ":8000"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.routers]
[http.routers.router1]
service = "service1"
rule = "PathPrefix(`/foo`) || PathPrefix(`/bar`)"
[http.routers.router2]
service = "service1"
rule = "PathPrefix(`/foo`, `/bar`)"
[http.routers.router3]
service = "service1"
rule = "QueryRegexp(`foo`, `bar`)"
ruleSyntax = "v2"
[http.services]
[http.services.service1]
[http.services.service1.loadBalancer]
[http.services.service1.loadBalancer.servers]

View file

@ -0,0 +1,35 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[api]
insecure = true
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.web]
address = ":8000"
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.routers]
[http.routers.router]
service = "service1"
rule = "Path(`/whoami`)"
[http.services]
[http.services.service1.loadBalancer]
[[http.services.service1.loadBalancer.servers]]
url = "{{ .Server1 }}"
weight = 3
[[http.services.service1.loadBalancer.servers]]
url = "{{ .Server2 }}"

View file

@ -35,7 +35,13 @@ import (
"gopkg.in/yaml.v3"
)
var showLog = flag.Bool("tlog", false, "always show Traefik logs")
var (
showLog = flag.Bool("tlog", false, "always show Traefik logs")
k8sConformance = flag.Bool("k8sConformance", false, "run K8s Gateway API conformance test")
k8sConformanceRunTest = flag.String("k8sConformanceRunTest", "", "run a specific K8s Gateway API conformance test")
)
const tailscaleSecretFilePath = "tailscale.secret"
type composeConfig struct {
Services map[string]composeService `yaml:"services"`
@ -99,6 +105,11 @@ func (s *BaseSuite) displayTraefikLogFile(path string) {
}
func (s *BaseSuite) SetupSuite() {
if isDockerDesktop(context.Background(), s.T()) {
_, err := os.Stat(tailscaleSecretFilePath)
require.NoError(s.T(), err, "Tailscale need to be configured when running integration tests with Docker Desktop: (https://doc.traefik.io/traefik/v2.11/contributing/building-testing/#testing)")
}
// configure default standard log.
stdlog.SetFlags(stdlog.Lshortfile | stdlog.LstdFlags)
// TODO
@ -124,7 +135,7 @@ func (s *BaseSuite) SetupSuite() {
s.hostIP = "172.31.42.1"
if isDockerDesktop(ctx, s.T()) {
s.hostIP = getDockerDesktopHostIP(ctx, s.T())
s.setupVPN("tailscale.secret")
s.setupVPN(tailscaleSecretFilePath)
}
}

View file

@ -0,0 +1,229 @@
package integration
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/traefik/traefik/v3/integration/try"
"github.com/traefik/traefik/v3/pkg/version"
"gopkg.in/yaml.v3"
ktypes "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
kclientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
gatev1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
conformanceV1alpha1 "sigs.k8s.io/gateway-api/conformance/apis/v1alpha1"
"sigs.k8s.io/gateway-api/conformance/tests"
"sigs.k8s.io/gateway-api/conformance/utils/config"
ksuite "sigs.k8s.io/gateway-api/conformance/utils/suite"
)
// K8sConformanceSuite tests suite.
type K8sConformanceSuite struct{ BaseSuite }
func TestK8sConformanceSuite(t *testing.T) {
suite.Run(t, new(K8sConformanceSuite))
}
func (s *K8sConformanceSuite) SetupSuite() {
s.BaseSuite.SetupSuite()
s.createComposeProject("k8s")
s.composeUp()
abs, err := filepath.Abs("./fixtures/k8s/config.skip/kubeconfig.yaml")
require.NoError(s.T(), err)
err = try.Do(60*time.Second, func() error {
_, err := os.Stat(abs)
return err
})
require.NoError(s.T(), err)
data, err := os.ReadFile(abs)
require.NoError(s.T(), err)
content := strings.ReplaceAll(string(data), "https://server:6443", fmt.Sprintf("https://%s", net.JoinHostPort(s.getComposeServiceIP("server"), "6443")))
err = os.WriteFile(abs, []byte(content), 0o644)
require.NoError(s.T(), err)
err = os.Setenv("KUBECONFIG", abs)
require.NoError(s.T(), err)
}
func (s *K8sConformanceSuite) TearDownSuite() {
s.BaseSuite.TearDownSuite()
generatedFiles := []string{
"./fixtures/k8s/config.skip/kubeconfig.yaml",
"./fixtures/k8s/config.skip/k3s.log",
"./fixtures/k8s/rolebindings.yaml",
"./fixtures/k8s/ccm.yaml",
}
for _, filename := range generatedFiles {
if err := os.Remove(filename); err != nil {
log.Warn().Err(err).Send()
}
}
}
func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
if !*k8sConformance {
s.T().Skip("Skip because it can take a long time to execute. To enable pass the `k8sConformance` flag.")
}
configFromFlags, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))
if err != nil {
s.T().Fatal(err)
}
kClient, err := client.New(configFromFlags, client.Options{})
if err != nil {
s.T().Fatalf("Error initializing Kubernetes client: %v", err)
}
kClientSet, err := kclientset.NewForConfig(configFromFlags)
if err != nil {
s.T().Fatal(err)
}
err = gatev1alpha2.AddToScheme(kClient.Scheme())
require.NoError(s.T(), err)
err = gatev1beta1.AddToScheme(kClient.Scheme())
require.NoError(s.T(), err)
err = gatev1.AddToScheme(kClient.Scheme())
require.NoError(s.T(), err)
s.traefikCmd(withConfigFile("fixtures/k8s_gateway_conformance.toml"))
// Wait for traefik to start
err = try.GetRequest("http://127.0.0.1:8080/api/entrypoints", 10*time.Second, try.BodyContains(`"name":"web"`))
require.NoError(s.T(), err)
err = try.Do(10*time.Second, func() error {
gwc := &gatev1.GatewayClass{}
err := kClient.Get(context.Background(), ktypes.NamespacedName{Name: "my-gateway-class"}, gwc)
if err != nil {
return fmt.Errorf("error fetching GatewayClass: %w", err)
}
return nil
})
require.NoError(s.T(), err)
opts := ksuite.Options{
Client: kClient,
RestConfig: configFromFlags,
Clientset: kClientSet,
GatewayClassName: "my-gateway-class",
Debug: true,
CleanupBaseResources: true,
TimeoutConfig: config.TimeoutConfig{
CreateTimeout: 5 * time.Second,
DeleteTimeout: 5 * time.Second,
GetTimeout: 5 * time.Second,
GatewayMustHaveAddress: 5 * time.Second,
GatewayMustHaveCondition: 5 * time.Second,
GatewayStatusMustHaveListeners: 10 * time.Second,
GatewayListenersMustHaveCondition: 5 * time.Second,
GWCMustBeAccepted: 60 * time.Second, // Pod creation in k3s cluster can be long.
HTTPRouteMustNotHaveParents: 5 * time.Second,
HTTPRouteMustHaveCondition: 5 * time.Second,
TLSRouteMustHaveCondition: 5 * time.Second,
RouteMustHaveParents: 5 * time.Second,
ManifestFetchTimeout: 5 * time.Second,
MaxTimeToConsistency: 5 * time.Second,
NamespacesMustBeReady: 60 * time.Second, // Pod creation in k3s cluster can be long.
RequestTimeout: 5 * time.Second,
LatestObservedGenerationSet: 5 * time.Second,
RequiredConsecutiveSuccesses: 0,
},
SupportedFeatures: sets.New[ksuite.SupportedFeature]().
Insert(ksuite.GatewayCoreFeatures.UnsortedList()...),
EnableAllSupportedFeatures: false,
RunTest: *k8sConformanceRunTest,
// Until the feature are all supported, following tests are skipped.
SkipTests: []string{
"HTTPExactPathMatching",
"HTTPRouteHostnameIntersection",
"GatewaySecretReferenceGrantAllInNamespace",
"HTTPRouteListenerHostnameMatching",
"HTTPRouteRequestHeaderModifier",
"GatewaySecretInvalidReferenceGrant",
"GatewayClassObservedGenerationBump",
"HTTPRouteInvalidNonExistentBackendRef",
"GatewayWithAttachedRoutes",
"HTTPRouteCrossNamespace",
"HTTPRouteDisallowedKind",
"HTTPRouteInvalidReferenceGrant",
"HTTPRouteObservedGenerationBump",
"GatewayInvalidRouteKind",
"TLSRouteSimpleSameNamespace",
"TLSRouteInvalidReferenceGrant",
"HTTPRouteInvalidCrossNamespaceParentRef",
"HTTPRouteInvalidParentRefNotMatchingSectionName",
"GatewaySecretReferenceGrantSpecific",
"GatewayModifyListeners",
"GatewaySecretMissingReferenceGrant",
"GatewayInvalidTLSConfiguration",
"HTTPRouteInvalidCrossNamespaceBackendRef",
"HTTPRouteMatchingAcrossRoutes",
"HTTPRoutePartiallyInvalidViaInvalidReferenceGrant",
"HTTPRouteRedirectHostAndStatus",
"HTTPRouteInvalidBackendRefUnknownKind",
"HTTPRoutePathMatchOrder",
"HTTPRouteSimpleSameNamespace",
"HTTPRouteMatching",
"HTTPRouteHeaderMatching",
"HTTPRouteReferenceGrant",
},
}
cSuite, err := ksuite.NewExperimentalConformanceTestSuite(ksuite.ExperimentalConformanceOptions{
Options: opts,
Implementation: conformanceV1alpha1.Implementation{
Organization: "traefik",
Project: "traefik",
URL: "https://traefik.io/",
Version: version.Version,
Contact: []string{"@traefik/maintainers"},
},
ConformanceProfiles: sets.New[ksuite.ConformanceProfileName](
ksuite.HTTPConformanceProfileName,
ksuite.TLSConformanceProfileName,
),
})
require.NoError(s.T(), err)
cSuite.Setup(s.T())
err = cSuite.Run(s.T(), tests.ConformanceTests)
require.NoError(s.T(), err)
report, err := cSuite.Report()
require.NoError(s.T(), err, "failed generating conformance report")
report.GatewayAPIVersion = "1.0.0"
rawReport, err := yaml.Marshal(report)
require.NoError(s.T(), err)
s.T().Logf("Conformance report:\n%s", string(rawReport))
require.NoError(s.T(), os.MkdirAll("./conformance-reports", 0o755))
outFile := filepath.Join("conformance-reports", fmt.Sprintf("traefik-traefik-%d.yaml", time.Now().UnixNano()))
require.NoError(s.T(), os.WriteFile(outFile, rawReport, 0o600))
s.T().Logf("Report written to: %s", outFile)
}

View file

@ -36,3 +36,14 @@ services:
labels:
traefik.http.Routers.Super.Rule: Host(`my.super.host`)
traefik.http.Services.powpow.LoadBalancer.server.Port: 2375
wrr-server:
image: traefik/whoami
labels:
traefik.http.Routers.wrr-server.Rule: Host(`my.wrr.host`)
traefik.http.Services.wrr-server.LoadBalancer.server.Weight: 4
wrr-server2:
image: traefik/whoami
labels:
traefik.http.Routers.wrr-server.Rule: Host(`my.wrr.host`)
traefik.http.Services.wrr-server.LoadBalancer.server.Weight: 1

View file

@ -659,6 +659,66 @@ func (s *SimpleSuite) TestSimpleConfigurationHostRequestTrailingPeriod() {
}
}
func (s *SimpleSuite) TestWithDefaultRuleSyntax() {
file := s.adaptFile("fixtures/with_default_rule_syntax.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix"))
require.NoError(s.T(), err)
// router1 has no error
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router1@file", 1*time.Second, try.BodyContains(`"status":"enabled"`))
require.NoError(s.T(), err)
err = try.GetRequest("http://127.0.0.1:8000/notfound", 1*time.Second, try.StatusCodeIs(http.StatusNotFound))
require.NoError(s.T(), err)
err = try.GetRequest("http://127.0.0.1:8000/foo", 1*time.Second, try.StatusCodeIs(http.StatusServiceUnavailable))
require.NoError(s.T(), err)
err = try.GetRequest("http://127.0.0.1:8000/bar", 1*time.Second, try.StatusCodeIs(http.StatusServiceUnavailable))
require.NoError(s.T(), err)
// router2 has an error because it uses the wrong rule syntax (v3 instead of v2)
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router2@file", 1*time.Second, try.BodyContains("error while parsing rule QueryRegexp(`foo`, `bar`): unsupported function: QueryRegexp"))
require.NoError(s.T(), err)
// router3 has an error because it uses the wrong rule syntax (v2 instead of v3)
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router3@file", 1*time.Second, try.BodyContains("error while adding rule PathPrefix: unexpected number of parameters; got 2, expected one of [1]"))
require.NoError(s.T(), err)
}
func (s *SimpleSuite) TestWithoutDefaultRuleSyntax() {
file := s.adaptFile("fixtures/without_default_rule_syntax.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix"))
require.NoError(s.T(), err)
// router1 has no error
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router1@file", 1*time.Second, try.BodyContains(`"status":"enabled"`))
require.NoError(s.T(), err)
err = try.GetRequest("http://127.0.0.1:8000/notfound", 1*time.Second, try.StatusCodeIs(http.StatusNotFound))
require.NoError(s.T(), err)
err = try.GetRequest("http://127.0.0.1:8000/foo", 1*time.Second, try.StatusCodeIs(http.StatusServiceUnavailable))
require.NoError(s.T(), err)
err = try.GetRequest("http://127.0.0.1:8000/bar", 1*time.Second, try.StatusCodeIs(http.StatusServiceUnavailable))
require.NoError(s.T(), err)
// router2 has an error because it uses the wrong rule syntax (v3 instead of v2)
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router2@file", 1*time.Second, try.BodyContains("error while adding rule PathPrefix: unexpected number of parameters; got 2, expected one of [1]"))
require.NoError(s.T(), err)
// router2 has an error because it uses the wrong rule syntax (v2 instead of v3)
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router3@file", 1*time.Second, try.BodyContains("error while parsing rule QueryRegexp(`foo`, `bar`): unsupported function: QueryRegexp"))
require.NoError(s.T(), err)
}
func (s *SimpleSuite) TestRouterConfigErrors() {
file := s.adaptFile("fixtures/router_errors.toml", struct{}{})
@ -749,6 +809,49 @@ func (s *SimpleSuite) TestUDPServiceConfigErrors() {
require.NoError(s.T(), err)
}
func (s *SimpleSuite) TestWRRServer() {
s.createComposeProject("base")
s.composeUp()
defer s.composeDown()
whoami1IP := s.getComposeServiceIP("whoami1")
whoami2IP := s.getComposeServiceIP("whoami2")
file := s.adaptFile("fixtures/wrr_server.toml", struct {
Server1 string
Server2 string
}{Server1: "http://" + whoami1IP, Server2: "http://" + whoami2IP})
s.traefikCmd(withConfigFile(file))
err := try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains("service1"))
require.NoError(s.T(), err)
repartition := map[string]int{}
for i := 0; i < 4; i++ {
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil)
require.NoError(s.T(), err)
response, err := http.DefaultClient.Do(req)
require.NoError(s.T(), err)
assert.Equal(s.T(), http.StatusOK, response.StatusCode)
body, err := io.ReadAll(response.Body)
require.NoError(s.T(), err)
if strings.Contains(string(body), whoami1IP) {
repartition[whoami1IP]++
}
if strings.Contains(string(body), whoami2IP) {
repartition[whoami2IP]++
}
}
assert.Equal(s.T(), 3, repartition[whoami1IP])
assert.Equal(s.T(), 1, repartition[whoami2IP])
}
func (s *SimpleSuite) TestWRR() {
s.createComposeProject("base")

View file

@ -34,6 +34,7 @@
],
"service": "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr",
"rule": "Host(`foo.com`) \u0026\u0026 Path(`/bar`)",
"ruleSyntax": "v3",
"priority": 31,
"status": "enabled",
"using": [
@ -46,6 +47,7 @@
],
"service": "default-http-app-1-my-https-gateway-websecure-1c0cf64bde37d9d0df06-wrr",
"rule": "Host(`foo.com`) \u0026\u0026 Path(`/bar`)",
"ruleSyntax": "v3",
"priority": 31,
"tls": {},
"status": "enabled",
@ -152,6 +154,7 @@
],
"service": "default-tcp-app-1-my-tcp-gateway-footcp-e3b0c44298fc1c149afb-wrr-0",
"rule": "HostSNI(`*`)",
"ruleSyntax": "v3",
"priority": -1,
"status": "enabled",
"using": [
@ -164,6 +167,7 @@
],
"service": "default-tcp-app-1-my-tls-gateway-footlsterminate-e3b0c44298fc1c149afb-wrr-0",
"rule": "HostSNI(`*`)",
"ruleSyntax": "v3",
"priority": -1,
"tls": {
"passthrough": false
@ -179,6 +183,7 @@
],
"service": "default-tls-app-1-my-tls-gateway-footlspassthrough-2279fe75c5156dc5eb26-wrr-0",
"rule": "HostSNI(`foo.bar`)",
"ruleSyntax": "v3",
"priority": 18,
"tls": {
"passthrough": true

541
pkg/cli/deprecation.go Normal file
View file

@ -0,0 +1,541 @@
package cli
import (
"errors"
"os"
"reflect"
"strings"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/traefik/paerser/cli"
"github.com/traefik/paerser/flag"
"github.com/traefik/paerser/parser"
)
type DeprecationLoader struct{}
func (d DeprecationLoader) Load(args []string, cmd *cli.Command) (bool, error) {
if logDeprecation(cmd.Configuration, args) {
return true, errors.New("incompatible deprecated static option found")
}
return false, nil
}
// logDeprecation prints deprecation hints and returns whether incompatible deprecated options need to be removed.
func logDeprecation(traefikConfiguration interface{}, args []string) bool {
// This part doesn't handle properly a flag defined like this:
// --accesslog true
// where `true` could be considered as a new argument.
// This is not really an issue with the deprecation loader since it will filter the unknown nodes later in this
// function.
for i, arg := range args {
if !strings.Contains(arg, "=") {
args[i] = arg + "=true"
}
}
labels, err := flag.Parse(args, nil)
if err != nil {
log.Error().Err(err).Msg("deprecated static options analysis failed")
return false
}
node, err := parser.DecodeToNode(labels, "traefik")
if err != nil {
log.Error().Err(err).Msg("deprecated static options analysis failed")
return false
}
if node != nil && len(node.Children) > 0 {
config := &configuration{}
filterUnknownNodes(reflect.TypeOf(config), node)
if len(node.Children) > 0 {
// Telling parser to look for the label struct tag to allow empty values.
err = parser.AddMetadata(config, node, parser.MetadataOpts{TagName: "label"})
if err != nil {
log.Error().Err(err).Msg("deprecated static options analysis failed")
return false
}
err = parser.Fill(config, node, parser.FillerOpts{})
if err != nil {
log.Error().Err(err).Msg("deprecated static options analysis failed")
return false
}
if config.deprecationNotice(log.With().Str("loader", "FLAG").Logger()) {
return true
}
// No further deprecation parsing and logging,
// as args configuration contains at least one deprecated option.
return false
}
}
// FILE
ref, err := flag.Parse(args, traefikConfiguration)
if err != nil {
log.Error().Err(err).Msg("deprecated static options analysis failed")
return false
}
configFileFlag := "traefik.configfile"
if _, ok := ref["traefik.configFile"]; ok {
configFileFlag = "traefik.configFile"
}
config := &configuration{}
_, err = loadConfigFiles(ref[configFileFlag], config)
if err == nil {
if config.deprecationNotice(log.With().Str("loader", "FILE").Logger()) {
return true
}
}
config = &configuration{}
l := EnvLoader{}
_, err = l.Load(os.Args, &cli.Command{
Configuration: config,
})
if err == nil {
if config.deprecationNotice(log.With().Str("loader", "ENV").Logger()) {
return true
}
}
return false
}
func filterUnknownNodes(fType reflect.Type, node *parser.Node) bool {
var children []*parser.Node
for _, child := range node.Children {
if hasKnownNodes(fType, child) {
children = append(children, child)
}
}
node.Children = children
return len(node.Children) > 0
}
func hasKnownNodes(rootType reflect.Type, node *parser.Node) bool {
rType := rootType
if rootType.Kind() == reflect.Pointer {
rType = rootType.Elem()
}
// unstructured type fitting anything, considering the current node as known.
if rType.Kind() == reflect.Map && rType.Elem().Kind() == reflect.Interface {
return true
}
// unstructured type fitting anything, considering the current node as known.
if rType.Kind() == reflect.Interface {
return true
}
// find matching field in struct type.
field, b := findTypedField(rType, node)
if !b {
return b
}
if len(node.Children) > 0 {
return filterUnknownNodes(field.Type, node)
}
return true
}
func findTypedField(rType reflect.Type, node *parser.Node) (reflect.StructField, bool) {
// avoid panicking.
if rType.Kind() != reflect.Struct {
return reflect.StructField{}, false
}
for i := 0; i < rType.NumField(); i++ {
cField := rType.Field(i)
// ignore unexported fields.
if cField.PkgPath == "" {
if strings.EqualFold(cField.Name, node.Name) {
node.FieldName = cField.Name
return cField, true
}
}
}
return reflect.StructField{}, false
}
// configuration holds the static configuration removed/deprecated options.
type configuration struct {
Experimental *experimental `json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" label:"allowEmpty" file:"allowEmpty"`
Pilot map[string]any `json:"pilot,omitempty" toml:"pilot,omitempty" yaml:"pilot,omitempty" label:"allowEmpty" file:"allowEmpty"`
Providers *providers `json:"providers,omitempty" toml:"providers,omitempty" yaml:"providers,omitempty" label:"allowEmpty" file:"allowEmpty"`
Tracing *tracing `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
func (c *configuration) deprecationNotice(logger zerolog.Logger) bool {
if c == nil {
return false
}
var incompatible bool
if c.Pilot != nil {
incompatible = true
logger.Error().Msg("Pilot configuration has been removed in v3, please remove all Pilot-related static configuration for Traefik to start." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#pilot")
}
incompatibleExperimental := c.Experimental.deprecationNotice(logger)
incompatibleProviders := c.Providers.deprecationNotice(logger)
incompatibleTracing := c.Tracing.deprecationNotice(logger)
return incompatible || incompatibleExperimental || incompatibleProviders || incompatibleTracing
}
type providers struct {
Docker *docker `json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty"`
Swarm *swarm `json:"swarm,omitempty" toml:"swarm,omitempty" yaml:"swarm,omitempty" label:"allowEmpty" file:"allowEmpty"`
Consul *consul `json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty"`
ConsulCatalog *consulCatalog `json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty"`
Nomad *nomad `json:"nomad,omitempty" toml:"nomad,omitempty" yaml:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty"`
Marathon map[string]any `json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" label:"allowEmpty" file:"allowEmpty"`
Rancher map[string]any `json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" label:"allowEmpty" file:"allowEmpty"`
ETCD *etcd `json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" label:"allowEmpty" file:"allowEmpty"`
Redis *redis `json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" label:"allowEmpty" file:"allowEmpty"`
HTTP *http `json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
func (p *providers) deprecationNotice(logger zerolog.Logger) bool {
if p == nil {
return false
}
var incompatible bool
if p.Marathon != nil {
incompatible = true
logger.Error().Msg("Marathon provider has been removed in v3, please remove all Marathon-related static configuration for Traefik to start." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#marathon-provider")
}
if p.Rancher != nil {
incompatible = true
logger.Error().Msg("Rancher provider has been removed in v3, please remove all Rancher-related static configuration for Traefik to start." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#rancher-v1-provider")
}
dockerIncompatible := p.Docker.deprecationNotice(logger)
consulIncompatible := p.Consul.deprecationNotice(logger)
consulCatalogIncompatible := p.ConsulCatalog.deprecationNotice(logger)
nomadIncompatible := p.Nomad.deprecationNotice(logger)
swarmIncompatible := p.Swarm.deprecationNotice(logger)
etcdIncompatible := p.ETCD.deprecationNotice(logger)
redisIncompatible := p.Redis.deprecationNotice(logger)
httpIncompatible := p.HTTP.deprecationNotice(logger)
return incompatible ||
dockerIncompatible ||
consulIncompatible ||
consulCatalogIncompatible ||
nomadIncompatible ||
swarmIncompatible ||
etcdIncompatible ||
redisIncompatible ||
httpIncompatible
}
type tls struct {
CAOptional *bool `json:"caOptional,omitempty" toml:"caOptional,omitempty" yaml:"caOptional,omitempty"`
}
type docker struct {
SwarmMode *bool `json:"swarmMode,omitempty" toml:"swarmMode,omitempty" yaml:"swarmMode,omitempty"`
TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
func (d *docker) deprecationNotice(logger zerolog.Logger) bool {
if d == nil {
return false
}
var incompatible bool
if d.SwarmMode != nil {
incompatible = true
logger.Error().Msg("Docker provider `swarmMode` option has been removed in v3, please use the Swarm Provider instead." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#docker-docker-swarm")
}
if d.TLS != nil && d.TLS.CAOptional != nil {
incompatible = true
logger.Error().Msg("Docker provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." +
"Please remove all occurrences from the static configuration for Traefik to start." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional")
}
return incompatible
}
type swarm struct {
TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
func (s *swarm) deprecationNotice(logger zerolog.Logger) bool {
if s == nil {
return false
}
var incompatible bool
if s.TLS != nil && s.TLS.CAOptional != nil {
incompatible = true
logger.Error().Msg("Swarm provider `tls.CAOptional` option does not exist, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." +
"Please remove all occurrences from the static configuration for Traefik to start.")
}
return incompatible
}
type etcd struct {
TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
func (e *etcd) deprecationNotice(logger zerolog.Logger) bool {
if e == nil {
return false
}
var incompatible bool
if e.TLS != nil && e.TLS.CAOptional != nil {
incompatible = true
logger.Error().Msg("ETCD provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." +
"Please remove all occurrences from the static configuration for Traefik to start." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_3")
}
return incompatible
}
type redis struct {
TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
func (r *redis) deprecationNotice(logger zerolog.Logger) bool {
if r == nil {
return false
}
var incompatible bool
if r.TLS != nil && r.TLS.CAOptional != nil {
incompatible = true
logger.Error().Msg("Redis provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." +
"Please remove all occurrences from the static configuration for Traefik to start." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_4")
}
return incompatible
}
type consul struct {
Namespace *string `json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"`
TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
func (c *consul) deprecationNotice(logger zerolog.Logger) bool {
if c == nil {
return false
}
var incompatible bool
if c.Namespace != nil {
incompatible = true
logger.Error().Msg("Consul provider `namespace` option has been removed, please use the `namespaces` option instead." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#consul-provider")
}
if c.TLS != nil && c.TLS.CAOptional != nil {
incompatible = true
logger.Error().Msg("Consul provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." +
"Please remove all occurrences from the static configuration for Traefik to start." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_1")
}
return incompatible
}
type consulCatalog struct {
Namespace *string `json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"`
Endpoint *endpointConfig `json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
type endpointConfig struct {
TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty"`
}
func (c *consulCatalog) deprecationNotice(logger zerolog.Logger) bool {
if c == nil {
return false
}
var incompatible bool
if c.Namespace != nil {
incompatible = true
logger.Error().Msg("ConsulCatalog provider `namespace` option has been removed, please use the `namespaces` option instead." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#consulcatalog-provider")
}
if c.Endpoint != nil && c.Endpoint.TLS != nil && c.Endpoint.TLS.CAOptional != nil {
incompatible = true
logger.Error().Msg("ConsulCatalog provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." +
"Please remove all occurrences from the static configuration for Traefik to start." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#endpointtlscaoptional")
}
return incompatible
}
type nomad struct {
Namespace *string `json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"`
Endpoint *endpointConfig `json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
func (n *nomad) deprecationNotice(logger zerolog.Logger) bool {
if n == nil {
return false
}
var incompatible bool
if n.Namespace != nil {
incompatible = true
logger.Error().Msg("Nomad provider `namespace` option has been removed, please use the `namespaces` option instead." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#nomad-provider")
}
if n.Endpoint != nil && n.Endpoint.TLS != nil && n.Endpoint.TLS.CAOptional != nil {
incompatible = true
logger.Error().Msg("Nomad provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." +
"Please remove all occurrences from the static configuration for Traefik to start." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#endpointtlscaoptional_1")
}
return incompatible
}
type http struct {
TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
func (h *http) deprecationNotice(logger zerolog.Logger) bool {
if h == nil {
return false
}
var incompatible bool
if h.TLS != nil && h.TLS.CAOptional != nil {
incompatible = true
logger.Error().Msg("HTTP provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." +
"Please remove all occurrences from the static configuration for Traefik to start." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_2")
}
return incompatible
}
type experimental struct {
HTTP3 *bool `json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty"`
}
func (e *experimental) deprecationNotice(logger zerolog.Logger) bool {
if e == nil {
return false
}
if e.HTTP3 != nil {
logger.Error().Msg("HTTP3 is not an experimental feature in v3 and the associated enablement has been removed." +
"Please remove its usage from the static configuration for Traefik to start." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#http3-experimental-configuration")
return true
}
return false
}
type tracing struct {
SpanNameLimit *int `json:"spanNameLimit,omitempty" toml:"spanNameLimit,omitempty" yaml:"spanNameLimit,omitempty"`
Jaeger map[string]any `json:"jaeger,omitempty" toml:"jaeger,omitempty" yaml:"jaeger,omitempty" label:"allowEmpty" file:"allowEmpty"`
Zipkin map[string]any `json:"zipkin,omitempty" toml:"zipkin,omitempty" yaml:"zipkin,omitempty" label:"allowEmpty" file:"allowEmpty"`
Datadog map[string]any `json:"datadog,omitempty" toml:"datadog,omitempty" yaml:"datadog,omitempty" label:"allowEmpty" file:"allowEmpty"`
Instana map[string]any `json:"instana,omitempty" toml:"instana,omitempty" yaml:"instana,omitempty" label:"allowEmpty" file:"allowEmpty"`
Haystack map[string]any `json:"haystack,omitempty" toml:"haystack,omitempty" yaml:"haystack,omitempty" label:"allowEmpty" file:"allowEmpty"`
Elastic map[string]any `json:"elastic,omitempty" toml:"elastic,omitempty" yaml:"elastic,omitempty" label:"allowEmpty" file:"allowEmpty"`
}
func (t *tracing) deprecationNotice(logger zerolog.Logger) bool {
if t == nil {
return false
}
var incompatible bool
if t.SpanNameLimit != nil {
incompatible = true
logger.Error().Msg("SpanNameLimit option for Tracing has been removed in v3, as Span names are now of a fixed length." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing")
}
if t.Jaeger != nil {
incompatible = true
logger.Error().Msg("Jaeger Tracing backend has been removed in v3, please remove all Jaeger-related Tracing static configuration for Traefik to start." +
"In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing")
}
if t.Zipkin != nil {
incompatible = true
logger.Error().Msg("Zipkin Tracing backend has been removed in v3, please remove all Zipkin-related Tracing static configuration for Traefik to start." +
"In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing")
}
if t.Datadog != nil {
incompatible = true
logger.Error().Msg("Datadog Tracing backend has been removed in v3, please remove all Datadog-related Tracing static configuration for Traefik to start." +
"In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing")
}
if t.Instana != nil {
incompatible = true
logger.Error().Msg("Instana Tracing backend has been removed in v3, please remove all Instana-related Tracing static configuration for Traefik to start." +
"In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing")
}
if t.Haystack != nil {
incompatible = true
logger.Error().Msg("Haystack Tracing backend has been removed in v3, please remove all Haystack-related Tracing static configuration for Traefik to start." +
"In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing")
}
if t.Elastic != nil {
incompatible = true
logger.Error().Msg("Elastic Tracing backend has been removed in v3, please remove all Elastic-related Tracing static configuration for Traefik to start." +
"In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." +
"For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing")
}
return incompatible
}

404
pkg/cli/deprecation_test.go Normal file
View file

@ -0,0 +1,404 @@
package cli
import (
"testing"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/paerser/cli"
"github.com/traefik/traefik/v3/cmd"
)
func ptr[T any](t T) *T {
return &t
}
func TestDeprecationNotice(t *testing.T) {
tests := []struct {
desc string
config configuration
}{
{
desc: "Docker provider swarmMode option is incompatible",
config: configuration{
Providers: &providers{
Docker: &docker{
SwarmMode: ptr(true),
},
},
},
},
{
desc: "Docker provider tls.CAOptional option is incompatible",
config: configuration{
Providers: &providers{
Docker: &docker{
TLS: &tls{
CAOptional: ptr(true),
},
},
},
},
},
{
desc: "Swarm provider tls.CAOptional option is incompatible",
config: configuration{
Providers: &providers{
Swarm: &swarm{
TLS: &tls{
CAOptional: ptr(true),
},
},
},
},
},
{
desc: "Consul provider namespace option is incompatible",
config: configuration{
Providers: &providers{
Consul: &consul{
Namespace: ptr("foobar"),
},
},
},
},
{
desc: "Consul provider tls.CAOptional option is incompatible",
config: configuration{
Providers: &providers{
Consul: &consul{
TLS: &tls{
CAOptional: ptr(true),
},
},
},
},
},
{
desc: "ConsulCatalog provider namespace option is incompatible",
config: configuration{
Providers: &providers{
ConsulCatalog: &consulCatalog{
Namespace: ptr("foobar"),
},
},
},
},
{
desc: "ConsulCatalog provider tls.CAOptional option is incompatible",
config: configuration{
Providers: &providers{
ConsulCatalog: &consulCatalog{
Endpoint: &endpointConfig{
TLS: &tls{
CAOptional: ptr(true),
},
},
},
},
},
},
{
desc: "Nomad provider namespace option is incompatible",
config: configuration{
Providers: &providers{
Nomad: &nomad{
Namespace: ptr("foobar"),
},
},
},
},
{
desc: "Nomad provider tls.CAOptional option is incompatible",
config: configuration{
Providers: &providers{
Nomad: &nomad{
Endpoint: &endpointConfig{
TLS: &tls{
CAOptional: ptr(true),
},
},
},
},
},
},
{
desc: "Marathon configuration is incompatible",
config: configuration{
Providers: &providers{
Marathon: map[string]any{
"foo": "bar",
},
},
},
},
{
desc: "Rancher configuration is incompatible",
config: configuration{
Providers: &providers{
Rancher: map[string]any{
"foo": "bar",
},
},
},
},
{
desc: "ETCD provider tls.CAOptional option is incompatible",
config: configuration{
Providers: &providers{
ETCD: &etcd{
TLS: &tls{
CAOptional: ptr(true),
},
},
},
},
},
{
desc: "Redis provider tls.CAOptional option is incompatible",
config: configuration{
Providers: &providers{
Redis: &redis{
TLS: &tls{
CAOptional: ptr(true),
},
},
},
},
},
{
desc: "HTTP provider tls.CAOptional option is incompatible",
config: configuration{
Providers: &providers{
HTTP: &http{
TLS: &tls{
CAOptional: ptr(true),
},
},
},
},
},
{
desc: "Pilot configuration is incompatible",
config: configuration{
Pilot: map[string]any{
"foo": "bar",
},
},
},
{
desc: "Experimental HTTP3 enablement configuration is incompatible",
config: configuration{
Experimental: &experimental{
HTTP3: ptr(true),
},
},
},
{
desc: "Tracing SpanNameLimit option is incompatible",
config: configuration{
Tracing: &tracing{
SpanNameLimit: ptr(42),
},
},
},
{
desc: "Tracing Jaeger configuration is incompatible",
config: configuration{
Tracing: &tracing{
Jaeger: map[string]any{
"foo": "bar",
},
},
},
},
{
desc: "Tracing Zipkin configuration is incompatible",
config: configuration{
Tracing: &tracing{
Zipkin: map[string]any{
"foo": "bar",
},
},
},
},
{
desc: "Tracing Datadog configuration is incompatible",
config: configuration{
Tracing: &tracing{
Datadog: map[string]any{
"foo": "bar",
},
},
},
},
{
desc: "Tracing Instana configuration is incompatible",
config: configuration{
Tracing: &tracing{
Instana: map[string]any{
"foo": "bar",
},
},
},
},
{
desc: "Tracing Haystack configuration is incompatible",
config: configuration{
Tracing: &tracing{
Haystack: map[string]any{
"foo": "bar",
},
},
},
},
{
desc: "Tracing Elastic configuration is incompatible",
config: configuration{
Tracing: &tracing{
Elastic: map[string]any{
"foo": "bar",
},
},
},
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var gotLog bool
var gotLevel zerolog.Level
testHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) {
gotLog = true
gotLevel = level
})
logger := log.With().Logger().Hook(testHook)
assert.True(t, test.config.deprecationNotice(logger))
assert.True(t, gotLog)
assert.Equal(t, zerolog.ErrorLevel, gotLevel)
})
}
}
func TestLoad(t *testing.T) {
testCases := []struct {
desc string
args []string
env map[string]string
wantDeprecated bool
}{
{
desc: "Empty",
args: []string{},
wantDeprecated: false,
},
{
desc: "[FLAG] providers.marathon is deprecated",
args: []string{
"--access-log",
"--log.level=DEBUG",
"--entrypoints.test.http.tls",
"--providers.nomad.endpoint.tls.insecureskipverify=true",
"--providers.marathon",
},
wantDeprecated: true,
},
{
desc: "[FLAG] multiple deprecated",
args: []string{
"--access-log",
"--log.level=DEBUG",
"--entrypoints.test.http.tls",
"--providers.marathon",
"--pilot.token=XXX",
},
wantDeprecated: true,
},
{
desc: "[FLAG] no deprecated",
args: []string{
"--access-log",
"--log.level=DEBUG",
"--entrypoints.test.http.tls",
"--providers.docker",
},
wantDeprecated: false,
},
{
desc: "[ENV] providers.marathon is deprecated",
env: map[string]string{
"TRAEFIK_ACCESS_LOG": "",
"TRAEFIK_LOG_LEVEL": "DEBUG",
"TRAEFIK_ENTRYPOINT_TEST_HTTP_TLS": "true",
"TRAEFIK_PROVIDERS_MARATHON": "true",
},
wantDeprecated: true,
},
{
desc: "[ENV] multiple deprecated",
env: map[string]string{
"TRAEFIK_ACCESS_LOG": "true",
"TRAEFIK_LOG_LEVEL": "DEBUG",
"TRAEFIK_ENTRYPOINT_TEST_HTTP_TLS": "true",
"TRAEFIK_PROVIDERS_MARATHON": "true",
"TRAEFIK_PILOT_TOKEN": "xxx",
},
wantDeprecated: true,
},
{
desc: "[ENV] no deprecated",
env: map[string]string{
"TRAEFIK_ACCESS_LOG": "true",
"TRAEFIK_LOG_LEVEL": "DEBUG",
"TRAEFIK_ENTRYPOINT_TEST_HTTP_TLS": "true",
},
wantDeprecated: false,
},
{
desc: "[FILE] providers.marathon is deprecated",
args: []string{
"--configfile=./fixtures/traefik_deprecated.toml",
},
wantDeprecated: true,
},
{
desc: "[FILE] multiple deprecated",
args: []string{
"--configfile=./fixtures/traefik_multiple_deprecated.toml",
},
wantDeprecated: true,
},
{
desc: "[FILE] no deprecated",
args: []string{
"--configfile=./fixtures/traefik_no_deprecated.toml",
},
wantDeprecated: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
tconfig := cmd.NewTraefikConfiguration()
c := &cli.Command{Configuration: tconfig}
l := DeprecationLoader{}
for name, val := range test.env {
t.Setenv(name, val)
}
deprecated, err := l.Load(test.args, c)
assert.Equal(t, test.wantDeprecated, deprecated)
if !test.wantDeprecated {
require.NoError(t, err)
}
})
}
}

View file

@ -0,0 +1,5 @@
[accesslog]
[entrypoints.test.http.tls]
[providers.marathon]

View file

@ -0,0 +1,8 @@
[accesslog]
[entrypoints.test.http.tls]
[providers.marathon]
[pilot]
token="xxx"

View file

@ -0,0 +1,3 @@
[accesslog]
[entrypoints.test.http.tls]

View file

@ -37,8 +37,9 @@ type HTTPConfiguration struct {
// Model is a set of default router's values.
type Model struct {
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
DefaultRuleSyntax string `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
}
// +k8s:deepcopy-gen=true
@ -59,6 +60,7 @@ type Router struct {
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`
Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"`
RuleSyntax string `json:"ruleSyntax,omitempty" toml:"ruleSyntax,omitempty" yaml:"ruleSyntax,omitempty" export:"true"`
Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"`
TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
DefaultRule bool `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
@ -225,6 +227,7 @@ func (r *ResponseForwarding) SetDefaults() {
// Server holds the server configuration.
type Server struct {
URL string `json:"url,omitempty" toml:"url,omitempty" yaml:"url,omitempty" label:"-"`
Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty" label:"weight"`
Scheme string `json:"-" toml:"-" yaml:"-" file:"-"`
Port string `json:"-" toml:"-" yaml:"-" file:"-"`
}

View file

@ -16,11 +16,19 @@ type TCPConfiguration struct {
Routers map[string]*TCPRouter `json:"routers,omitempty" toml:"routers,omitempty" yaml:"routers,omitempty" export:"true"`
Services map[string]*TCPService `json:"services,omitempty" toml:"services,omitempty" yaml:"services,omitempty" export:"true"`
Middlewares map[string]*TCPMiddleware `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
Models map[string]*TCPModel `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
ServersTransports map[string]*TCPServersTransport `json:"serversTransports,omitempty" toml:"serversTransports,omitempty" yaml:"serversTransports,omitempty" label:"-" export:"true"`
}
// +k8s:deepcopy-gen=true
// TCPModel is a set of default router's values.
type TCPModel struct {
DefaultRuleSyntax string `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
}
// +k8s:deepcopy-gen=true
// TCPService holds a tcp service configuration (can only be of one type at the same time).
type TCPService struct {
LoadBalancer *TCPServersLoadBalancer `json:"loadBalancer,omitempty" toml:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty" export:"true"`
@ -56,6 +64,7 @@ type TCPRouter struct {
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`
Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"`
RuleSyntax string `json:"ruleSyntax,omitempty" toml:"ruleSyntax,omitempty" yaml:"ruleSyntax,omitempty" export:"true"`
Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"`
TLS *RouterTCPTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
}

View file

@ -1128,6 +1128,11 @@ func (in *RouterTLSConfig) DeepCopy() *RouterTLSConfig {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Server) DeepCopyInto(out *Server) {
*out = *in
if in.Weight != nil {
in, out := &in.Weight, &out.Weight
*out = new(int)
**out = **in
}
return
}
@ -1180,7 +1185,9 @@ func (in *ServersLoadBalancer) DeepCopyInto(out *ServersLoadBalancer) {
if in.Servers != nil {
in, out := &in.Servers, &out.Servers
*out = make([]Server, len(*in))
copy(*out, *in)
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.HealthCheck != nil {
in, out := &in.HealthCheck, &out.HealthCheck
@ -1437,6 +1444,21 @@ func (in *TCPConfiguration) DeepCopyInto(out *TCPConfiguration) {
(*out)[key] = outVal
}
}
if in.Models != nil {
in, out := &in.Models, &out.Models
*out = make(map[string]*TCPModel, len(*in))
for key, val := range *in {
var outVal *TCPModel
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = new(TCPModel)
**out = **in
}
(*out)[key] = outVal
}
}
if in.ServersTransports != nil {
in, out := &in.ServersTransports, &out.ServersTransports
*out = make(map[string]*TCPServersTransport, len(*in))
@ -1554,6 +1576,22 @@ func (in *TCPMiddleware) DeepCopy() *TCPMiddleware {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TCPModel) DeepCopyInto(out *TCPModel) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPModel.
func (in *TCPModel) DeepCopy() *TCPModel {
if in == nil {
return nil
}
out := new(TCPModel)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TCPRouter) DeepCopyInto(out *TCPRouter) {
*out = *in

View file

@ -71,11 +71,23 @@ type Configuration struct {
CertificatesResolvers map[string]CertificateResolver `description:"Certificates resolvers configuration." json:"certificatesResolvers,omitempty" toml:"certificatesResolvers,omitempty" yaml:"certificatesResolvers,omitempty" export:"true"`
Experimental *Experimental `description:"experimental features." json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" export:"true"`
Experimental *Experimental `description:"Experimental features." json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" export:"true"`
Core *Core `description:"Core controls." json:"core,omitempty" toml:"core,omitempty" yaml:"core,omitempty" export:"true"`
Spiffe *SpiffeClientConfig `description:"SPIFFE integration configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" export:"true"`
}
// Core configures Traefik core behavior.
type Core struct {
DefaultRuleSyntax string `description:"Defines the rule parser default syntax (v2 or v3)" json:"defaultRuleSyntax,omitempty" toml:"defaultRuleSyntax,omitempty" yaml:"defaultRuleSyntax,omitempty"`
}
// SetDefaults sets the default values.
func (c *Core) SetDefaults() {
c.DefaultRuleSyntax = "v3"
}
// SpiffeClientConfig defines the SPIFFE client configuration.
type SpiffeClientConfig struct {
WorkloadAPIAddr string `description:"Defines the workload API address." json:"workloadAPIAddr,omitempty" toml:"workloadAPIAddr,omitempty" yaml:"workloadAPIAddr,omitempty"`
@ -317,6 +329,17 @@ func (c *Configuration) ValidateConfiguration() error {
acmeEmail = resolver.ACME.Email
}
if c.Core != nil {
switch c.Core.DefaultRuleSyntax {
case "v3": // NOOP
case "v2":
// TODO: point to migration guide.
log.Warn().Msgf("v2 rules syntax is now deprecated, please use v3 instead...")
default:
return fmt.Errorf("unsupported default rule syntax configuration: %q", c.Core.DefaultRuleSyntax)
}
}
if c.Tracing != nil && c.Tracing.OTLP != nil {
if c.Tracing.OTLP.HTTP == nil && c.Tracing.OTLP.GRPC == nil {
return errors.New("tracing OTLP: at least one of HTTP and gRPC options should be defined")

View file

@ -13,12 +13,13 @@ import (
)
func TestDatadog(t *testing.T) {
t.Cleanup(StopDatadog)
udp.SetAddr(":18125")
// This is needed to make sure that UDP Listener listens for data a bit longer, otherwise it will quit after a millisecond
udp.Timeout = 5 * time.Second
datadogRegistry := RegisterDatadog(context.Background(), &types.Datadog{Address: ":18125", PushInterval: ptypes.Duration(time.Second), AddEntryPointsLabels: true, AddRoutersLabels: true, AddServicesLabels: true})
defer StopDatadog()
if !datadogRegistry.IsEpEnabled() || !datadogRegistry.IsRouterEnabled() || !datadogRegistry.IsSvcEnabled() {
t.Errorf("DatadogRegistry should return true for IsEnabled(), IsRouterEnabled() and IsSvcEnabled()")
@ -27,9 +28,7 @@ func TestDatadog(t *testing.T) {
}
func TestDatadogWithPrefix(t *testing.T) {
t.Cleanup(func() {
StopDatadog()
})
t.Cleanup(StopDatadog)
udp.SetAddr(":18125")
// This is needed to make sure that UDP Listener listens for data a bit longer, otherwise it will quit after a millisecond

View file

@ -6,7 +6,6 @@ import (
"io"
"net/http"
"net/http/httptest"
"regexp"
"strconv"
"testing"
"time"
@ -26,7 +25,6 @@ func TestInfluxDB2(t *testing.T) {
c <- &bodyStr
_, _ = fmt.Fprintln(w, "ok")
}))
defer ts.Close()
influxDB2Registry := RegisterInfluxDB2(context.Background(),
&types.InfluxDB2{
@ -39,7 +37,11 @@ func TestInfluxDB2(t *testing.T) {
AddRoutersLabels: true,
AddServicesLabels: true,
})
defer StopInfluxDB2()
t.Cleanup(func() {
StopInfluxDB2()
ts.Close()
})
if !influxDB2Registry.IsEpEnabled() || !influxDB2Registry.IsRouterEnabled() || !influxDB2Registry.IsSvcEnabled() {
t.Fatalf("InfluxDB2Registry should return true for IsEnabled(), IsRouterEnabled() and IsSvcEnabled()")
@ -137,14 +139,3 @@ func TestInfluxDB2(t *testing.T) {
assertMessage(t, *msgServiceRetries, expectedServiceRetries)
}
func assertMessage(t *testing.T, msg string, patterns []string) {
t.Helper()
for _, pattern := range patterns {
re := regexp.MustCompile(pattern)
match := re.FindStringSubmatch(msg)
if len(match) != 2 {
t.Errorf("Got %q %v, want %q", msg, match, pattern)
}
}
}

View file

@ -4,10 +4,12 @@ import (
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strconv"
"testing"
"time"
@ -287,10 +289,8 @@ func TestOpenTelemetry_GaugeCollectorSet(t *testing.T) {
}
func TestOpenTelemetry(t *testing.T) {
t.Parallel()
c := make(chan *string, 5)
c := make(chan *string)
defer close(c)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gzr, err := gzip.NewReader(r.Body)
require.NoError(t, err)
@ -310,7 +310,11 @@ func TestOpenTelemetry(t *testing.T) {
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
t.Cleanup(func() {
StopOpenTelemetry()
ts.Close()
})
sURL, err := url.Parse(ts.URL)
require.NoError(t, err)
@ -333,57 +337,53 @@ func TestOpenTelemetry(t *testing.T) {
`({"key":"service.name","value":{"stringValue":"traefik"}})`,
`({"key":"service.version","value":{"stringValue":"` + version.Version + `"}})`,
}
msgMisc := <-c
assertMessage(t, *msgMisc, expected)
tryAssertMessage(t, c, expected)
// TODO: the len of startUnixNano is no supposed to be 20, it should be 19
expected = append(expected,
expectedConfig := []string{
`({"name":"traefik_config_reloads_total","description":"Config reloads","unit":"1","sum":{"dataPoints":\[{"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_config_last_reload_success","description":"Last config reload success","unit":"ms","gauge":{"dataPoints":\[{"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
`({"name":"traefik_open_connections","description":"How many open connections exist, by entryPoint and protocol","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test"}},{"key":"protocol","value":{"stringValue":"TCP"}}\],"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
)
}
registry.ConfigReloadsCounter().Add(1)
registry.LastConfigReloadSuccessGauge().Set(1)
registry.OpenConnectionsGauge().With("entrypoint", "test", "protocol", "TCP").Set(1)
msgServer := <-c
assertMessage(t, *msgServer, expected)
tryAssertMessage(t, c, expectedConfig)
expected = append(expected,
expectedTLSCerts := []string{
`({"name":"traefik_tls_certs_not_after","description":"Certificate expiration timestamp","unit":"ms","gauge":{"dataPoints":\[{"attributes":\[{"key":"key","value":{"stringValue":"value"}}\],"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
)
}
registry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1)
msgTLS := <-c
assertMessage(t, *msgTLS, expected)
tryAssertMessage(t, c, expectedTLSCerts)
expected = append(expected,
expectedEntryPoints := []string{
`({"name":"traefik_entrypoint_requests_total","description":"How many HTTP requests processed on an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_entrypoint_requests_tls_total","description":"How many HTTP requests with TLS processed on an entrypoint, partitioned by TLS Version and TLS cipher Used.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test2"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_entrypoint_request_duration_seconds","description":"How long it took to process the request on an entrypoint, partitioned by status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test3"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`,
`({"name":"traefik_entrypoint_requests_bytes_total","description":"The total size of requests in bytes handled by an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_entrypoint_responses_bytes_total","description":"The total size of responses in bytes handled by an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
)
}
registry.EntryPointReqsCounter().With(nil, "entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
registry.EntryPointReqsTLSCounter().With("entrypoint", "test2", "tls_version", "foo", "tls_cipher", "bar").Add(1)
registry.EntryPointReqDurationHistogram().With("entrypoint", "test3").Observe(10000)
registry.EntryPointReqsBytesCounter().With("entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
registry.EntryPointRespsBytesCounter().With("entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
msgEntrypoint := <-c
assertMessage(t, *msgEntrypoint, expected)
tryAssertMessage(t, c, expectedEntryPoints)
expected = append(expected,
expectedRouters := []string{
`({"name":"traefik_router_requests_total","description":"How many HTTP requests are processed on a router, partitioned by service, status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1},{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_router_requests_tls_total","description":"How many HTTP requests with TLS are processed on a router, partitioned by service, TLS Version, and TLS cipher Used.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"router","value":{"stringValue":"demo"}},{"key":"service","value":{"stringValue":"test"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_router_request_duration_seconds","description":"How long it took to process the request on a router, partitioned by service, status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"router","value":{"stringValue":"demo"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`,
`({"name":"traefik_router_requests_bytes_total","description":"The total size of requests in bytes handled by a router, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_router_responses_bytes_total","description":"The total size of responses in bytes handled by a router, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
)
}
registry.RouterReqsCounter().With(nil, "router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
registry.RouterReqsCounter().With(nil, "router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
@ -391,18 +391,17 @@ func TestOpenTelemetry(t *testing.T) {
registry.RouterReqDurationHistogram().With("router", "demo", "service", "test", "code", strconv.Itoa(http.StatusOK)).Observe(10000)
registry.RouterReqsBytesCounter().With("router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
registry.RouterRespsBytesCounter().With("router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
msgRouter := <-c
assertMessage(t, *msgRouter, expected)
tryAssertMessage(t, c, expectedRouters)
expected = append(expected,
expectedServices := []string{
`({"name":"traefik_service_requests_total","description":"How many HTTP requests processed on a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1},{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_service_requests_tls_total","description":"How many HTTP requests with TLS processed on a service, partitioned by TLS version and TLS cipher.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"service","value":{"stringValue":"test"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_service_request_duration_seconds","description":"How long it took to process the request on a service, partitioned by status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`,
`({"name":"traefik_service_server_up","description":"service server is up, described by gauge value of 0 or 1.","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"service","value":{"stringValue":"test"}},{"key":"url","value":{"stringValue":"http://127.0.0.1"}}\],"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`,
`({"name":"traefik_service_requests_bytes_total","description":"The total size of requests in bytes received by a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
`({"name":"traefik_service_responses_bytes_total","description":"The total size of responses in bytes returned by a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`,
)
}
registry.ServiceReqsCounter().With(nil, "service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
registry.ServiceReqsCounter().With(nil, "service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
@ -411,21 +410,19 @@ func TestOpenTelemetry(t *testing.T) {
registry.ServiceServerUpGauge().With("service", "test", "url", "http://127.0.0.1").Set(1)
registry.ServiceReqsBytesCounter().With("service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
registry.ServiceRespsBytesCounter().With("service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
msgService := <-c
assertMessage(t, *msgService, expected)
tryAssertMessage(t, c, expectedServices)
expected = append(expected,
expectedServicesRetries := []string{
`({"attributes":\[{"key":"service","value":{"stringValue":"foobar"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1})`,
`({"attributes":\[{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":2})`,
)
}
registry.ServiceRetriesCounter().With("service", "test").Add(1)
registry.ServiceRetriesCounter().With("service", "test").Add(1)
registry.ServiceRetriesCounter().With("service", "foobar").Add(1)
msgServiceRetries := <-c
assertMessage(t, *msgServiceRetries, expected)
tryAssertMessage(t, c, expectedServicesRetries)
// We cannot rely on the previous expected pattern,
// because this pattern was for matching only one dataPoint in the histogram,
@ -437,14 +434,46 @@ func TestOpenTelemetry(t *testing.T) {
registry.EntryPointReqDurationHistogram().With("entrypoint", "myEntrypoint").Observe(10000)
registry.EntryPointReqDurationHistogram().With("entrypoint", "myEntrypoint").Observe(20000)
msgEntryPointReqDurationHistogram := <-c
assertMessage(t, *msgEntryPointReqDurationHistogram, expectedEntryPointReqDuration)
// We need to unlock the HTTP Server for the last export call when stopping
// OpenTelemetry.
go func() {
<-c
}()
StopOpenTelemetry()
tryAssertMessage(t, c, expectedEntryPointReqDuration)
}
func assertMessage(t *testing.T, msg string, expected []string) {
t.Helper()
errs := verifyMessage(msg, expected)
for _, err := range errs {
t.Error(err)
}
}
func tryAssertMessage(t *testing.T, c chan *string, expected []string) {
t.Helper()
var errs []error
timeout := time.After(1 * time.Second)
for {
select {
case <-timeout:
for _, err := range errs {
t.Error(err)
}
case msg := <-c:
errs = verifyMessage(*msg, expected)
if len(errs) == 0 {
return
}
}
}
}
func verifyMessage(msg string, expected []string) []error {
var errs []error
for _, pattern := range expected {
re := regexp.MustCompile(pattern)
match := re.FindStringSubmatch(msg)
if len(match) != 2 {
errs = append(errs, fmt.Errorf("Got %q %v, want %q", msg, match, pattern))
}
}
return errs
}

View file

@ -73,7 +73,7 @@ func TestClientIPMatcher(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
err = muxer.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, "", 0, handler)
if test.expectedError {
require.Error(t, err)
return
@ -147,7 +147,7 @@ func TestMethodMatcher(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
err = muxer.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, "", 0, handler)
if test.expectedError {
require.Error(t, err)
return
@ -265,7 +265,7 @@ func TestHostMatcher(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
err = muxer.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, "", 0, handler)
if test.expectedError {
require.Error(t, err)
return
@ -365,7 +365,7 @@ func TestHostRegexpMatcher(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
err = muxer.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, "", 0, handler)
if test.expectedError {
require.Error(t, err)
return
@ -439,7 +439,7 @@ func TestPathMatcher(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
err = muxer.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, "", 0, handler)
if test.expectedError {
require.Error(t, err)
return
@ -532,7 +532,7 @@ func TestPathRegexpMatcher(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
err = muxer.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, "", 0, handler)
if test.expectedError {
require.Error(t, err)
return
@ -604,7 +604,7 @@ func TestPathPrefixMatcher(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
err = muxer.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, "", 0, handler)
if test.expectedError {
require.Error(t, err)
return
@ -692,7 +692,7 @@ func TestHeaderMatcher(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
err = muxer.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, "", 0, handler)
if test.expectedError {
require.Error(t, err)
return
@ -800,7 +800,7 @@ func TestHeaderRegexpMatcher(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
err = muxer.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, "", 0, handler)
if test.expectedError {
require.Error(t, err)
return
@ -889,7 +889,7 @@ func TestQueryMatcher(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
err = muxer.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, "", 0, handler)
if test.expectedError {
require.Error(t, err)
return
@ -1003,7 +1003,7 @@ func TestQueryRegexpMatcher(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
err = muxer.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, "", 0, handler)
if test.expectedError {
require.Error(t, err)
return

View file

@ -0,0 +1,226 @@
package http
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
)
var httpFuncsV2 = map[string]func(*matchersTree, ...string) error{
"Host": hostV2,
"HostHeader": hostV2,
"HostRegexp": hostRegexpV2,
"ClientIP": clientIPV2,
"Path": pathV2,
"PathPrefix": pathPrefixV2,
"Method": methodsV2,
"Headers": headersV2,
"HeadersRegexp": headersRegexpV2,
"Query": queryV2,
}
func pathV2(tree *matchersTree, paths ...string) error {
for _, path := range paths {
if !strings.HasPrefix(path, "/") {
return fmt.Errorf("path %q does not start with a '/'", path)
}
}
tree.matcher = func(req *http.Request) bool {
for _, path := range paths {
if req.URL.Path == path {
return true
}
}
return false
}
return nil
}
func pathPrefixV2(tree *matchersTree, paths ...string) error {
for _, path := range paths {
if !strings.HasPrefix(path, "/") {
return fmt.Errorf("path %q does not start with a '/'", path)
}
}
tree.matcher = func(req *http.Request) bool {
for _, path := range paths {
if strings.HasPrefix(req.URL.Path, path) {
return true
}
}
return false
}
return nil
}
func hostV2(tree *matchersTree, hosts ...string) error {
for i, host := range hosts {
if !IsASCII(host) {
return fmt.Errorf("invalid value %q for \"Host\" matcher, non-ASCII characters are not allowed", host)
}
hosts[i] = strings.ToLower(host)
}
tree.matcher = func(req *http.Request) bool {
reqHost := requestdecorator.GetCanonizedHost(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) {
log.Ctx(req.Context()).Warn().Msgf("Could not retrieve CanonizedHost, rejecting %s", req.Host)
}
return false
}
flatH := requestdecorator.GetCNAMEFlatten(req.Context())
if len(flatH) > 0 {
for _, host := range hosts {
if strings.EqualFold(reqHost, host) || strings.EqualFold(flatH, host) {
return true
}
log.Ctx(req.Context()).Debug().Msgf("CNAMEFlattening: request %s which resolved to %s, is not matched to route %s", reqHost, flatH, host)
}
return false
}
for _, host := range hosts {
if reqHost == host {
return true
}
// Check for match on trailing period on host
if last := len(host) - 1; last >= 0 && host[last] == '.' {
h := host[:last]
if reqHost == h {
return true
}
}
// Check for match on trailing period on request
if last := len(reqHost) - 1; last >= 0 && reqHost[last] == '.' {
h := reqHost[:last]
if h == host {
return true
}
}
}
return false
}
return nil
}
func clientIPV2(tree *matchersTree, clientIPs ...string) error {
checker, err := ip.NewChecker(clientIPs)
if err != nil {
return fmt.Errorf("could not initialize IP Checker for \"ClientIP\" matcher: %w", err)
}
strategy := ip.RemoteAddrStrategy{}
tree.matcher = func(req *http.Request) bool {
ok, err := checker.Contains(strategy.GetIP(req))
if err != nil {
log.Ctx(req.Context()).Warn().Err(err).Msg("\"ClientIP\" matcher: could not match remote address")
return false
}
return ok
}
return nil
}
func methodsV2(tree *matchersTree, methods ...string) error {
route := mux.NewRouter().NewRoute()
route.Methods(methods...)
if err := route.GetError(); err != nil {
return err
}
tree.matcher = func(req *http.Request) bool {
return route.Match(req, &mux.RouteMatch{})
}
return nil
}
func headersV2(tree *matchersTree, headers ...string) error {
route := mux.NewRouter().NewRoute()
route.Headers(headers...)
if err := route.GetError(); err != nil {
return err
}
tree.matcher = func(req *http.Request) bool {
return route.Match(req, &mux.RouteMatch{})
}
return nil
}
func queryV2(tree *matchersTree, query ...string) error {
var queries []string
for _, elem := range query {
queries = append(queries, strings.SplitN(elem, "=", 2)...)
}
route := mux.NewRouter().NewRoute()
route.Queries(queries...)
if err := route.GetError(); err != nil {
return err
}
tree.matcher = func(req *http.Request) bool {
return route.Match(req, &mux.RouteMatch{})
}
return nil
}
func hostRegexpV2(tree *matchersTree, hosts ...string) error {
router := mux.NewRouter()
for _, host := range hosts {
if !IsASCII(host) {
return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host)
}
tmpRt := router.Host(host)
if tmpRt.GetError() != nil {
return tmpRt.GetError()
}
}
tree.matcher = func(req *http.Request) bool {
return router.Match(req, &mux.RouteMatch{})
}
return nil
}
func headersRegexpV2(tree *matchersTree, headers ...string) error {
route := mux.NewRouter().NewRoute()
route.HeadersRegexp(headers...)
if err := route.GetError(); err != nil {
return err
}
tree.matcher = func(req *http.Request) bool {
return route.Match(req, &mux.RouteMatch{})
}
return nil
}

File diff suppressed because it is too large Load diff

View file

@ -12,8 +12,9 @@ import (
// Muxer handles routing with rules.
type Muxer struct {
routes routes
parser predicate.Parser
routes routes
parser predicate.Parser
parserV2 predicate.Parser
}
// NewMuxer returns a new muxer instance.
@ -28,8 +29,19 @@ func NewMuxer() (*Muxer, error) {
return nil, fmt.Errorf("error while creating parser: %w", err)
}
var matchersV2 []string
for matcher := range httpFuncsV2 {
matchersV2 = append(matchersV2, matcher)
}
parserV2, err := rules.NewParser(matchersV2)
if err != nil {
return nil, fmt.Errorf("error while creating v2 parser: %w", err)
}
return &Muxer{
parser: parser,
parser: parser,
parserV2: parserV2,
}, nil
}
@ -53,10 +65,26 @@ func GetRulePriority(rule string) int {
}
// AddRoute add a new route to the router.
func (m *Muxer) AddRoute(rule string, priority int, handler http.Handler) error {
parse, err := m.parser.Parse(rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler http.Handler) error {
var parse interface{}
var err error
var matcherFuncs map[string]func(*matchersTree, ...string) error
switch syntax {
case "v2":
parse, err = m.parserV2.Parse(rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
matcherFuncs = httpFuncsV2
default:
parse, err = m.parser.Parse(rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
matcherFuncs = httpFuncs
}
buildTree, ok := parse.(rules.TreeBuilder)
@ -65,7 +93,7 @@ func (m *Muxer) AddRoute(rule string, priority int, handler http.Handler) error
}
var matchers matchersTree
err = matchers.addRule(buildTree())
err = matchers.addRule(buildTree(), matcherFuncs)
if err != nil {
return fmt.Errorf("error while adding rule %s: %w", rule, err)
}
@ -87,6 +115,9 @@ func ParseDomains(rule string) ([]string, error) {
for matcher := range httpFuncs {
matchers = append(matchers, matcher)
}
for matcher := range httpFuncsV2 {
matchers = append(matchers, matcher)
}
parser, err := rules.NewParser(matchers)
if err != nil {
@ -166,25 +197,27 @@ func (m *matchersTree) match(req *http.Request) bool {
}
}
func (m *matchersTree) addRule(rule *rules.Tree) error {
type matcherFuncs map[string]func(*matchersTree, ...string) error
func (m *matchersTree) addRule(rule *rules.Tree, funcs matcherFuncs) error {
switch rule.Matcher {
case "and", "or":
m.operator = rule.Matcher
m.left = &matchersTree{}
err := m.left.addRule(rule.RuleLeft)
err := m.left.addRule(rule.RuleLeft, funcs)
if err != nil {
return fmt.Errorf("error while adding rule %s: %w", rule.Matcher, err)
}
m.right = &matchersTree{}
return m.right.addRule(rule.RuleRight)
return m.right.addRule(rule.RuleRight, funcs)
default:
err := rules.CheckRule(rule)
if err != nil {
return fmt.Errorf("error while checking rule %s: %w", rule.Matcher, err)
}
err = httpFuncs[rule.Matcher](m, rule.Value...)
err = funcs[rule.Matcher](m, rule.Value...)
if err != nil {
return fmt.Errorf("error while adding rule %s: %w", rule.Matcher, err)
}

View file

@ -231,7 +231,7 @@ func TestMuxer(t *testing.T) {
require.NoError(t, err)
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
@ -394,7 +394,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, handler)
require.NoError(t, err, route.rule)
}
@ -519,7 +519,7 @@ func TestEmptyHost(t *testing.T) {
muxer, err := NewMuxer()
require.NoError(t, err)
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

View file

@ -38,7 +38,7 @@ func Test_HostSNICatchAll(t *testing.T) {
muxer, err := NewMuxer()
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{
@ -144,7 +144,7 @@ func Test_HostSNI(t *testing.T) {
muxer, err := NewMuxer()
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,7 +227,7 @@ func Test_HostSNIRegexp(t *testing.T) {
muxer, err := NewMuxer()
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
@ -299,7 +299,7 @@ func Test_ClientIP(t *testing.T) {
muxer, err := NewMuxer()
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
@ -363,7 +363,7 @@ func Test_ALPN(t *testing.T) {
muxer, err := NewMuxer()
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

240
pkg/muxer/tcp/matcher_v2.go Normal file
View file

@ -0,0 +1,240 @@
package tcp
import (
"bytes"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/ip"
)
var tcpFuncsV2 = map[string]func(*matchersTree, ...string) error{
"ALPN": alpnV2,
"ClientIP": clientIPV2,
"HostSNI": hostSNIV2,
"HostSNIRegexp": hostSNIRegexpV2,
}
func clientIPV2(tree *matchersTree, clientIPs ...string) error {
checker, err := ip.NewChecker(clientIPs)
if err != nil {
return fmt.Errorf("could not initialize IP Checker for \"ClientIP\" matcher: %w", err)
}
tree.matcher = func(meta ConnData) bool {
if meta.remoteIP == "" {
return false
}
ok, err := checker.Contains(meta.remoteIP)
if err != nil {
log.Warn().Err(err).Msg("ClientIP matcher: could not match remote address")
return false
}
return ok
}
return nil
}
// alpnV2 checks if any of the connection ALPN protocols matches one of the matcher protocols.
func alpnV2(tree *matchersTree, protos ...string) error {
if len(protos) == 0 {
return errors.New("empty value for \"ALPN\" matcher is not allowed")
}
for _, proto := range protos {
if proto == tlsalpn01.ACMETLS1Protocol {
return fmt.Errorf("invalid protocol value for \"ALPN\" matcher, %q is not allowed", proto)
}
}
tree.matcher = func(meta ConnData) bool {
for _, proto := range meta.alpnProtos {
for _, filter := range protos {
if proto == filter {
return true
}
}
}
return false
}
return nil
}
// hostSNIV2 checks if the SNI Host of the connection match the matcher host.
func hostSNIV2(tree *matchersTree, hosts ...string) error {
if len(hosts) == 0 {
return errors.New("empty value for \"HostSNI\" matcher is not allowed")
}
for i, host := range hosts {
// Special case to allow global wildcard
if host == "*" {
continue
}
if !hostOrIP.MatchString(host) {
return fmt.Errorf("invalid value for \"HostSNI\" matcher, %q is not a valid hostname or IP", host)
}
hosts[i] = strings.ToLower(host)
}
tree.matcher = func(meta ConnData) bool {
// Since a HostSNI(`*`) rule has been provided as catchAll for non-TLS TCP,
// it allows matching with an empty serverName.
// Which is why we make sure to take that case into account before
// checking meta.serverName.
if hosts[0] == "*" {
return true
}
if meta.serverName == "" {
return false
}
for _, host := range hosts {
if host == "*" {
return true
}
if host == meta.serverName {
return true
}
// trim trailing period in case of FQDN
host = strings.TrimSuffix(host, ".")
if host == meta.serverName {
return true
}
}
return false
}
return nil
}
// hostSNIRegexpV2 checks if the SNI Host of the connection matches the matcher host regexp.
func hostSNIRegexpV2(tree *matchersTree, templates ...string) error {
if len(templates) == 0 {
return fmt.Errorf("empty value for \"HostSNIRegexp\" matcher is not allowed")
}
var regexps []*regexp.Regexp
for _, template := range templates {
preparedPattern, err := preparePattern(template)
if err != nil {
return fmt.Errorf("invalid pattern value for \"HostSNIRegexp\" matcher, %q is not a valid pattern: %w", template, err)
}
regexp, err := regexp.Compile(preparedPattern)
if err != nil {
return err
}
regexps = append(regexps, regexp)
}
tree.matcher = func(meta ConnData) bool {
for _, regexp := range regexps {
if regexp.MatchString(meta.serverName) {
return true
}
}
return false
}
return nil
}
// preparePattern builds a regexp pattern from the initial user defined expression.
// This function reuses the code dedicated to host matching of the newRouteRegexp func from the gorilla/mux library.
// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618.
func preparePattern(template string) (string, error) {
// Check if it is well-formed.
idxs, errBraces := braceIndices(template)
if errBraces != nil {
return "", errBraces
}
defaultPattern := "[^.]+"
pattern := bytes.NewBufferString("")
// Host SNI matching is case-insensitive
_, _ = fmt.Fprint(pattern, "(?i)")
pattern.WriteByte('^')
var end int
for i := 0; i < len(idxs); i += 2 {
// Set all values we are interested in.
raw := template[end:idxs[i]]
end = idxs[i+1]
parts := strings.SplitN(template[idxs[i]+1:end-1], ":", 2)
name := parts[0]
patt := defaultPattern
if len(parts) == 2 {
patt = parts[1]
}
// Name or pattern can't be empty.
if name == "" || patt == "" {
return "", fmt.Errorf("mux: missing name or pattern in %q",
template[idxs[i]:end])
}
// Build the regexp pattern.
_, _ = fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt)
}
// Add the remaining.
raw := template[end:]
pattern.WriteString(regexp.QuoteMeta(raw))
pattern.WriteByte('$')
return pattern.String(), nil
}
// varGroupName builds a capturing group name for the indexed variable.
// This function is a copy of varGroupName func from the gorilla/mux library.
// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618.
func varGroupName(idx int) string {
return "v" + strconv.Itoa(idx)
}
// braceIndices returns the first level curly brace indices from a string.
// This function is a copy of braceIndices func from the gorilla/mux library.
// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618.
func braceIndices(s string) ([]int, error) {
var level, idx int
var idxs []int
for i := 0; i < len(s); i++ {
switch s[i] {
case '{':
if level++; level == 1 {
idx = i
}
case '}':
if level--; level == 0 {
idxs = append(idxs, idx, i+1)
} else if level < 0 {
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
}
}
}
if level != 0 {
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
}
return idxs, nil
}

File diff suppressed because it is too large Load diff

View file

@ -41,8 +41,9 @@ 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
routes routes
parser predicate.Parser
parserV2 predicate.Parser
}
// NewMuxer returns a TCP muxer.
@ -57,7 +58,20 @@ func NewMuxer() (*Muxer, error) {
return nil, fmt.Errorf("error while creating rules parser: %w", err)
}
return &Muxer{parser: parser}, nil
var matchersV2 []string
for matcher := range tcpFuncsV2 {
matchersV2 = append(matchersV2, matcher)
}
parserV2, err := rules.NewParser(matchersV2)
if err != nil {
return nil, fmt.Errorf("error while creating v2 rules parser: %w", err)
}
return &Muxer{
parser: parser,
parserV2: parserV2,
}, nil
}
// Match returns the handler of the first route matching the connection metadata,
@ -106,10 +120,26 @@ 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, priority int, handler tcp.Handler) error {
parse, err := m.parser.Parse(rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler tcp.Handler) error {
var parse interface{}
var err error
var matcherFuncs map[string]func(*matchersTree, ...string) error
switch syntax {
case "v2":
parse, err = m.parserV2.Parse(rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
matcherFuncs = tcpFuncsV2
default:
parse, err = m.parser.Parse(rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
matcherFuncs = tcpFuncs
}
buildTree, ok := parse.(rules.TreeBuilder)
@ -120,7 +150,7 @@ func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error {
ruleTree := buildTree()
var matchers matchersTree
err = matchers.addRule(ruleTree)
err = matchers.addRule(ruleTree, matcherFuncs)
if err != nil {
return fmt.Errorf("error while adding rule %s: %w", rule, err)
}
@ -155,6 +185,9 @@ func ParseHostSNI(rule string) ([]string, error) {
for matcher := range tcpFuncs {
matchers = append(matchers, matcher)
}
for matcher := range tcpFuncsV2 {
matchers = append(matchers, matcher)
}
parser, err := rules.NewParser(matchers)
if err != nil {
@ -237,25 +270,27 @@ func (m *matchersTree) match(meta ConnData) bool {
}
}
func (m *matchersTree) addRule(rule *rules.Tree) error {
type matcherFuncs map[string]func(*matchersTree, ...string) error
func (m *matchersTree) addRule(rule *rules.Tree, funcs matcherFuncs) error {
switch rule.Matcher {
case "and", "or":
m.operator = rule.Matcher
m.left = &matchersTree{}
err := m.left.addRule(rule.RuleLeft)
err := m.left.addRule(rule.RuleLeft, funcs)
if err != nil {
return err
}
m.right = &matchersTree{}
return m.right.addRule(rule.RuleRight)
return m.right.addRule(rule.RuleRight, funcs)
default:
err := rules.CheckRule(rule)
if err != nil {
return err
}
err = tcpFuncs[rule.Matcher](m, rule.Value...)
err = funcs[rule.Matcher](m, rule.Value...)
if err != nil {
return err
}

View file

@ -277,7 +277,7 @@ func Test_addTCPRoute(t *testing.T) {
router, err := NewMuxer()
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
@ -447,7 +447,7 @@ func Test_Priority(t *testing.T) {
matchedRule := ""
for rule, priority := range test.rules {
rule := rule
err := muxer.AddRoute(rule, priority, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
err := muxer.AddRoute(rule, "", priority, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
matchedRule = rule
}))
require.NoError(t, err)

View file

@ -112,6 +112,7 @@ 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,

View file

@ -102,6 +102,7 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client
Middlewares: mds,
Rule: route.Match,
Priority: route.Priority,
RuleSyntax: route.Syntax,
Service: serviceName,
}

View file

@ -33,6 +33,9 @@ type Route struct {
// Priority defines the router's priority.
// More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#priority
Priority int `json:"priority,omitempty"`
// Syntax defines the router's rule syntax.
// More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax
Syntax string `json:"syntax,omitempty"`
// Services defines the list of Service.
// It can contain any combination of TraefikService and/or reference to a Kubernetes Service.
Services []Service `json:"services,omitempty"`

View file

@ -29,6 +29,9 @@ type RouteTCP struct {
// Priority defines the router's priority.
// More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#priority_1
Priority int `json:"priority,omitempty"`
// Syntax defines the router's rule syntax.
// More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax_1
Syntax string `json:"syntax,omitempty"`
// Services defines the list of TCP services.
Services []ServiceTCP `json:"services,omitempty"`
// Middlewares defines the list of references to MiddlewareTCP resources.

View file

@ -230,6 +230,7 @@ func (p *Provider) loadConfigurationFromGateway(ctx context.Context, client Clie
err := client.UpdateGatewayClassStatus(gatewayClass, metav1.Condition{
Type: string(gatev1.GatewayClassConditionStatusAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: gatewayClass.Generation,
Reason: "Handled",
Message: "Handled by Traefik controller",
LastTransitionTime: metav1.Now(),
@ -587,7 +588,16 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses [
Type: string(gatev1.GatewayConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: gateway.Generation,
Reason: string(gatev1.GatewayConditionAccepted),
Reason: string(gatev1.GatewayReasonAccepted),
Message: "Gateway successfully scheduled",
LastTransitionTime: metav1.Now(),
},
// update "Programmed" status with "Programmed" reason
metav1.Condition{
Type: string(gatev1.GatewayConditionProgrammed),
Status: metav1.ConditionTrue,
ObservedGeneration: gateway.Generation,
Reason: string(gatev1.GatewayReasonProgrammed),
Message: "Gateway successfully scheduled",
LastTransitionTime: metav1.Now(),
},
@ -760,6 +770,7 @@ func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, li
router := dynamic.Router{
Rule: rule,
RuleSyntax: "v3",
EntryPoints: []string{ep},
}
@ -898,6 +909,7 @@ func gatewayTCPRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Li
router := dynamic.TCPRouter{
Rule: "HostSNI(`*`)",
EntryPoints: []string{ep},
RuleSyntax: "v3",
}
if listener.Protocol == gatev1.TLSProtocolType && listener.TLS != nil {
@ -1062,6 +1074,7 @@ func gatewayTLSRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Li
router := dynamic.TCPRouter{
Rule: rule,
RuleSyntax: "v3",
EntryPoints: []string{ep},
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: listener.TLS.Mode != nil && *listener.TLS.Mode == gatev1.TLSModePassthrough,
@ -1385,7 +1398,7 @@ func extractHeaderRules(headers []gatev1.HTTPHeaderMatch) ([]string, error) {
switch *header.Type {
case gatev1.HeaderMatchExact:
headerRules = append(headerRules, fmt.Sprintf("Headers(`%s`,`%s`)", header.Name, header.Value))
headerRules = append(headerRules, fmt.Sprintf("Header(`%s`,`%s`)", header.Name, header.Value))
default:
return nil, fmt.Errorf("unsupported header match type %s", *header.Type)
}

View file

@ -550,6 +550,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr",
Rule: "Host(`foo.com`) && Path(`/bar`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.Middleware{},
@ -609,6 +610,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "api@internal",
Rule: "Host(`foo.com`) && Path(`/bar`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.Middleware{},
@ -641,6 +643,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr",
Rule: "Host(`foo.com`) && Path(`/bar`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.Middleware{},
@ -704,6 +707,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"websecure"},
Service: "default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06-wrr",
Rule: "Host(`foo.com`) && Path(`/bar`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTLSConfig{},
},
},
@ -773,6 +777,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-66e726cd8903b49727ae-wrr",
Rule: "(Host(`foo.com`) || Host(`bar.com`)) && PathPrefix(`/`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.Middleware{},
@ -832,6 +837,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-3b78e2feb3295ddd87f0-wrr",
Rule: "(Host(`foo.com`) || HostRegexp(`^[a-zA-Z0-9-]+\\.bar\\.com$`)) && PathPrefix(`/`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.Middleware{},
@ -891,6 +897,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-b0521a61fb43068694b4-wrr",
Rule: "(Host(`foo.com`) || HostRegexp(`^[a-zA-Z0-9-]+\\.foo\\.com$`)) && PathPrefix(`/`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.Middleware{},
@ -949,11 +956,13 @@ func TestLoadHTTPRoutes(t *testing.T) {
"default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.com`) && Path(`/bar`)",
RuleSyntax: "v3",
Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr",
},
"default-http-app-1-my-gateway-web-d737b4933fa88e68ab8a": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.com`) && Path(`/bir`)",
RuleSyntax: "v3",
Service: "default-http-app-1-my-gateway-web-d737b4933fa88e68ab8a-wrr",
},
},
@ -1039,6 +1048,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
"default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.com`) && Path(`/bar`)",
RuleSyntax: "v3",
Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr",
},
},
@ -1124,11 +1134,13 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-http-web-1c0cf64bde37d9d0df06-wrr",
Rule: "Host(`foo.com`) && Path(`/bar`)",
RuleSyntax: "v3",
},
"default-http-app-1-my-gateway-https-websecure-1c0cf64bde37d9d0df06": {
EntryPoints: []string{"websecure"},
Service: "default-http-app-1-my-gateway-https-websecure-1c0cf64bde37d9d0df06-wrr",
Rule: "Host(`foo.com`) && Path(`/bar`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTLSConfig{},
},
},
@ -1213,11 +1225,13 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr",
Rule: "Host(`foo.com`) && Path(`/bar`)",
RuleSyntax: "v3",
},
"default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06": {
EntryPoints: []string{"websecure"},
Service: "default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06-wrr",
Rule: "Host(`foo.com`) && Path(`/bar`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTLSConfig{},
},
},
@ -1293,20 +1307,22 @@ func TestLoadHTTPRoutes(t *testing.T) {
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-http-app-1-my-gateway-web-330d644a7f2079e8f454": {
"default-http-app-1-my-gateway-web-4a1b73e6f83804949a37": {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-330d644a7f2079e8f454-wrr",
Rule: "Host(`foo.com`) && PathPrefix(`/bar`) && Headers(`my-header`,`foo`) && Headers(`my-header2`,`bar`)",
Service: "default-http-app-1-my-gateway-web-4a1b73e6f83804949a37-wrr",
Rule: "Host(`foo.com`) && PathPrefix(`/bar`) && Header(`my-header`,`foo`) && Header(`my-header2`,`bar`)",
RuleSyntax: "v3",
},
"default-http-app-1-my-gateway-web-fe80e69a38713941ea22": {
"default-http-app-1-my-gateway-web-aaba0f24fd26e1ca2276": {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-fe80e69a38713941ea22-wrr",
Rule: "Host(`foo.com`) && Path(`/bar`) && Headers(`my-header`,`bar`)",
Service: "default-http-app-1-my-gateway-web-aaba0f24fd26e1ca2276-wrr",
Rule: "Host(`foo.com`) && Path(`/bar`) && Header(`my-header`,`bar`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-http-app-1-my-gateway-web-330d644a7f2079e8f454-wrr": {
"default-http-app-1-my-gateway-web-4a1b73e6f83804949a37-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
@ -1316,7 +1332,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
},
},
},
"default-http-app-1-my-gateway-web-fe80e69a38713941ea22-wrr": {
"default-http-app-1-my-gateway-web-aaba0f24fd26e1ca2276-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
@ -1371,6 +1387,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-default-my-gateway-web-efde1997778109a1f6eb-wrr",
Rule: "Host(`foo.com`) && Path(`/foo`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.Middleware{},
@ -1430,11 +1447,13 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-default-my-gateway-web-efde1997778109a1f6eb-wrr",
Rule: "Host(`foo.com`) && Path(`/foo`)",
RuleSyntax: "v3",
},
"bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597": {
EntryPoints: []string{"web"},
Service: "bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597-wrr",
Rule: "Host(`bar.com`) && Path(`/bar`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.Middleware{},
@ -1520,6 +1539,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597-wrr",
Rule: "Host(`bar.com`) && Path(`/bar`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.Middleware{},
@ -1579,6 +1599,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr",
Rule: "Host(`example.org`) && PathPrefix(`/`)",
RuleSyntax: "v3",
Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0"},
},
},
@ -1647,6 +1668,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr",
Rule: "Host(`example.org`) && PathPrefix(`/`)",
RuleSyntax: "v3",
Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0"},
},
},
@ -1912,6 +1934,7 @@ func TestLoadTCPRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "default-tcp-app-1-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
@ -1969,11 +1992,13 @@ func TestLoadTCPRoutes(t *testing.T) {
EntryPoints: []string{"tcp-1"},
Service: "default-tcp-app-1-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
"default-tcp-app-2-my-tcp-gateway-tcp-2-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tcp-2"},
Service: "default-tcp-app-2-my-tcp-gateway-tcp-2-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
@ -2053,6 +2078,7 @@ func TestLoadTCPRoutes(t *testing.T) {
EntryPoints: []string{"tcp-1"},
Service: "default-tcp-app-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
@ -2144,6 +2170,7 @@ func TestLoadTCPRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "default-tcp-app-1-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
@ -2203,6 +2230,7 @@ func TestLoadTCPRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
@ -2266,6 +2294,7 @@ func TestLoadTCPRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "default-tcp-app-default-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
@ -2321,11 +2350,13 @@ func TestLoadTCPRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "default-tcp-app-default-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
"bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tcp"},
Service: "bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
@ -2403,6 +2434,7 @@ func TestLoadTCPRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
@ -2696,6 +2728,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "default-tcp-app-1-my-tls-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
@ -2761,6 +2794,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "default-tcp-app-1-my-tls-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -2819,6 +2853,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "default-tls-app-1-my-tls-gateway-tcp-f0dd0dd89f82eae1c270-wrr-0",
Rule: "HostSNI(`foo.example.com`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -2878,12 +2913,14 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "default-tcp-app-1-my-tls-gateway-tls-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{},
},
"default-tls-app-1-my-tls-gateway-tcp-673acf455cb2dab0b43a": {
EntryPoints: []string{"tcp"},
Service: "default-tls-app-1-my-tls-gateway-tcp-673acf455cb2dab0b43a-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -2973,6 +3010,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
@ -3042,6 +3080,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "default-tls-app-1-my-gateway-tls-f0dd0dd89f82eae1c270-wrr-0",
Rule: "HostSNI(`foo.example.com`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -3100,6 +3139,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "default-tls-app-1-my-gateway-tls-f0dd0dd89f82eae1c270-wrr-0",
Rule: "HostSNI(`foo.example.com`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -3158,6 +3198,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "default-tls-app-1-my-gateway-tls-f0dd0dd89f82eae1c270-wrr-0",
Rule: "HostSNI(`foo.example.com`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -3216,6 +3257,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "default-tls-app-1-my-gateway-tls-d5342d75658583f03593-wrr-0",
Rule: "HostSNI(`foo.example.com`) || HostSNI(`bar.example.com`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -3274,6 +3316,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "default-tls-app-default-my-gateway-tls-06ae57dcf13ab4c60ee5-wrr-0",
Rule: "HostSNI(`foo.default`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -3332,6 +3375,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "default-tls-app-default-my-gateway-tls-06ae57dcf13ab4c60ee5-wrr-0",
Rule: "HostSNI(`foo.default`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -3340,6 +3384,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "bar-tls-app-bar-my-gateway-tls-2279fe75c5156dc5eb26-wrr-0",
Rule: "HostSNI(`foo.bar`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -3420,6 +3465,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "bar-tls-app-bar-my-gateway-tls-2279fe75c5156dc5eb26-wrr-0",
Rule: "HostSNI(`foo.bar`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -3478,6 +3524,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tcp-1"},
Service: "default-tls-app-my-gateway-tcp-1-673acf455cb2dab0b43a-wrr",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -3702,17 +3749,20 @@ func TestLoadMixedRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "default-tcp-app-1-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
"default-tcp-app-1-my-gateway-tls-1-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls-1"},
Service: "default-tcp-app-1-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{},
},
"default-tls-app-1-my-gateway-tls-2-59130f7db6718b7700c1": {
EntryPoints: []string{"tls-2"},
Service: "default-tls-app-1-my-gateway-tls-2-59130f7db6718b7700c1-wrr-0",
Rule: "HostSNI(`pass.tls.foo.example.com`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -3771,11 +3821,13 @@ func TestLoadMixedRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
},
"default-http-app-1-my-gateway-websecure-a431b128267aabc954fd": {
EntryPoints: []string{"websecure"},
Service: "default-http-app-1-my-gateway-websecure-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTLSConfig{},
},
},
@ -3881,17 +3933,20 @@ func TestLoadMixedRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
"default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls-1"},
Service: "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{},
},
"default-tls-app-default-my-gateway-tls-2-59130f7db6718b7700c1": {
EntryPoints: []string{"tls-2"},
Service: "default-tls-app-default-my-gateway-tls-2-59130f7db6718b7700c1-wrr-0",
Rule: "HostSNI(`pass.tls.foo.example.com`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -3950,11 +4005,13 @@ func TestLoadMixedRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
},
"default-http-app-default-my-gateway-websecure-a431b128267aabc954fd": {
EntryPoints: []string{"websecure"},
Service: "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTLSConfig{},
},
},
@ -4032,17 +4089,20 @@ func TestLoadMixedRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
"default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls-1"},
Service: "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{},
},
"default-tls-app-default-my-gateway-tls-2-59130f7db6718b7700c1": {
EntryPoints: []string{"tls-2"},
Service: "default-tls-app-default-my-gateway-tls-2-59130f7db6718b7700c1-wrr-0",
Rule: "HostSNI(`pass.tls.foo.example.com`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -4051,11 +4111,13 @@ func TestLoadMixedRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "bar-tcp-app-bar-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
"bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls-1"},
Service: "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
@ -4144,22 +4206,26 @@ func TestLoadMixedRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
},
"default-http-app-default-my-gateway-websecure-a431b128267aabc954fd": {
EntryPoints: []string{"websecure"},
Service: "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTLSConfig{},
},
"bar-http-app-bar-my-gateway-web-a431b128267aabc954fd": {
EntryPoints: []string{"web"},
Service: "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
},
"bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd": {
EntryPoints: []string{"websecure"},
Service: "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTLSConfig{},
},
},
@ -4273,17 +4339,20 @@ func TestLoadMixedRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "bar-tcp-app-bar-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
"bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls-1"},
Service: "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{},
},
"bar-tls-app-bar-my-gateway-tls-2-59130f7db6718b7700c1": {
EntryPoints: []string{"tls-2"},
Service: "bar-tls-app-bar-my-gateway-tls-2-59130f7db6718b7700c1-wrr-0",
Rule: "HostSNI(`pass.tls.foo.example.com`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
},
@ -4342,11 +4411,13 @@ func TestLoadMixedRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
},
"bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd": {
EntryPoints: []string{"websecure"},
Service: "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTLSConfig{},
},
},
@ -4423,11 +4494,13 @@ func TestLoadMixedRoutes(t *testing.T) {
EntryPoints: []string{"tcp"},
Service: "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
},
"default-tcp-app-default-my-gateway-tls-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls"},
Service: "default-tcp-app-default-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
@ -4474,11 +4547,13 @@ func TestLoadMixedRoutes(t *testing.T) {
EntryPoints: []string{"web"},
Service: "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
},
"default-http-app-default-my-gateway-websecure-a431b128267aabc954fd": {
EntryPoints: []string{"websecure"},
Service: "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr",
Rule: "PathPrefix(`/`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTLSConfig{},
},
},
@ -4807,7 +4882,7 @@ func Test_extractRule(t *testing.T) {
},
},
},
expectedRule: "Path(`/foo/`) || Headers(`my-header`,`foo`)",
expectedRule: "Path(`/foo/`) || Header(`my-header`,`foo`)",
},
{
desc: "Path && Header rules",
@ -4828,7 +4903,7 @@ func Test_extractRule(t *testing.T) {
},
},
},
expectedRule: "Path(`/foo/`) && Headers(`my-header`,`foo`)",
expectedRule: "Path(`/foo/`) && Header(`my-header`,`foo`)",
},
{
desc: "Host && Path && Header rules",
@ -4850,7 +4925,7 @@ func Test_extractRule(t *testing.T) {
},
},
},
expectedRule: "Host(`foo.com`) && Path(`/foo/`) && Headers(`my-header`,`foo`)",
expectedRule: "Host(`foo.com`) && Path(`/foo/`) && Header(`my-header`,`foo`)",
},
{
desc: "Host && (Path || Header) rules",
@ -4874,7 +4949,7 @@ func Test_extractRule(t *testing.T) {
},
},
},
expectedRule: "Host(`foo.com`) && (Path(`/foo/`) || Headers(`my-header`,`foo`))",
expectedRule: "Host(`foo.com`) && (Path(`/foo/`) || Header(`my-header`,`foo`))",
},
}

View file

@ -22,6 +22,11 @@
"priority": 2147483645
}
},
"services": {
"api": {},
"dashboard": {},
"noop": {}
},
"middlewares": {
"dashboard_redirect": {
"redirectRegex": {
@ -38,11 +43,6 @@
]
}
}
},
"services": {
"api": {},
"dashboard": {},
"noop": {}
}
},
"tcp": {},

View file

@ -54,6 +54,14 @@
"priority": 2147483647
}
},
"services": {
"api": {},
"dashboard": {},
"noop": {},
"ping": {},
"prometheus": {},
"rest": {}
},
"middlewares": {
"dashboard_redirect": {
"redirectRegex": {
@ -70,14 +78,6 @@
]
}
}
},
"services": {
"api": {},
"dashboard": {},
"noop": {},
"ping": {},
"prometheus": {},
"rest": {}
}
},
"tcp": {},

View file

@ -12,6 +12,9 @@
"rule": "HostRegexp(`^.+$`)"
}
},
"services": {
"noop": {}
},
"middlewares": {
"redirect-web-to-websecure": {
"redirectScheme": {
@ -20,9 +23,6 @@
"permanent": true
}
}
},
"services": {
"noop": {}
}
},
"tcp": {},

View file

@ -12,6 +12,9 @@
"rule": "HostRegexp(`^.+$`)"
}
},
"services": {
"noop": {}
},
"middlewares": {
"redirect-web-to-443": {
"redirectScheme": {
@ -20,9 +23,6 @@
"permanent": true
}
}
},
"services": {
"noop": {}
}
},
"tcp": {},

View file

@ -12,6 +12,9 @@
"rule": "HostRegexp(`^.+$`)"
}
},
"services": {
"noop": {}
},
"middlewares": {
"redirect-web-to-websecure": {
"redirectScheme": {
@ -20,9 +23,6 @@
"permanent": true
}
}
},
"services": {
"noop": {}
}
},
"tcp": {},

View file

@ -65,6 +65,7 @@ func (i *Provider) createConfiguration(ctx context.Context) *dynamic.Configurati
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
Models: make(map[string]*dynamic.TCPModel),
ServersTransports: make(map[string]*dynamic.TCPServersTransport),
},
TLS: &dynamic.TLSConfiguration{
@ -191,8 +192,13 @@ func (i *Provider) getEntryPointPort(name string, def *static.Redirections) (str
}
func (i *Provider) entryPointModels(cfg *dynamic.Configuration) {
defaultRuleSyntax := ""
if i.staticCfg.Core != nil && i.staticCfg.Core.DefaultRuleSyntax != "" {
defaultRuleSyntax = i.staticCfg.Core.DefaultRuleSyntax
}
for name, ep := range i.staticCfg.EntryPoints {
if len(ep.HTTP.Middlewares) == 0 && ep.HTTP.TLS == nil {
if len(ep.HTTP.Middlewares) == 0 && ep.HTTP.TLS == nil && defaultRuleSyntax == "" {
continue
}
@ -208,7 +214,19 @@ func (i *Provider) entryPointModels(cfg *dynamic.Configuration) {
}
}
m.DefaultRuleSyntax = defaultRuleSyntax
cfg.HTTP.Models[name] = m
if cfg.TCP == nil {
continue
}
mTCP := &dynamic.TCPModel{
DefaultRuleSyntax: defaultRuleSyntax,
}
cfg.TCP.Models[name] = mTCP
}
}

View file

@ -24,6 +24,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
Models: make(map[string]*dynamic.TCPModel),
ServersTransports: make(map[string]*dynamic.TCPServersTransport),
},
UDP: &dynamic.UDPConfiguration{
@ -152,6 +153,13 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
for name, rt := range cfg.HTTP.Routers {
router := rt.DeepCopy()
if !router.DefaultRule && router.RuleSyntax == "" {
for _, model := range cfg.HTTP.Models {
router.RuleSyntax = model.DefaultRuleSyntax
break
}
}
eps := router.EntryPoints
router.EntryPoints = nil
@ -183,6 +191,25 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
cfg.HTTP.Routers = rts
if cfg.TCP == nil || len(cfg.TCP.Models) == 0 {
return cfg
}
tcpRouters := make(map[string]*dynamic.TCPRouter)
for _, rt := range cfg.TCP.Routers {
router := rt.DeepCopy()
if router.RuleSyntax == "" {
for _, model := range cfg.TCP.Models {
router.RuleSyntax = model.DefaultRuleSyntax
break
}
}
}
cfg.TCP.Routers = tcpRouters
return cfg
}

View file

@ -473,6 +473,7 @@ func Test_mergeConfiguration_defaultTCPEntryPoint(t *testing.T) {
Services: map[string]*dynamic.TCPService{
"service-1@provider-1": {},
},
Models: map[string]*dynamic.TCPModel{},
ServersTransports: make(map[string]*dynamic.TCPServersTransport),
}

View file

@ -23,7 +23,7 @@ type mockProvider struct {
throttleDuration time.Duration
}
func (p *mockProvider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
func (p *mockProvider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
wait := p.wait
if wait == 0 {
wait = 20 * time.Millisecond
@ -48,7 +48,7 @@ func (p *mockProvider) Provide(configurationChan chan<- dynamic.Message, pool *s
}
// ThrottleDuration returns the throttle duration.
func (p mockProvider) ThrottleDuration() time.Duration {
func (p *mockProvider) ThrottleDuration() time.Duration {
return p.throttleDuration
}
@ -92,6 +92,7 @@ func TestNewConfigurationWatcher(t *testing.T) {
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
Models: map[string]*dynamic.TCPModel{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
@ -123,7 +124,7 @@ func TestWaitForRequiredProvider(t *testing.T) {
config := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo")),
th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar")),
),
}
@ -167,60 +168,14 @@ func TestIgnoreTransientConfiguration(t *testing.T) {
config := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo")),
th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar")),
),
}
config2 := &dynamic.Configuration{
expectedConfig := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("baz")),
th.WithLoadBalancerServices(th.WithService("toto")),
),
}
watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{"defaultEP"}, "")
publishedConfigCount := 0
var lastConfig dynamic.Configuration
blockConfConsumer := make(chan struct{})
watcher.AddListener(func(config dynamic.Configuration) {
publishedConfigCount++
lastConfig = config
<-blockConfConsumer
})
watcher.Start()
t.Cleanup(watcher.Stop)
t.Cleanup(routinesPool.Stop)
watcher.allProvidersConfigs <- dynamic.Message{
ProviderName: "mock",
Configuration: config,
}
watcher.allProvidersConfigs <- dynamic.Message{
ProviderName: "mock",
Configuration: config2,
}
watcher.allProvidersConfigs <- dynamic.Message{
ProviderName: "mock",
Configuration: config,
}
close(blockConfConsumer)
// give some time so that the configuration can be processed
time.Sleep(20 * time.Millisecond)
// after 20 milliseconds we should have 1 configs published
assert.Equal(t, 1, publishedConfigCount, "times configs were published")
expected := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("defaultEP"))),
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar@mock")),
th.WithMiddlewares(),
),
@ -228,6 +183,7 @@ func TestIgnoreTransientConfiguration(t *testing.T) {
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
Models: map[string]*dynamic.TCPModel{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
UDP: &dynamic.UDPConfiguration{
@ -242,7 +198,109 @@ func TestIgnoreTransientConfiguration(t *testing.T) {
},
}
assert.Equal(t, expected, lastConfig)
expectedConfig3 := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar-config3@mock")),
th.WithMiddlewares(),
),
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
Models: map[string]*dynamic.TCPModel{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TLS: &dynamic.TLSConfiguration{
Options: map[string]tls.Options{
"default": tls.DefaultTLSOptions,
},
Stores: map[string]tls.Store{},
},
}
config2 := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("baz", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("toto")),
),
}
config3 := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar-config3")),
),
}
watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{}, "")
// To be able to "block" the writes, we change the chan to remove buffering.
watcher.allProvidersConfigs = make(chan dynamic.Message)
publishedConfigCount := 0
firstConfigHandled := make(chan struct{})
blockConfConsumer := make(chan struct{})
blockConfConsumerAssert := make(chan struct{})
watcher.AddListener(func(config dynamic.Configuration) {
publishedConfigCount++
if publishedConfigCount > 2 {
t.Fatal("More than 2 published configuration")
}
if publishedConfigCount == 1 {
assert.Equal(t, expectedConfig, config)
close(firstConfigHandled)
<-blockConfConsumer
time.Sleep(500 * time.Millisecond)
}
if publishedConfigCount == 2 {
assert.Equal(t, expectedConfig3, config)
close(blockConfConsumerAssert)
}
})
watcher.Start()
t.Cleanup(watcher.Stop)
t.Cleanup(routinesPool.Stop)
watcher.allProvidersConfigs <- dynamic.Message{
ProviderName: "mock",
Configuration: config,
}
<-firstConfigHandled
watcher.allProvidersConfigs <- dynamic.Message{
ProviderName: "mock",
Configuration: config2,
}
watcher.allProvidersConfigs <- dynamic.Message{
ProviderName: "mock",
Configuration: config,
}
close(blockConfConsumer)
watcher.allProvidersConfigs <- dynamic.Message{
ProviderName: "mock",
Configuration: config3,
}
select {
case <-blockConfConsumerAssert:
case <-time.After(10 * time.Second):
t.Fatal("Timeout")
}
}
func TestListenProvidersThrottleProviderConfigReload(t *testing.T) {
@ -258,7 +316,7 @@ func TestListenProvidersThrottleProviderConfigReload(t *testing.T) {
ProviderName: "mock",
Configuration: &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i))),
th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i), th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar")),
),
},
@ -318,7 +376,7 @@ func TestListenProvidersSkipsSameConfigurationForProvider(t *testing.T) {
ProviderName: "mock",
Configuration: &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo")),
th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar")),
),
},
@ -350,14 +408,14 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) {
configuration := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo")),
th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar")),
),
}
transientConfiguration := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("bad")),
th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bad")),
),
}
@ -372,7 +430,7 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) {
},
}
watcher := NewConfigurationWatcher(routinesPool, pvd, []string{"defaultEP"}, "")
watcher := NewConfigurationWatcher(routinesPool, pvd, []string{}, "")
var lastConfig dynamic.Configuration
watcher.AddListener(func(conf dynamic.Configuration) {
@ -389,7 +447,7 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) {
expected := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("defaultEP"))),
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar@mock")),
th.WithMiddlewares(),
),
@ -397,6 +455,7 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) {
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
Models: map[string]*dynamic.TCPModel{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
UDP: &dynamic.UDPConfiguration{
@ -419,14 +478,14 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) {
configuration := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo")),
th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar")),
),
}
transientConfiguration := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("bad")),
th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bad")),
),
}
@ -452,7 +511,7 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) {
err := providerAggregator.AddProvider(pvd)
assert.NoError(t, err)
watcher := NewConfigurationWatcher(routinesPool, providerAggregator, []string{"defaultEP"}, "")
watcher := NewConfigurationWatcher(routinesPool, providerAggregator, []string{}, "")
var configurationReloads int
var lastConfig dynamic.Configuration
@ -479,7 +538,7 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) {
expected := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("defaultEP"))),
th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar@mock")),
th.WithMiddlewares(),
),
@ -487,6 +546,7 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) {
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
Models: map[string]*dynamic.TCPModel{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
UDP: &dynamic.UDPConfiguration{
@ -509,7 +569,7 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) {
func TestApplyConfigUnderStress(t *testing.T) {
routinesPool := safe.NewPool(context.Background())
watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{"defaultEP"}, "")
watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{}, "")
routinesPool.GoCtx(func(ctx context.Context) {
i := 0
@ -519,7 +579,7 @@ func TestApplyConfigUnderStress(t *testing.T) {
return
case watcher.allProvidersConfigs <- dynamic.Message{ProviderName: "mock", Configuration: &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i))),
th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i), th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar")),
),
}}:
@ -554,28 +614,28 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) {
configuration := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo")),
th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar")),
),
}
transientConfiguration := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("bad")),
th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bad")),
),
}
transientConfiguration2 := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("bad2")),
th.WithRouters(th.WithRouter("bad2", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bad2")),
),
}
finalConfiguration := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("final")),
th.WithRouters(th.WithRouter("final", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("final")),
),
}
@ -595,7 +655,7 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) {
err := providerAggregator.AddProvider(pvd)
assert.NoError(t, err)
watcher := NewConfigurationWatcher(routinesPool, providerAggregator, []string{"defaultEP"}, "")
watcher := NewConfigurationWatcher(routinesPool, providerAggregator, []string{}, "")
var configurationReloads int
var lastConfig dynamic.Configuration
@ -614,7 +674,7 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) {
expected := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("final@mock", th.WithEntryPoints("defaultEP"))),
th.WithRouters(th.WithRouter("final@mock", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("final@mock")),
th.WithMiddlewares(),
),
@ -622,6 +682,7 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) {
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
Models: map[string]*dynamic.TCPModel{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
UDP: &dynamic.UDPConfiguration{
@ -646,7 +707,7 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) {
configuration := &dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(th.WithRouter("foo")),
th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))),
th.WithLoadBalancerServices(th.WithService("bar")),
),
}
@ -658,7 +719,7 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) {
},
}
watcher := NewConfigurationWatcher(routinesPool, pvd, []string{"defaultEP"}, "")
watcher := NewConfigurationWatcher(routinesPool, pvd, []string{}, "")
var publishedProviderConfig dynamic.Configuration
@ -677,8 +738,8 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) {
expected := dynamic.Configuration{
HTTP: th.BuildConfiguration(
th.WithRouters(
th.WithRouter("foo@mock", th.WithEntryPoints("defaultEP")),
th.WithRouter("foo@mock2", th.WithEntryPoints("defaultEP")),
th.WithRouter("foo@mock", th.WithEntryPoints("ep")),
th.WithRouter("foo@mock2", th.WithEntryPoints("ep")),
),
th.WithLoadBalancerServices(
th.WithService("bar@mock"),
@ -690,6 +751,7 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) {
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
Models: map[string]*dynamic.TCPModel{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
TLS: &dynamic.TLSConfiguration{

View file

@ -131,7 +131,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
continue
}
if err = muxer.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil {
if err = muxer.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
routerConfig.AddError(err, true)
logger.Error().Err(err).Send()
continue

View file

@ -311,7 +311,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
if routerConfig.TLS == nil {
logger.Debug().Msgf("Adding route for %q", routerConfig.Rule)
if err := router.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil {
if err := router.muxerTCP.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
routerConfig.AddError(err, true)
logger.Error().Err(err).Send()
}
@ -321,7 +321,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
if routerConfig.TLS.Passthrough {
logger.Debug().Msgf("Adding Passthrough route for %q", routerConfig.Rule)
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil {
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
routerConfig.AddError(err, true)
logger.Error().Err(err).Send()
}
@ -355,7 +355,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
logger.Debug().Msgf("Adding special TLS closing route for %q because broken TLS options %s", routerConfig.Rule, tlsOptionsName)
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.Priority, &brokenTLSRouter{}); err != nil {
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, &brokenTLSRouter{}); err != nil {
routerConfig.AddError(err, true)
logger.Error().Err(err).Send()
}
@ -389,7 +389,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
logger.Debug().Msgf("Adding TLS route for %q", routerConfig.Rule)
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil {
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
routerConfig.AddError(err, true)
logger.Error().Err(err).Send()
continue

View file

@ -201,9 +201,9 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
conn.Close()
}
// AddRoute defines a handler for the given rule.
func (r *Router) AddRoute(rule string, priority int, target tcp.Handler) error {
return r.muxerTCP.AddRoute(rule, priority, target)
// AddTCPRoute defines a handler for the given rule.
func (r *Router) AddTCPRoute(rule string, priority int, target tcp.Handler) error {
return r.muxerTCP.AddRoute(rule, "", priority, target)
}
// AddHTTPTLSConfig defines a handler for a given sniHost and sets the matching tlsConfig.
@ -267,7 +267,7 @@ func (r *Router) SetHTTPSForwarder(handler tcp.Handler) {
}
rule := "HostSNI(`" + sniHost + "`)"
if err := r.muxerHTTPS.AddRoute(rule, tcpmuxer.GetRulePriority(rule), tcpHandler); err != nil {
if err := r.muxerHTTPS.AddRoute(rule, "", tcpmuxer.GetRulePriority(rule), tcpHandler); err != nil {
log.Error().Err(err).Msg("Error while adding route for host")
}
}

View file

@ -947,10 +947,10 @@ func TestPostgres(t *testing.T) {
// This test requires to have a TLS route, but does not actually check the
// content of the handler. It would require to code a TLS handshake to
// check the SNI and content of the handlerFunc.
err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", 0, nil)
err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, nil)
require.NoError(t, err)
err = router.AddRoute("HostSNI(`*`)", 0, tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
err = router.muxerTCP.AddRoute("HostSNI(`*`)", "", 0, tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
_, _ = conn.Write([]byte("OK"))
_ = conn.Close()
}))

View file

@ -47,7 +47,7 @@ func TestShutdownTCP(t *testing.T) {
router, err := tcprouter.NewRouter()
require.NoError(t, err)
err = router.AddRoute("HostSNI(`*`)", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
err = router.AddTCPRoute("HostSNI(`*`)", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
_, err := http.ReadRequest(bufio.NewReader(conn))
if err != nil {
return

View file

@ -319,7 +319,7 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
proxy = tracingMiddle.NewService(ctx, serviceName, proxy)
lb.Add(proxyName, proxy, nil)
lb.Add(proxyName, proxy, server.Weight)
// servers are considered UP by default.
info.UpdateServerStatus(target.String(), runtime.StatusUp)