Compare commits

...

28 Commits

Author SHA1 Message Date
Romain
a49b537d9c Prepare release v2.7.0-rc2 2022-03-29 17:00:09 +02:00
romain
45328ab719 Merge v2.6 into v2.7 2022-03-29 15:43:10 +02:00
Jean-Baptiste Doumenjou
c0b0f3f0f7 Fix hub tls documentation 2022-03-25 15:42:08 +01:00
Tom Moulard
a4560fa20d Prepare release v2.7.0-rc1 2022-03-24 20:54:08 +01:00
Jean-Baptiste Doumenjou
fbdb6e6e78 Add Traefik Hub Integration (Experimental Feature) 2022-03-24 19:44:08 +01:00
romain
8d58f33a28 Merge v2.6 into master 2022-03-24 17:22:56 +01:00
Douglas De Toni Machado
d2a2362be5 Add a Feature Deprecation page 2022-03-24 12:28:07 +01:00
Ludovic Fernandez
4c0a3721d0 Plugins and token 2022-03-24 08:54:07 +01:00
Tom Moulard
3bf4a8fbe2 Merge current v2.6 into master 2022-03-22 15:55:44 +01:00
Romain
2da7fa0397 Add HostSNIRegexp rule matcher for TCP 2022-03-18 16:04:08 +01:00
Tom Moulard
0d58e8d1ad Add Traefik Hub access and remove Pilot access 2022-03-18 11:06:08 +01:00
Daniel Tomcej
dad76e0478 Add muxer for TCP Routers 2022-03-17 18:02:08 +01:00
Tom Moulard
79aab5aab8 Add Failover service
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
2022-03-17 12:02:09 +01:00
Tom Moulard
6622027c7c Merge current v2.6 into master 2022-03-11 10:07:20 +01:00
Tchoupinax
401c171bbd Add a link to service on router detail view 2022-03-07 16:16:08 +01:00
Tom Moulard
63bb770b9c Allow empty services in Kubernetes CRD 2022-03-07 11:08:07 +01:00
Tom Moulard
c7b24f4e9c Replace npm with yarn to install/run the webui 2022-03-03 18:08:07 +01:00
Tom Moulard
25725e9b2f Merge current v2.6 into master 2022-02-21 14:07:27 +01:00
Kevin Pollet
aaf5aa4506 Configure advertised port using h3 server option
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
2022-02-15 16:04:09 +01:00
Sylvain Rabot
9297055ad8 Upgrade quic-go to v0.25.0 2022-02-15 10:16:08 +01:00
Tom Moulard
a79868fadc Merge current v2.6 into master 2022-02-15 09:09:16 +01:00
Dmitry Sharshakov
ca55dfe1c6 Support InfluxDB v2 metrics backend 2022-02-09 15:32:12 +01:00
Richard Kojedzinszky
5780dc2b15 Refactor configuration reload/throttling
Co-authored-by: Mathieu Lonjaret <mathieu.lonjaret@gmail.com>
2022-02-07 11:58:04 +01:00
Tom Moulard
764bf59d4d Merge current v2.6 into master 2022-02-04 14:32:57 +01:00
JasonWang2016
7543709ecf Watch for Consul events to rebuild the dynamic configuration
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
2022-01-28 17:16:07 +01:00
kevinpollet
1048348ae6 Merge current v2.6 into master 2022-01-25 18:19:40 +01:00
Tom Moulard
ba822acb23 Merge current v2.6 into master 2022-01-10 16:17:25 +01:00
Tom Moulard
f5dd233a3b Merge current v2.6 into master 2021-12-29 17:35:32 +01:00
144 changed files with 16532 additions and 18263 deletions

View File

@@ -214,3 +214,6 @@
[[issues.exclude-rules]]
path = "(.+)\\.go"
text = "struct-tag: unknown option 'inline' in JSON tag"
[[issues.exclude-rules]]
path = "pkg/server/router/tcp/manager.go"
text = "Function 'buildEntryPointHandler' is too long (.+)"

View File

@@ -64,7 +64,7 @@ blocks:
- name: GH_VERSION
value: 1.12.1
- name: CODENAME
value: "rocamadour"
value: "epoisses"
- name: IN_DOCKER
value: ""
prologue:

View File

@@ -1,3 +1,12 @@
## [v2.7.0-rc2](https://github.com/traefik/traefik/tree/v2.7.0-rc2) (2022-03-29)
[All Commits](https://github.com/traefik/traefik/compare/v2.7.0-rc1...v2.7.0-rc2)
**Documentation:**
- **[hub]** Fix Traefik Hub TLS documentation ([#8883](https://github.com/traefik/traefik/pull/8883) by [jbdoumenjou](https://github.com/jbdoumenjou))
**Misc:**
- Merge current v2.6 into v2.7 ([#8899](https://github.com/traefik/traefik/pull/8899) by [rtribotte](https://github.com/rtribotte))
## [v2.6.3](https://github.com/traefik/traefik/tree/v2.6.3) (2022-03-28)
[All Commits](https://github.com/traefik/traefik/compare/v2.6.2...v2.6.3)
@@ -5,6 +14,39 @@
- **[plugins]** Fix slice parsing for plugins ([#8886](https://github.com/traefik/traefik/pull/8886) by [ldez](https://github.com/ldez))
- **[tls]** Return TLS unrecognized_name error when no certificate is available ([#8893](https://github.com/traefik/traefik/pull/8893) by [rtribotte](https://github.com/rtribotte))
## [v2.7.0-rc1](https://github.com/traefik/traefik/tree/v2.7.0-rc1) (2022-03-24)
[All Commits](https://github.com/traefik/traefik/compare/v2.6.0-rc1...v2.7.0-rc1)
**Enhancements:**
- **[consulcatalog]** Watch for Consul events to rebuild the dynamic configuration ([#8476](https://github.com/traefik/traefik/pull/8476) by [JasonWangA](https://github.com/JasonWangA))
- **[healthcheck]** Add Failover service ([#8825](https://github.com/traefik/traefik/pull/8825) by [tomMoulard](https://github.com/tomMoulard))
- **[http3]** Configure advertised port using h3 server option ([#8778](https://github.com/traefik/traefik/pull/8778) by [kevinpollet](https://github.com/kevinpollet))
- **[http3]** Upgrade quic-go to v0.25.0 ([#8760](https://github.com/traefik/traefik/pull/8760) by [sylr](https://github.com/sylr))
- **[hub]** Add Traefik Hub Integration (Experimental Feature) ([#8837](https://github.com/traefik/traefik/pull/8837) by [jbdoumenjou](https://github.com/jbdoumenjou))
- **[k8s/crd,k8s]** Allow empty services in Kubernetes CRD ([#8802](https://github.com/traefik/traefik/pull/8802) by [tomMoulard](https://github.com/tomMoulard))
- **[metrics]** Support InfluxDB v2 metrics backend ([#8250](https://github.com/traefik/traefik/pull/8250) by [sh7dm](https://github.com/sh7dm))
- **[plugins]** Remove Pilot token setup constraint to use plugins ([#8869](https://github.com/traefik/traefik/pull/8869) by [ldez](https://github.com/ldez))
- **[provider]** Refactor configuration reload/throttling ([#6633](https://github.com/traefik/traefik/pull/6633) by [rkojedzinszky](https://github.com/rkojedzinszky))
- **[rules,tcp]** Add HostSNIRegexp rule matcher for TCP ([#8849](https://github.com/traefik/traefik/pull/8849) by [rtribotte](https://github.com/rtribotte))
- **[tcp]** Add muxer for TCP Routers ([#8182](https://github.com/traefik/traefik/pull/8182) by [dtomcej](https://github.com/dtomcej))
- **[webui,pilot]** Add Traefik Hub access and remove Pilot access ([#8848](https://github.com/traefik/traefik/pull/8848) by [tomMoulard](https://github.com/tomMoulard))
- **[webui]** Add a link to service on router detail view ([#8821](https://github.com/traefik/traefik/pull/8821) by [Tchoupinax](https://github.com/Tchoupinax))
**Documentation:**
- Add a Feature Deprecation page ([#8868](https://github.com/traefik/traefik/pull/8868) by [ddtmachado](https://github.com/ddtmachado))
**Misc:**
- Merge current v2.6 into master ([#8877](https://github.com/traefik/traefik/pull/8877) by [rtribotte](https://github.com/rtribotte))
- Merge current v2.6 into master ([#8865](https://github.com/traefik/traefik/pull/8865) by [tomMoulard](https://github.com/tomMoulard))
- Merge current v2.6 into master ([#8832](https://github.com/traefik/traefik/pull/8832) by [tomMoulard](https://github.com/tomMoulard))
- Merge current v2.6 into master ([#8793](https://github.com/traefik/traefik/pull/8793) by [tomMoulard](https://github.com/tomMoulard))
- Merge current v2.6 into master ([#8777](https://github.com/traefik/traefik/pull/8777) by [tomMoulard](https://github.com/tomMoulard))
- Merge current v2.6 into master ([#8757](https://github.com/traefik/traefik/pull/8757) by [tomMoulard](https://github.com/tomMoulard))
- Merge current v2.6 into master ([#8754](https://github.com/traefik/traefik/pull/8754) by [tomMoulard](https://github.com/tomMoulard))
- Merge current v2.6 into master ([#8736](https://github.com/traefik/traefik/pull/8736) by [kevinpollet](https://github.com/kevinpollet))
- Merge current v2.6 into master ([#8689](https://github.com/traefik/traefik/pull/8689) by [tomMoulard](https://github.com/tomMoulard))
- Merge current v2.6 into master ([#8666](https://github.com/traefik/traefik/pull/8666) by [tomMoulard](https://github.com/tomMoulard))
## [v2.6.2](https://github.com/traefik/traefik/tree/v2.6.2) (2022-03-24)
[All Commits](https://github.com/traefik/traefik/compare/v2.6.1...v2.6.2)

View File

@@ -36,8 +36,6 @@ DOCKER_RUN_TRAEFIK_NOTTY := docker run $(INTEGRATION_OPTS) $(if $(DOCKER_NON_INT
IN_DOCKER ?= true
PLATFORM_URL := $(if $(PLATFORM_URL),$(PLATFORM_URL),"https://pilot.traefik.io")
default: binary
## Build Dev Docker image
@@ -54,7 +52,7 @@ dist:
## Build WebUI Docker image
build-webui-image:
docker build -t traefik-webui --build-arg ARG_PLATFORM_URL=$(PLATFORM_URL) -f webui/Dockerfile webui
docker build -t traefik-webui -f webui/Dockerfile webui
## Clean WebUI static generated assets
clean-webui:
@@ -65,7 +63,7 @@ clean-webui:
## Generate WebUI
webui/static/index.html:
$(MAKE) build-webui-image
docker run --rm -v "$$PWD/webui/static":'/src/webui/static' traefik-webui npm run build:nc
docker run --rm -v "$$PWD/webui/static":'/src/webui/static' traefik-webui yarn build:nc
docker run --rm -v "$$PWD/webui/static":'/src/webui/static' traefik-webui chown -R $(shell id -u):$(shell id -g) ./static
generate-webui: webui/static/index.html

View File

@@ -27,10 +27,10 @@ func initPlugins(staticCfg *static.Configuration) (*plugins.Client, map[string]p
var client *plugins.Client
plgs := map[string]plugins.Descriptor{}
if isPilotEnabled(staticCfg) && hasPlugins(staticCfg) {
if hasPlugins(staticCfg) {
opts := plugins.ClientOptions{
Output: outputDir,
Token: staticCfg.Pilot.Token,
Token: getPilotToken(staticCfg),
}
var err error
@@ -79,6 +79,14 @@ func isPilotEnabled(staticCfg *static.Configuration) bool {
return staticCfg.Pilot != nil && staticCfg.Pilot.Token != ""
}
func getPilotToken(staticCfg *static.Configuration) string {
if staticCfg.Pilot == nil {
return ""
}
return staticCfg.Pilot.Token
}
func hasPlugins(staticCfg *static.Configuration) bool {
return staticCfg.Experimental != nil && len(staticCfg.Experimental.Plugins) > 0
}

View File

@@ -180,8 +180,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
tlsManager := traefiktls.NewManager()
httpChallengeProvider := acme.NewChallengeHTTP()
// we need to wait at least 2 times the ProvidersThrottleDuration to be sure to handle the challenge.
tlsChallengeProvider := acme.NewChallengeTLSALPN(time.Duration(staticConfiguration.Providers.ProvidersThrottleDuration) * 2)
tlsChallengeProvider := acme.NewChallengeTLSALPN()
err = providerAggregator.AddProvider(tlsChallengeProvider)
if err != nil {
return nil, err
@@ -216,6 +215,8 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
}
if staticConfiguration.Pilot != nil {
log.WithoutContext().Warn("Traefik Pilot is deprecated and will be removed soon. Please check our Blog for migration instructions later this year")
version.PilotEnabled = staticConfiguration.Pilot.Dashboard
}
@@ -240,6 +241,19 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
}
}
// Traefik Hub
if staticConfiguration.Hub != nil {
if err = providerAggregator.AddProvider(staticConfiguration.Hub); err != nil {
return nil, fmt.Errorf("adding Traefik Hub provider: %w", err)
}
// API is mandatory for Traefik Hub to access the dynamic configuration.
if staticConfiguration.API == nil {
staticConfiguration.API = &static.API{}
}
}
// Metrics
metricRegistries := registerMetricClients(staticConfiguration.Metrics)
@@ -265,7 +279,6 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
watcher := server.NewConfigurationWatcher(
routinesPool,
providerAggregator,
time.Duration(staticConfiguration.Providers.ProvidersThrottleDuration),
getDefaultsEntrypoints(staticConfiguration),
"internal",
)
@@ -323,7 +336,10 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
continue
}
if _, ok := resolverNames[rt.TLS.CertResolver]; !ok {
if _, ok := resolverNames[rt.TLS.CertResolver]; !ok &&
// "traefik-hub" is an allowed certificate resolver name in a Traefik Hub Experimental feature context.
// It is used to activate its own certificate resolution, even though it is not a "classical" traefik certificate resolver.
(staticConfiguration.Hub == nil || rt.TLS.CertResolver != "traefik-hub") {
log.WithoutContext().Errorf("the router %s uses a non-existent resolver: %s", rtName, rt.TLS.CertResolver)
}
}
@@ -346,6 +362,11 @@ func getHTTPChallengeHandler(acmeProviders []*acme.Provider, httpChallengeProvid
func getDefaultsEntrypoints(staticConfiguration *static.Configuration) []string {
var defaultEntryPoints []string
for name, cfg := range staticConfiguration.EntryPoints {
// Traefik Hub entryPoint should not be part of the set of default entryPoints.
if staticConfiguration.Hub != nil && staticConfiguration.Hub.EntryPoint == name {
continue
}
protocol, err := cfg.GetProtocol()
if err != nil {
// Should never happen because Traefik should not start if protocol is invalid.
@@ -450,6 +471,16 @@ func registerMetricClients(metricsConfig *types.Metrics) []metrics.Registry {
metricsConfig.InfluxDB.Address, metricsConfig.InfluxDB.PushInterval)
}
if metricsConfig.InfluxDB2 != nil {
ctx := log.With(context.Background(), log.Str(log.MetricsProviderName, "influxdb2"))
influxDB2Register := metrics.RegisterInfluxDB2(ctx, metricsConfig.InfluxDB2)
if influxDB2Register != nil {
registries = append(registries, influxDB2Register)
log.FromContext(ctx).Debugf("Configured InfluxDB v2 metrics: pushing to %s (%s org/%s bucket) once every %s",
metricsConfig.InfluxDB2.Address, metricsConfig.InfluxDB2.Org, metricsConfig.InfluxDB2.Bucket, metricsConfig.InfluxDB2.PushInterval)
}
}
return registries
}

View File

@@ -0,0 +1,20 @@
# Feature Deprecation Notices
This page is maintained and updated periodically to reflect our roadmap and any decisions around feature deprecation.
| Feature | Deprecated | End of Support | Removal |
|-------------------------------------------------------|------------|----------------|---------|
| [Pilot Dashboard (Metrics)](#pilot-dashboard-metrics) | 2.7 | 2.8 | 2.9 |
| [Pilot Plugins](#pilot-plugins) | 2.7 | 2.8 | 2.9 |
## Impact
### Pilot Dashboard (Metrics)
Metrics will continue to function normally up to 2.8, when they will be disabled.
In 2.9, the Pilot platform and all Traefik integration code will be permanently removed.
### Pilot Plugins
Starting on 2.7 the pilot token will not be a requirement anymore.
At 2.9, a new plugin catalog home should be available, decoupled from pilot.

View File

@@ -74,7 +74,7 @@ traefik --help
# or
docker run traefik[:version] --help
# ex: docker run traefik:v2.6 --help
# ex: docker run traefik:v2.7 --help
```
All available arguments can also be found [here](../reference/static-configuration/cli.md).

View File

@@ -16,7 +16,7 @@ Choose one of the [official Docker images](https://hub.docker.com/_/traefik) and
```bash
docker run -d -p 8080:8080 -p 80:80 \
-v $PWD/traefik.yml:/etc/traefik/traefik.yml traefik:v2.6
-v $PWD/traefik.yml:/etc/traefik/traefik.yml traefik:v2.7
```
For more details, go to the [Docker provider documentation](../providers/docker.md)
@@ -24,7 +24,7 @@ For more details, go to the [Docker provider documentation](../providers/docker.
!!! tip
* Prefer a fixed version than the latest that could be an unexpected version.
ex: `traefik:v2.6`
ex: `traefik:v2.7`
* Docker images are based from the [Alpine Linux Official image](https://hub.docker.com/_/alpine).
* Any orchestrator using docker images can fetch the official Traefik docker image.

View File

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

View File

@@ -275,7 +275,7 @@ Then, a [router's TLS field](../routing/routers/index.md#tls) can refer to one o
```yaml tab="K8s IngressRoute"
# The definitions below require the definitions for the TLSOption and IngressRoute kinds.
# https://doc.traefik.io/traefik/v2.6/reference/dynamic-configuration/kubernetes-crd/#definitions
# https://doc.traefik.io/traefik/v2.7/reference/dynamic-configuration/kubernetes-crd/#definitions
apiVersion: traefik.containo.us/v1alpha1
kind: TLSOption
metadata:

View File

@@ -452,3 +452,10 @@ the value for the method label becomes `EXTENSION_METHOD`, instead of the reques
### Tracing
In `v2.6.1`, the Datadog tags added to a span changed from `service.name` to `traefik.service.name` and from `router.name` to `traefik.router.name`.
## v2.7
### Traefik Pilot
In `v2.7`, the `pilot.token` and `pilot.dashboard` options are deprecated.
Please check the [feature deprecation page](../deprecation/features.md) and our Blog for migration instructions later this year.

View File

@@ -247,7 +247,7 @@ version: "3.7"
services:
traefik:
image: traefik:v2.6
image: traefik:v2.7
environment:
- TZ=US/Alaska
command:

View File

@@ -0,0 +1,219 @@
# InfluxDB v2
To enable the InfluxDB2:
```yaml tab="File (YAML)"
metrics:
influxDB2: {}
```
```toml tab="File (TOML)"
[metrics]
[metrics.influxDB2]
```
```bash tab="CLI"
--metrics.influxdb2=true
```
#### `address`
_Required, Default="http://localhost:8086"_
Address of the InfluxDB v2 instance.
```yaml tab="File (YAML)"
metrics:
influxDB2:
address: http://localhost:8086
```
```toml tab="File (TOML)"
[metrics]
[metrics.influxDB2]
address = "http://localhost:8086"
```
```bash tab="CLI"
--metrics.influxdb2.address=http://localhost:8086
```
#### `token`
_Required, Default=""_
Token with which to connect to InfluxDB v2.
```yaml tab="File (YAML)"
metrics:
influxDB2:
token: secret
```
```toml tab="File (TOML)"
[metrics]
[metrics.influxDB2]
token = "secret"
```
```bash tab="CLI"
--metrics.influxdb2.token=secret
```
#### `org`
_Required, Default=""_
Organisation where metrics will be stored.
```yaml tab="File (YAML)"
metrics:
influxDB2:
org: my-org
```
```toml tab="File (TOML)"
[metrics]
[metrics.influxDB2]
org = "my-org"
```
```bash tab="CLI"
--metrics.influxdb2.org=my-org
```
#### `bucket`
_Required, Default=""_
Bucket where metrics will be stored.
```yaml tab="File (YAML)"
metrics:
influxDB2:
bucket: my-bucket
```
```toml tab="File (TOML)"
[metrics]
[metrics.influxDB2]
bucket = "my-bucket"
```
```bash tab="CLI"
--metrics.influxdb2.bucket=my-bucket
```
#### `addEntryPointsLabels`
_Optional, Default=true_
Enable metrics on entry points.
```yaml tab="File (YAML)"
metrics:
influxDB2:
addEntryPointsLabels: true
```
```toml tab="File (TOML)"
[metrics]
[metrics.influxDB2]
addEntryPointsLabels = true
```
```bash tab="CLI"
--metrics.influxdb2.addEntryPointsLabels=true
```
#### `addRoutersLabels`
_Optional, Default=false_
Enable metrics on routers.
```yaml tab="File (YAML)"
metrics:
influxDB2:
addRoutersLabels: true
```
```toml tab="File (TOML)"
[metrics]
[metrics.influxDB2]
addRoutersLabels = true
```
```bash tab="CLI"
--metrics.influxdb2.addrouterslabels=true
```
#### `addServicesLabels`
_Optional, Default=true_
Enable metrics on services.
```yaml tab="File (YAML)"
metrics:
influxDB2:
addServicesLabels: true
```
```toml tab="File (TOML)"
[metrics]
[metrics.influxDB2]
addServicesLabels = true
```
```bash tab="CLI"
--metrics.influxdb2.addServicesLabels=true
```
#### `pushInterval`
_Optional, Default=10s_
The interval used by the exporter to push metrics to InfluxDB server.
```yaml tab="File (YAML)"
metrics:
influxDB2:
pushInterval: 10s
```
```toml tab="File (TOML)"
[metrics]
[metrics.influxDB2]
pushInterval = "10s"
```
```bash tab="CLI"
--metrics.influxdb2.pushInterval=10s
```
#### `additionalLabels`
_Optional, Default={}_
Additional labels (InfluxDB tags) on all metrics.
```yaml tab="File (YAML)"
metrics:
influxDB2:
additionalLabels:
host: example.com
environment: production
```
```toml tab="File (TOML)"
[metrics]
[metrics.influxDB2]
[metrics.influxDB2.additionalLabels]
host = "example.com"
environment = "production"
```
```bash tab="CLI"
--metrics.influxdb2.additionallabels.host=example.com --metrics.influxdb2.additionallabels.environment=production
```

View File

@@ -4,16 +4,17 @@ Traefik supports 4 metrics backends:
- [Datadog](./datadog.md)
- [InfluxDB](./influxdb.md)
- [InfluxDB2](./influxdb2.md)
- [Prometheus](./prometheus.md)
- [StatsD](./statsd.md)
## Global Metrics
| Metric | DataDog | InfluxDB | Prometheus | StatsD |
|-------------------------------------------------------------------------|---------|----------|------------|--------|
| [Configuration reloads](#configuration-reloads) | ✓ | ✓ | ✓ | ✓ |
| [Last Configuration Reload Success](#last-configuration-reload-success) | ✓ | ✓ | ✓ | ✓ |
| [TLS certificates expiration](#tls-certificates-expiration) | ✓ | ✓ | ✓ | ✓ |
| Metric | DataDog | InfluxDB / InfluxDB2 | Prometheus | StatsD |
|-------------------------------------------------------------------------|---------|----------------------|------------|--------|
| [Configuration reloads](#configuration-reloads) | ✓ | ✓ | ✓ | ✓ |
| [Last Configuration Reload Success](#last-configuration-reload-success) | ✓ | ✓ | ✓ | ✓ |
| [TLS certificates expiration](#tls-certificates-expiration) | ✓ | ✓ | ✓ | ✓ |
### Configuration Reloads
@@ -23,7 +24,7 @@ The total count of configuration reloads.
config.reload.total
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.config.reload.total
```
@@ -44,7 +45,7 @@ The timestamp of the last configuration reload success.
config.reload.lastSuccessTimestamp
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.config.reload.lastSuccessTimestamp
```
@@ -67,7 +68,7 @@ The expiration date of certificates.
tls.certs.notAfterTimestamp
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.tls.certs.notAfterTimestamp
```
@@ -82,12 +83,12 @@ traefik_tls_certs_not_after
## EntryPoint Metrics
| Metric | DataDog | InfluxDB | Prometheus | StatsD |
|-----------------------------------------------------------|---------|----------|------------|--------|
| [HTTP Requests Count](#http-requests-count) | ✓ | ✓ | ✓ | ✓ |
| [HTTPS Requests Count](#https-requests-count) | ✓ | ✓ | ✓ | ✓ |
| [Request Duration Histogram](#request-duration-histogram) | ✓ | ✓ | ✓ | ✓ |
| [Open Connections Count](#open-connections-count) | ✓ | ✓ | ✓ | ✓ |
| Metric | DataDog | InfluxDB / InfluxDB2 | Prometheus | StatsD |
|-----------------------------------------------------------|---------|----------------------|------------|--------|
| [HTTP Requests Count](#http-requests-count) | ✓ | ✓ | ✓ | ✓ |
| [HTTPS Requests Count](#https-requests-count) | ✓ | ✓ | ✓ | ✓ |
| [Request Duration Histogram](#request-duration-histogram) | ✓ | ✓ | ✓ | ✓ |
| [Open Connections Count](#open-connections-count) | ✓ | ✓ | ✓ | ✓ |
### HTTP Requests Count
@@ -99,7 +100,7 @@ The total count of HTTP requests received by an entrypoint.
entrypoint.request.total
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.entrypoint.requests.total
```
@@ -122,7 +123,7 @@ The total count of HTTPS requests received by an entrypoint.
entrypoint.request.tls.total
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.entrypoint.requests.tls.total
```
@@ -145,7 +146,7 @@ Request processing duration histogram on an entrypoint.
entrypoint.request.duration
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.entrypoint.request.duration
```
@@ -168,7 +169,7 @@ The current count of open connections on an entrypoint.
entrypoint.connections.open
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.entrypoint.connections.open
```
@@ -183,12 +184,12 @@ traefik_entrypoint_open_connections
## Router Metrics
| Metric | DataDog | InfluxDB | Prometheus | StatsD |
|-------------------------------------------------------------|---------|----------|------------|--------|
| [HTTP Requests Count](#http-requests-count_1) | ✓ | ✓ | ✓ | ✓ |
| [HTTPS Requests Count](#https-requests-count_1) | ✓ | ✓ | ✓ | ✓ |
| [Request Duration Histogram](#request-duration-histogram_1) | ✓ | ✓ | ✓ | ✓ |
| [Open Connections Count](#open-connections-count_1) | ✓ | ✓ | ✓ | ✓ |
| Metric | DataDog | InfluxDB / InfluxDB2 | Prometheus | StatsD |
|-------------------------------------------------------------|---------|----------------------|------------|--------|
| [HTTP Requests Count](#http-requests-count_1) | ✓ | ✓ | ✓ | ✓ |
| [HTTPS Requests Count](#https-requests-count_1) | ✓ | ✓ | ✓ | ✓ |
| [Request Duration Histogram](#request-duration-histogram_1) | ✓ | ✓ | ✓ | ✓ |
| [Open Connections Count](#open-connections-count_1) | ✓ | ✓ | ✓ | ✓ |
### HTTP Requests Count
@@ -200,7 +201,7 @@ The total count of HTTP requests handled by a router.
router.request.total
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.router.requests.total
```
@@ -223,7 +224,7 @@ The total count of HTTPS requests handled by a router.
router.request.tls.total
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.router.requests.tls.total
```
@@ -246,7 +247,7 @@ Request processing duration histogram on a router.
router.request.duration
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.router.request.duration
```
@@ -269,7 +270,7 @@ The current count of open connections on a router.
router.connections.open
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.router.connections.open
```
@@ -284,14 +285,14 @@ traefik_router_open_connections
## Service Metrics
| Metric | DataDog | InfluxDB | Prometheus | StatsD |
|-------------------------------------------------------------|---------|----------|------------|--------|
| [HTTP Requests Count](#http-requests-count_2) | ✓ | ✓ | ✓ | ✓ |
| [HTTPS Requests Count](#https-requests-count_2) | ✓ | ✓ | ✓ | ✓ |
| [Request Duration Histogram](#request-duration-histogram_2) | ✓ | ✓ | ✓ | ✓ |
| [Open Connections Count](#open-connections-count_2) | ✓ | ✓ | ✓ | ✓ |
| [Requests Retries Count](#requests-retries-count) | ✓ | ✓ | ✓ | ✓ |
| [Service Server UP](#service-server-up) | ✓ | ✓ | ✓ | ✓ |
| Metric | DataDog | InfluxDB / InfluxDB2 | Prometheus | StatsD |
|-------------------------------------------------------------|---------|----------------------|------------|--------|
| [HTTP Requests Count](#http-requests-count_2) | ✓ | ✓ | ✓ | ✓ |
| [HTTPS Requests Count](#https-requests-count_2) | ✓ | ✓ | ✓ | ✓ |
| [Request Duration Histogram](#request-duration-histogram_2) | ✓ | ✓ | ✓ | ✓ |
| [Open Connections Count](#open-connections-count_2) | ✓ | ✓ | ✓ | ✓ |
| [Requests Retries Count](#requests-retries-count) | ✓ | ✓ | ✓ | ✓ |
| [Service Server UP](#service-server-up) | ✓ | ✓ | ✓ | ✓ |
### HTTP Requests Count
@@ -303,7 +304,7 @@ The total count of HTTP requests processed on a service.
service.request.total
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.service.requests.total
```
@@ -326,7 +327,7 @@ The total count of HTTPS requests processed on a service.
router.service.tls.total
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.service.requests.tls.total
```
@@ -349,7 +350,7 @@ Request processing duration histogram on a service.
service.request.duration
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.service.request.duration
```
@@ -372,7 +373,7 @@ The current count of open connections on a service.
service.connections.open
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.service.connections.open
```
@@ -395,7 +396,7 @@ The count of requests retries on a service.
service.retries.total
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.service.retries.total
```
@@ -418,7 +419,7 @@ Current service's server status, described by a gauge with a value of 0 for a do
service.server.up
```
```influxdb tab="InfluxDB"
```influxdb tab="InfluxDB / InfluxDB2"
traefik.service.server.up
```

View File

@@ -1,5 +1,10 @@
# Plugins and Traefik Pilot
!!! warning "Traefik Pilot Deprecation"
Traefik Pilot is deprecated and will be removed soon.
Please check our Blog for migration instructions later this year.
Traefik Pilot is a software-as-a-service (SaaS) platform that connects to Traefik to extend its capabilities.
It offers a number of features to enhance observability and control of Traefik through a global control plane and dashboard, including:

View File

@@ -721,3 +721,27 @@ providers:
--providers.consulcatalog.namespace=production
# ...
```
### `watch`
_Optional, Default=false_
When set to `true`, watches for Consul changes ([Consul watches checks](https://www.consul.io/docs/dynamic-app-config/watches#checks)).
```yaml tab="File (YAML)"
providers:
consulCatalog:
watch: true
# ...
```
```toml tab="File (TOML)"
[providers.consulCatalog]
watch = true
# ...
```
```bash tab="CLI"
--providers.consulcatalog.watch=true
# ...
```

View File

@@ -252,7 +252,7 @@ See the sections [Docker API Access](#docker-api-access) and [Docker Swarm API A
services:
traefik:
image: traefik:v2.6 # The official v2 Traefik docker image
image: traefik:v2.7 # The official v2 Traefik docker image
ports:
- "80:80"
volumes:

View File

@@ -264,11 +264,38 @@ providers:
--providers.kubernetescrd.throttleDuration=10s
```
### `allowEmptyServices`
_Optional, Default: false_
If the parameter is set to `true`,
it allows the creation of an empty [servers load balancer](../routing/services/index.md#servers-load-balancer) if the targeted Kubernetes service has no endpoints available.
With IngressRoute resources,
this results in `503` HTTP responses instead of `404` ones.
```yaml tab="File (YAML)"
providers:
kubernetesCRD:
allowEmptyServices: true
# ...
```
```toml tab="File (TOML)"
[providers.kubernetesCRD]
allowEmptyServices = true
# ...
```
```bash tab="CLI"
--providers.kubernetesCRD.allowEmptyServices=true
```
### `allowCrossNamespace`
_Optional, Default: false_
If the parameter is set to `true`, IngressRoutes are able to reference resources in other namespaces than theirs.
If the parameter is set to `true`,
IngressRoute are able to reference resources in namespaces other than theirs.
```yaml tab="File (YAML)"
providers:

View File

@@ -445,7 +445,11 @@ providers:
### `allowEmptyServices`
_Optional, Default: false
_Optional, Default: false_
If the parameter is set to `true`,
it allows the creation of an empty [servers load balancer](../routing/services/index.md#servers-load-balancer) if the targeted Kubernetes service has no endpoints available.
This results in `503` HTTP responses instead of `404` ones.
```yaml tab="File (YAML)"
providers:
@@ -464,14 +468,12 @@ providers:
--providers.kubernetesingress.allowEmptyServices=true
```
Allow the creation of services if there are no endpoints available.
This results in `503` http responses instead of `404`.
### `allowExternalNameServices`
_Optional, Default: false_
If the parameter is set to `true`, Ingresses are able to reference ExternalName services.
If the parameter is set to `true`,
Ingresses are able to reference ExternalName services.
```yaml tab="File (YAML)"
providers:

View File

@@ -165,6 +165,7 @@
- "traefik.tcp.routers.tcprouter0.entrypoints=foobar, foobar"
- "traefik.tcp.routers.tcprouter0.middlewares=foobar, foobar"
- "traefik.tcp.routers.tcprouter0.rule=foobar"
- "traefik.tcp.routers.tcprouter0.priority=42"
- "traefik.tcp.routers.tcprouter0.service=foobar"
- "traefik.tcp.routers.tcprouter0.tls=true"
- "traefik.tcp.routers.tcprouter0.tls.certresolver=foobar"
@@ -177,6 +178,7 @@
- "traefik.tcp.routers.tcprouter1.entrypoints=foobar, foobar"
- "traefik.tcp.routers.tcprouter1.middlewares=foobar, foobar"
- "traefik.tcp.routers.tcprouter1.rule=foobar"
- "traefik.tcp.routers.tcprouter1.priority=42"
- "traefik.tcp.routers.tcprouter1.service=foobar"
- "traefik.tcp.routers.tcprouter1.tls=true"
- "traefik.tcp.routers.tcprouter1.tls.certresolver=foobar"

View File

@@ -95,6 +95,12 @@
secure = true
httpOnly = true
sameSite = "foobar"
[http.services.Service04]
[http.services.Service04.failover]
service = "foobar"
fallback = "foobar"
[http.services.Service04.failover.healthCheck]
[http.middlewares]
[http.middlewares.Middleware00]
[http.middlewares.Middleware00.addPrefix]
@@ -320,6 +326,7 @@
middlewares = ["foobar", "foobar"]
service = "foobar"
rule = "foobar"
priority = 42
[tcp.routers.TCPRouter0.tls]
passthrough = true
options = "foobar"
@@ -337,6 +344,7 @@
middlewares = ["foobar", "foobar"]
service = "foobar"
rule = "foobar"
priority = 42
[tcp.routers.TCPRouter1.tls]
passthrough = true
options = "foobar"

View File

@@ -95,6 +95,11 @@ http:
secure: true
httpOnly: true
sameSite: foobar
Service04:
failover:
service: foobar
fallback: foobar
healthCheck: {}
middlewares:
Middleware00:
addPrefix:
@@ -361,6 +366,7 @@ tcp:
- foobar
service: foobar
rule: foobar
priority: 42
tls:
passthrough: true
options: foobar
@@ -383,6 +389,7 @@ tcp:
- foobar
service: foobar
rule: foobar
priority: 42
tls:
passthrough: true
options: foobar

View File

@@ -25,7 +25,7 @@ spec:
serviceAccountName: traefik-controller
containers:
- name: traefik
image: traefik:v2.6
image: traefik:v2.7
args:
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443

View File

@@ -228,12 +228,16 @@
| `traefik/http/services/Service03/weighted/sticky/cookie/name` | `foobar` |
| `traefik/http/services/Service03/weighted/sticky/cookie/sameSite` | `foobar` |
| `traefik/http/services/Service03/weighted/sticky/cookie/secure` | `true` |
| `traefik/http/services/Service04/failover/fallback` | `foobar` |
| `traefik/http/services/Service04/failover/healthCheck` | `` |
| `traefik/http/services/Service04/failover/service` | `foobar` |
| `traefik/tcp/middlewares/Middleware00/ipWhiteList/sourceRange/0` | `foobar` |
| `traefik/tcp/middlewares/Middleware00/ipWhiteList/sourceRange/1` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/entryPoints/0` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/entryPoints/1` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/middlewares/0` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/middlewares/1` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/priority` | `42` |
| `traefik/tcp/routers/TCPRouter0/rule` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/service` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/tls/certResolver` | `foobar` |
@@ -249,6 +253,7 @@
| `traefik/tcp/routers/TCPRouter1/entryPoints/1` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/middlewares/0` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/middlewares/1` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/priority` | `42` |
| `traefik/tcp/routers/TCPRouter1/rule` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/service` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/tls/certResolver` | `foobar` |

View File

@@ -163,6 +163,7 @@
"traefik.http.services.service01.loadbalancer.serverstransport": "foobar",
"traefik.tcp.routers.tcprouter0.entrypoints": "foobar, foobar",
"traefik.tcp.routers.tcprouter0.rule": "foobar",
"traefik.tcp.routers.tcprouter0.priority": "42",
"traefik.tcp.routers.tcprouter0.service": "foobar",
"traefik.tcp.routers.tcprouter0.tls": "true",
"traefik.tcp.routers.tcprouter0.tls.certresolver": "foobar",
@@ -174,6 +175,7 @@
"traefik.tcp.routers.tcprouter0.tls.passthrough": "true",
"traefik.tcp.routers.tcprouter1.entrypoints": "foobar, foobar",
"traefik.tcp.routers.tcprouter1.rule": "foobar",
"traefik.tcp.routers.tcprouter1.priority": "42",
"traefik.tcp.routers.tcprouter1.service": "foobar",
"traefik.tcp.routers.tcprouter1.tls": "true",
"traefik.tcp.routers.tcprouter1.tls.certresolver": "foobar",

View File

@@ -62,6 +62,8 @@ spec:
- name
type: object
type: array
priority:
type: integer
services:
items:
description: ServiceTCP defines an upstream to proxy traffic.

View File

@@ -183,6 +183,9 @@ Timeout defines how long to wait on an idle session before releasing the related
`--experimental.http3`:
Enable HTTP3. (Default: ```false```)
`--experimental.hub`:
Enable the Traefik Hub provider. (Default: ```false```)
`--experimental.kubernetesgateway`:
Allow the Kubernetes gateway api provider usage. (Default: ```false```)
@@ -216,6 +219,24 @@ resolv.conf used for DNS resolving (Default: ```/etc/resolv.conf```)
`--hostresolver.resolvdepth`:
The maximal depth of DNS recursive resolving (Default: ```5```)
`--hub`:
Traefik Hub configuration. (Default: ```false```)
`--hub.entrypoint`:
Entrypoint that exposes data for Traefik Hub. It should be a dedicated one, and not used by any router. (Default: ```traefik-hub```)
`--hub.tls.ca`:
The certificate authority authenticates the Traefik Hub Agent certificate.
`--hub.tls.cert`:
The TLS certificate for Traefik Proxy as a TLS client.
`--hub.tls.insecure`:
Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent. (Default: ```false```)
`--hub.tls.key`:
The TLS key for Traefik Proxy as a TLS client.
`--log`:
Traefik log settings. (Default: ```false```)
@@ -285,6 +306,36 @@ InfluxDB retention policy used when protocol is http.
`--metrics.influxdb.username`:
InfluxDB username (only with http).
`--metrics.influxdb2`:
InfluxDB v2 metrics exporter type. (Default: ```false```)
`--metrics.influxdb2.addentrypointslabels`:
Enable metrics on entry points. (Default: ```true```)
`--metrics.influxdb2.additionallabels.<name>`:
Additional labels (influxdb tags) on all metrics
`--metrics.influxdb2.address`:
InfluxDB v2 address. (Default: ```http://localhost:8086```)
`--metrics.influxdb2.addrouterslabels`:
Enable metrics on routers. (Default: ```false```)
`--metrics.influxdb2.addserviceslabels`:
Enable metrics on services. (Default: ```true```)
`--metrics.influxdb2.bucket`:
InfluxDB v2 bucket ID.
`--metrics.influxdb2.org`:
InfluxDB v2 org ID.
`--metrics.influxdb2.pushinterval`:
InfluxDB v2 push interval. (Default: ```10```)
`--metrics.influxdb2.token`:
InfluxDB v2 access token.
`--metrics.prometheus`:
Prometheus metrics exporter type. (Default: ```false```)
@@ -456,6 +507,9 @@ Name of the Traefik service in Consul Catalog (needs to be registered via the or
`--providers.consulcatalog.stale`:
Use stale consistency for catalog reads. (Default: ```false```)
`--providers.consulcatalog.watch`:
Watch Consul API events. (Default: ```false```)
`--providers.docker`:
Enable Docker backend with default settings. (Default: ```false```)
@@ -615,6 +669,9 @@ Enable Kubernetes backend with default settings. (Default: ```false```)
`--providers.kubernetescrd.allowcrossnamespace`:
Allow cross namespace resource reference. (Default: ```false```)
`--providers.kubernetescrd.allowemptyservices`:
Allow the creation of services without endpoints. (Default: ```false```)
`--providers.kubernetescrd.allowexternalnameservices`:
Allow ExternalName services. (Default: ```false```)

View File

@@ -183,6 +183,9 @@ Timeout defines how long to wait on an idle session before releasing the related
`TRAEFIK_EXPERIMENTAL_HTTP3`:
Enable HTTP3. (Default: ```false```)
`TRAEFIK_EXPERIMENTAL_HUB`:
Enable the Traefik Hub provider. (Default: ```false```)
`TRAEFIK_EXPERIMENTAL_KUBERNETESGATEWAY`:
Allow the Kubernetes gateway api provider usage. (Default: ```false```)
@@ -216,6 +219,24 @@ resolv.conf used for DNS resolving (Default: ```/etc/resolv.conf```)
`TRAEFIK_HOSTRESOLVER_RESOLVDEPTH`:
The maximal depth of DNS recursive resolving (Default: ```5```)
`TRAEFIK_HUB`:
Traefik Hub configuration. (Default: ```false```)
`TRAEFIK_HUB_ENTRYPOINT`:
Entrypoint that exposes data for Traefik Hub. It should be a dedicated one, and not used by any router. (Default: ```traefik-hub```)
`TRAEFIK_HUB_TLS_CA`:
The certificate authority authenticates the Traefik Hub Agent certificate.
`TRAEFIK_HUB_TLS_CERT`:
The TLS certificate for Traefik Proxy as a TLS client.
`TRAEFIK_HUB_TLS_INSECURE`:
Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent. (Default: ```false```)
`TRAEFIK_HUB_TLS_KEY`:
The TLS key for Traefik Proxy as a TLS client.
`TRAEFIK_LOG`:
Traefik log settings. (Default: ```false```)
@@ -252,6 +273,36 @@ Datadog push interval. (Default: ```10```)
`TRAEFIK_METRICS_INFLUXDB`:
InfluxDB metrics exporter type. (Default: ```false```)
`TRAEFIK_METRICS_INFLUXDB2`:
InfluxDB v2 metrics exporter type. (Default: ```false```)
`TRAEFIK_METRICS_INFLUXDB2_ADDENTRYPOINTSLABELS`:
Enable metrics on entry points. (Default: ```true```)
`TRAEFIK_METRICS_INFLUXDB2_ADDITIONALLABELS_<NAME>`:
Additional labels (influxdb tags) on all metrics
`TRAEFIK_METRICS_INFLUXDB2_ADDRESS`:
InfluxDB v2 address. (Default: ```http://localhost:8086```)
`TRAEFIK_METRICS_INFLUXDB2_ADDROUTERSLABELS`:
Enable metrics on routers. (Default: ```false```)
`TRAEFIK_METRICS_INFLUXDB2_ADDSERVICESLABELS`:
Enable metrics on services. (Default: ```true```)
`TRAEFIK_METRICS_INFLUXDB2_BUCKET`:
InfluxDB v2 bucket ID.
`TRAEFIK_METRICS_INFLUXDB2_ORG`:
InfluxDB v2 org ID.
`TRAEFIK_METRICS_INFLUXDB2_PUSHINTERVAL`:
InfluxDB v2 push interval. (Default: ```10```)
`TRAEFIK_METRICS_INFLUXDB2_TOKEN`:
InfluxDB v2 access token.
`TRAEFIK_METRICS_INFLUXDB_ADDENTRYPOINTSLABELS`:
Enable metrics on entry points. (Default: ```true```)
@@ -423,6 +474,9 @@ Name of the Traefik service in Consul Catalog (needs to be registered via the or
`TRAEFIK_PROVIDERS_CONSULCATALOG_STALE`:
Use stale consistency for catalog reads. (Default: ```false```)
`TRAEFIK_PROVIDERS_CONSULCATALOG_WATCH`:
Watch Consul API events. (Default: ```false```)
`TRAEFIK_PROVIDERS_CONSUL_ENDPOINTS`:
KV store endpoints (Default: ```127.0.0.1:8500```)
@@ -615,6 +669,9 @@ Enable Kubernetes backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWCROSSNAMESPACE`:
Allow cross namespace resource reference. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWEMPTYSERVICES`:
Allow the creation of services without endpoints. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWEXTERNALNAMESERVICES`:
Allow ExternalName services. (Default: ```false```)

View File

@@ -121,6 +121,7 @@
labelSelector = "foobar"
ingressClass = "foobar"
throttleDuration = 42
allowEmptyServices = true
[providers.kubernetesGateway]
endpoint = "foobar"
token = "foobar"
@@ -149,6 +150,7 @@
exposedByDefault = true
defaultRule = "foobar"
namespace = "foobar"
watch = true
[providers.consulCatalog.endpoint]
address = "foobar"
scheme = "foobar"
@@ -280,6 +282,17 @@
addServicesLabels = true
[metrics.influxDB.additionalLabels]
foobar = "foobar"
[metrics.influxDB2]
address = "foobar"
token = "foobar"
pushInterval = "42s"
org = "foobar"
bucket = "foobar"
addEntryPointsLabels = true
addRoutersLabels = true
addServicesLabels = true
[metrics.influxDB2.additionalLabels]
foobar = "foobar"
[ping]
entryPoint = "foobar"
@@ -406,9 +419,18 @@
token = "foobar"
dashboard = true
[hub]
entrypoint = "foobar"
[hub.tls]
insecure = true
ca = "foobar"
cert = "foobar"
key = "foobar"
[experimental]
kubernetesGateway = true
http3 = true
hub = true
[experimental.plugins]
[experimental.plugins.Descriptor0]
moduleName = "foobar"

View File

@@ -131,6 +131,7 @@ providers:
labelSelector: foobar
ingressClass: foobar
throttleDuration: 42s
allowEmptyServices: true
kubernetesGateway:
endpoint: foobar
token: foobar
@@ -161,6 +162,7 @@ providers:
exposedByDefault: true
defaultRule: foobar
namespace: foobar
watch: true
endpoint:
address: foobar
scheme: foobar
@@ -302,6 +304,18 @@ metrics:
addServicesLabels: true
additionalLabels:
foobar: foobar
influxDB2:
address: foobar
token: foobar
pushInterval: 42s
org: foobar
bucket: foobar
addEntryPointsLabels: true
addRoutersLabels: true
addServicesLabels: true
additionalLabels:
foobar: foobar
ping:
entryPoint: foobar
manualRouting: true
@@ -426,9 +440,17 @@ certificatesResolvers:
pilot:
token: foobar
dashboard: true
hub:
entrypoint: foobar
tls:
insecure: true
ca: foobar
cert: foobar
key: foobar
experimental:
kubernetesGateway: true
http3: true
hub: true
plugins:
Descriptor0:
moduleName: foobar

View File

@@ -99,7 +99,8 @@ For example, to change the rule, you could add the tag ```traefik.http.routers.m
```
??? info "`traefik.http.routers.<router_name>.priority`"
<!-- TODO doc priority in routers page -->
See [priority](../routers/index.md#priority) for more information.
```yaml
traefik.http.routers.myrouter.priority=42

View File

@@ -538,6 +538,14 @@ You can declare TCP Routers and/or Services using labels.
- "traefik.tcp.routers.mytcprouter.tls.passthrough=true"
```
??? info "`traefik.tcp.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority_1) for more information.
```yaml
- "traefik.tcp.routers.myrouter.priority=42"
```
#### TCP Services
??? info "`traefik.tcp.services.<service_name>.loadbalancer.server.port`"

View File

@@ -379,6 +379,14 @@ You can declare TCP Routers and/or Services using labels.
traefik.tcp.routers.mytcprouter.tls.passthrough=true
```
??? info "`traefik.tcp.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority_1) for more information.
```yaml
traefik.tcp.routers.myrouter.priority=42
```
#### TCP Services
??? info "`traefik.tcp.services.<service_name>.loadbalancer.server.port`"

View File

@@ -43,7 +43,7 @@ The Kubernetes Ingress Controller, The Custom Resource Way.
serviceAccountName: traefik-ingress-controller
containers:
- name: traefik
image: traefik:v2.6
image: traefik:v2.7
args:
- --log.level=DEBUG
- --api
@@ -357,27 +357,27 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne
- b.example.net
```
| Ref | Attribute | Purpose |
|------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [1] | `entryPoints` | List of [entry points](../routers/index.md#entrypoints) names |
| [2] | `routes` | List of routes |
| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule) corresponding to an underlying router. |
| [4] | `routes[n].priority` | [Disambiguate](../routers/index.md#priority) rules of the same length, for route matching |
| [5] | `routes[n].middlewares` | List of reference to [Middleware](#kind-middleware) |
| [6] | `middlewares[n].name` | Defines the [Middleware](#kind-middleware) name |
| [7] | `middlewares[n].namespace` | Defines the [Middleware](#kind-middleware) namespace |
| [8] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) (See below for `ExternalName Service` setup) |
| [9] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. |
| Ref | Attribute | Purpose |
|------|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [1] | `entryPoints` | List of [entry points](../routers/index.md#entrypoints) names |
| [2] | `routes` | List of routes |
| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule) corresponding to an underlying router. |
| [4] | `routes[n].priority` | Defines the [priority](../routers/index.md#priority) to disambiguate rules of the same length, for route matching |
| [5] | `routes[n].middlewares` | List of reference to [Middleware](#kind-middleware) |
| [6] | `middlewares[n].name` | Defines the [Middleware](#kind-middleware) name |
| [7] | `middlewares[n].namespace` | Defines the [Middleware](#kind-middleware) namespace |
| [8] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) (See below for `ExternalName Service` setup) |
| [9] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. |
| [10] | `services[n].serversTransport` | Defines the reference to a [ServersTransport](#kind-serverstransport). The ServersTransport namespace is assumed to be the [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) namespace (see [ServersTransport reference](#serverstransport-reference)). |
| [11] | `tls` | Defines [TLS](../routers/index.md#tls) certificate configuration |
| [12] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) |
| [13] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) |
| [14] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name |
| [15] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace |
| [16] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver) |
| [17] | `tls.domains` | List of [domains](../routers/index.md#domains) |
| [18] | `domains[n].main` | Defines the main domain name |
| [19] | `domains[n].sans` | List of SANs (alternative domains) |
| [11] | `tls` | Defines [TLS](../routers/index.md#tls) certificate configuration |
| [12] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) |
| [13] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) |
| [14] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name |
| [15] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace |
| [16] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver) |
| [17] | `tls.domains` | List of [domains](../routers/index.md#domains) |
| [18] | `domains[n].main` | Defines the main domain name |
| [19] | `domains[n].sans` | List of SANs (alternative domains) |
??? example "Declaring an IngressRoute"
@@ -1088,54 +1088,56 @@ Register the `IngressRouteTCP` [kind](../../reference/dynamic-configuration/kube
- footcp
routes: # [2]
- match: HostSNI(`*`) # [3]
priority: 10 # [4]
middlewares:
- name: middleware1 # [4]
namespace: default # [5]
services: # [6]
- name: foo # [7]
port: 8080 # [8]
weight: 10 # [9]
terminationDelay: 400 # [10]
proxyProtocol: # [11]
version: 1 # [12]
tls: # [13]
secretName: supersecret # [14]
options: # [15]
name: opt # [16]
namespace: default # [17]
certResolver: foo # [18]
domains: # [19]
- main: example.net # [20]
sans: # [21]
- name: middleware1 # [5]
namespace: default # [6]
services: # [7]
- name: foo # [8]
port: 8080 # [9]
weight: 10 # [10]
terminationDelay: 400 # [11]
proxyProtocol: # [12]
version: 1 # [13]
tls: # [14]
secretName: supersecret # [15]
options: # [16]
name: opt # [17]
namespace: default # [18]
certResolver: foo # [19]
domains: # [20]
- main: example.net # [21]
sans: # [22]
- a.example.net
- b.example.net
passthrough: false # [22]
passthrough: false # [23]
```
| Ref | Attribute | Purpose |
|------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [1] | `entryPoints` | List of [entrypoints](../routers/index.md#entrypoints_1) names |
| [2] | `routes` | List of routes |
| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule_1) corresponding to an underlying router |
| [4] | `middlewares[n].name` | Defines the [MiddlewareTCP](#kind-middlewaretcp) name |
| [5] | `middlewares[n].namespace` | Defines the [MiddlewareTCP](#kind-middlewaretcp) namespace |
| [6] | `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions (See below for `ExternalName Service` setup) |
| [7] | `services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) |
| [8] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. |
| [9] | `services[n].weight` | Defines the weight to apply to the server load balancing |
| [10] | `services[n].terminationDelay` | corresponds to the deadline that the proxy sets, after one of its connected peers indicates it has closed the writing capability of its connection, to close the reading capability as well, hence fully terminating the connection. It is a duration in milliseconds, defaulting to 100. A negative value means an infinite deadline (i.e. the reading capability is never closed). |
| [11] | `proxyProtocol` | Defines the [PROXY protocol](../services/index.md#proxy-protocol) configuration |
| [12] | `version` | Defines the [PROXY protocol](../services/index.md#proxy-protocol) version |
| [13] | `tls` | Defines [TLS](../routers/index.md#tls_1) certificate configuration |
| [14] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) |
| [15] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) |
| [16] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name |
| [17] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace |
| [18] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver_1) |
| [19] | `tls.domains` | List of [domains](../routers/index.md#domains_1) |
| [20] | `domains[n].main` | Defines the main domain name |
| [21] | `domains[n].sans` | List of SANs (alternative domains) |
| [22] | `tls.passthrough` | If `true`, delegates the TLS termination to the backend |
| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule_1) of the underlying router |
| [4] | `routes[n].priority` | Defines the [priority](../routers/index.md#priority_1) to disambiguate rules of the same length, for route matching |
| [5] | `middlewares[n].name` | Defines the [MiddlewareTCP](#kind-middlewaretcp) name |
| [6] | `middlewares[n].namespace` | Defines the [MiddlewareTCP](#kind-middlewaretcp) namespace |
| [7] | `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions (See below for `ExternalName Service` setup) |
| [8] | `services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) |
| [9] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. |
| [10] | `services[n].weight` | Defines the weight to apply to the server load balancing |
| [11] | `services[n].terminationDelay` | corresponds to the deadline that the proxy sets, after one of its connected peers indicates it has closed the writing capability of its connection, to close the reading capability as well, hence fully terminating the connection. It is a duration in milliseconds, defaulting to 100. A negative value means an infinite deadline (i.e. the reading capability is never closed). |
| [12] | `proxyProtocol` | Defines the [PROXY protocol](../services/index.md#proxy-protocol) configuration |
| [13] | `version` | Defines the [PROXY protocol](../services/index.md#proxy-protocol) version |
| [14] | `tls` | Defines [TLS](../routers/index.md#tls_1) certificate configuration |
| [15] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) |
| [16] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) |
| [17] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name |
| [18] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace |
| [19] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver_1) |
| [20] | `tls.domains` | List of [domains](../routers/index.md#domains_1) |
| [21] | `domains[n].main` | Defines the main domain name |
| [22] | `domains[n].sans` | List of SANs (alternative domains) |
| [23] | `tls.passthrough` | If `true`, delegates the TLS termination to the backend |
??? example "Declaring an IngressRouteTCP"
@@ -1151,6 +1153,7 @@ Register the `IngressRouteTCP` [kind](../../reference/dynamic-configuration/kube
routes:
# Match is the rule corresponding to an underlying router.
- match: HostSNI(`*`)
priority: 10
services:
- name: foo
port: 8080

View File

@@ -141,7 +141,7 @@ which in turn will create the resulting routers, services, handlers, etc.
serviceAccountName: traefik-ingress-controller
containers:
- name: traefik
image: traefik:v2.6
image: traefik:v2.7
args:
- --entrypoints.web.address=:80
- --providers.kubernetesingress
@@ -532,7 +532,7 @@ This way, any Ingress attached to this Entrypoint will have TLS termination by d
serviceAccountName: traefik-ingress-controller
containers:
- name: traefik
image: traefik:v2.6
image: traefik:v2.7
args:
- --entrypoints.websecure.address=:443
- --entrypoints.websecure.http.tls
@@ -741,7 +741,7 @@ For more options, please refer to the available [annotations](#on-ingress).
serviceAccountName: traefik-ingress-controller
containers:
- name: traefik
image: traefik:v2.6
image: traefik:v2.7
args:
- --entrypoints.websecure.address=:443
- --providers.kubernetesingress

View File

@@ -366,7 +366,6 @@ You can declare TCP Routers and/or Services using KV.
| Key (Path) | Value |
|-----------------------------------------------|----------|
| `traefik/tcp/routers/mytcprouter/tls/options` | `foobar` |
??? info "`traefik/tcp/routers/<router_name>/tls/passthrough`"
@@ -376,6 +375,14 @@ You can declare TCP Routers and/or Services using KV.
|---------------------------------------------------|--------|
| `traefik/tcp/routers/mytcprouter/tls/passthrough` | `true` |
??? info "`traefik/tcp/routers/<router_name>/priority`"
See [priority](../routers/index.md#priority_1) for more information.
| Key (Path) | Value |
|------------------------------------------|-------|
| `traefik/tcp/routers/myrouter/priority` | `42` |
#### TCP Services
??? info "`traefik/tcp/services/<service_name>/loadbalancer/servers/<n>/url`"

View File

@@ -412,6 +412,14 @@ You can declare TCP Routers and/or Services using labels.
"traefik.tcp.routers.mytcprouter.tls.passthrough": "true"
```
??? info "`traefik.tcp.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority_1) for more information.
```json
"traefik.tcp.routers.myrouter.priority": "42"
```
#### TCP Services
??? info "`traefik.tcp.services.<service_name>.loadbalancer.server.port`"

View File

@@ -415,6 +415,14 @@ You can declare TCP Routers and/or Services using labels.
- "traefik.tcp.routers.mytcprouter.tls.passthrough=true"
```
??? info "`traefik.tcp.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority_1) for more information.
```yaml
- "traefik.tcp.routers.myrouter.priority=42"
```
#### TCP Services
??? info "`traefik.tcp.services.<service_name>.loadbalancer.server.port`"

View File

@@ -212,7 +212,7 @@ If the rule is verified, the router becomes active, calls middlewares, and then
??? tip "Backticks or Quotes?"
To set the value of a rule, use [backticks](https://en.wiktionary.org/wiki/backtick) ``` ` ``` or escaped double-quotes `\"`.
Single quotes `'` are not accepted as values are [Golang's String Literals](https://golang.org/ref/spec#String_literals).
Single quotes `'` are not accepted since the values are [Golang's String Literals](https://golang.org/ref/spec#String_literals).
!!! example "Host is example.com"
@@ -257,11 +257,12 @@ The table below lists all the available matchers:
!!! info "Combining Matchers Using Operators and Parenthesis"
You can combine multiple matchers using the AND (`&&`) and OR (`||`) operators. You can also use parenthesis.
The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
as well as parentheses.
!!! info "Invert a matcher"
!!! info "Inverting a matcher"
You can invert a matcher by using the `!` operator.
One can invert a matcher by using the `!` operator.
!!! important "Rule, Middleware, and Services"
@@ -795,20 +796,147 @@ If you want to limit the router scope to a set of entry points, set the entry po
### Rule
| Rule | Description |
|--------------------------------|-------------------------------------------------------------------------|
| ```HostSNI(`domain-1`, ...)``` | Check if the Server Name Indication corresponds to the given `domains`. |
Rules are a set of matchers configured with values, that determine if a particular request matches specific criteria.
If the rule is verified, the router becomes active, calls middlewares, and then forwards the request to the service.
??? tip "Backticks or Quotes?"
To set the value of a rule, use [backticks](https://en.wiktionary.org/wiki/backtick) ``` ` ``` or escaped double-quotes `\"`.
Single quotes `'` are not accepted since the values are [Golang's String Literals](https://golang.org/ref/spec#String_literals).
!!! example "HostSNI is example.com"
```toml
rule = "HostSNI(`example.com`)"
```
!!! example "HostSNI is example.com OR HostSNI is example.org AND ClientIP is 0.0.0.0"
```toml
rule = "HostSNI(`example.com`) || (HostSNI(`example.org`) && ClientIP(`0.0.0.0`))"
```
The table below lists all the available matchers:
| Rule | Description |
|---------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|
| ```HostSNI(`domain-1`, ...)``` | Check if the Server Name Indication corresponds to the given `domains`. |
| ```HostSNIRegexp(`example.com`, `{subdomain:[a-z]+}.example.com`, ...)``` | Check if the Server Name Indication matches the given regular expressions. See "Regexp Syntax" below. |
| ```ClientIP(`10.0.0.0/16`, `::1`)``` | Check if the request client IP is one of the given IP/CIDR. It accepts IPv4, IPv6 and CIDR formats. |
!!! important "Non-ASCII Domain Names"
Non-ASCII characters are not supported in the `HostSNI` expression, and by doing so the associated TCP router will be invalid.
Non-ASCII characters are not supported in the `HostSNI` and `HostSNIRegexp` expressions, and so using them would invalidate the associated TCP router.
Domain names containing non-ASCII characters must be provided as punycode encoded values ([rfc 3492](https://tools.ietf.org/html/rfc3492)).
!!! important "Regexp Syntax"
`HostSNIRegexp` accepts an expression with zero or more groups enclosed by curly braces, which are called named regexps.
Named regexps, of the form `{name:regexp}`, are the only expressions considered for regexp matching.
The regexp name (`name` in the above example) is an arbitrary value, that exists only for historical reasons.
Any `regexp` supported by [Go's regexp package](https://golang.org/pkg/regexp/) may be used.
!!! important "HostSNI & TLS"
It is important to note that the Server Name Indication is an extension of the TLS protocol.
Hence, only TLS routers will be able to specify a domain name with that rule.
However, non-TLS routers will have to explicitly use that rule with `*` (every domain) to state that every non-TLS request will be handled by the router.
However, there is one special use case for HostSNI with non-TLS routers:
when one wants a non-TLS router that matches all (non-TLS) requests,
one should use the specific `HostSNI(*)` syntax.
!!! info "Combining Matchers Using Operators and Parenthesis"
The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
as well as parentheses.
!!! info "Inverting a matcher"
One can invert a matcher by using the `!` operator.
!!! important "Rule, Middleware, and Services"
The rule is evaluated "before" any middleware has the opportunity to work, and "before" the request is forwarded to the service.
### Priority
To avoid path overlap, routes are sorted, by default, in descending order using rules length.
The priority is directly equal to the length of the rule, and so the longest length has the highest priority.
A value of `0` for the priority is ignored: `priority = 0` means that the default rules length sorting is used.
??? info "How default priorities are computed"
```yaml tab="File (YAML)"
## Dynamic configuration
tcp:
routers:
Router-1:
rule: "ClientIP(`192.168.0.12`)"
# ...
Router-2:
rule: "ClientIP(`192.168.0.0/24`)"
# ...
```
```toml tab="File (TOML)"
## Dynamic configuration
[tcp.routers]
[tcp.routers.Router-1]
rule = "ClientIP(`192.168.0.12`)"
# ...
[tcp.routers.Router-2]
rule = "ClientIP(`192.168.0.0/24`)"
# ...
```
The table below shows that `Router-2` has a higher computed priority than `Router-1`.
| Name | Rule | Priority |
|----------|-------------------------------------------------------------|----------|
| Router-1 | ```ClientIP(`192.168.0.12`)``` | 24 |
| Router-2 | ```ClientIP(`192.168.0.0/24`)``` | 26 |
Which means that requests from `192.168.0.12` would go to Router-2 even though Router-1 is intended to specifically handle them.
To achieve this intention, a priority (higher than 26) should be set on Router-1.
??? example "Setting priorities -- using the [File Provider](../../providers/file.md)"
```yaml tab="File (YAML)"
## Dynamic configuration
tcp:
routers:
Router-1:
rule: "ClientIP(`192.168.0.12`)"
entryPoints:
- "web"
service: service-1
priority: 2
Router-2:
rule: "ClientIP(`192.168.0.0/24`)"
entryPoints:
- "web"
priority: 1
service: service-2
```
```toml tab="File (TOML)"
## Dynamic configuration
[tcp.routers]
[tcp.routers.Router-1]
rule = "ClientIP(`192.168.0.12`)"
entryPoints = ["web"]
service = "service-1"
priority = 2
[tcp.routers.Router-2]
rule = "ClientIP(`192.168.0.0/24`)"
entryPoints = ["web"]
priority = 1
service = "service-2"
```
In this configuration, the priority is configured so that `Router-1` will handle requests from `192.168.0.12`.
### Middlewares

View File

@@ -1212,6 +1212,139 @@ http:
url = "http://private-ip-server-2/"
```
### Failover (service)
A failover service job is to forward all requests to a fallback service when the main service becomes unreachable.
!!! info "Relation to HealthCheck"
The failover service relies on the HealthCheck system to get notified when its main service becomes unreachable,
which means HealthCheck needs to be enabled and functional on the main service.
However, HealthCheck does not need to be enabled on the failover service itself for it to be functional.
It is only required in order to propagate upwards the information when the failover itself becomes down
(i.e. both its main and its fallback are down too).
!!! info "Supported Providers"
This strategy can currently only be defined with the [File](../../providers/file.md) provider.
```yaml tab="YAML"
## Dynamic configuration
http:
services:
app:
failover:
service: main
fallback: backup
main:
loadBalancer:
healthCheck:
path: /status
interval: 10s
timeout: 3s
servers:
- url: "http://private-ip-server-1/"
backup:
loadBalancer:
servers:
- url: "http://private-ip-server-2/"
```
```toml tab="TOML"
## Dynamic configuration
[http.services]
[http.services.app]
[http.services.app.failover]
service = "main"
fallback = "backup"
[http.services.main]
[http.services.main.loadBalancer]
[http.services.main.loadBalancer.healthCheck]
path = "/health"
interval = "10s"
timeout = "3s"
[[http.services.main.loadBalancer.servers]]
url = "http://private-ip-server-1/"
[http.services.backup]
[http.services.backup.loadBalancer]
[[http.services.backup.loadBalancer.servers]]
url = "http://private-ip-server-2/"
```
#### Health Check
HealthCheck enables automatic self-healthcheck for this service,
i.e. if the main and the fallback services become unreachable,
the information is propagated upwards to its parent.
!!! info "All or nothing"
If HealthCheck is enabled for a given service, but any of its descendants does
not have it enabled, the creation of the service will fail.
HealthCheck on a Failover service can currently only be defined with the [File](../../providers/file.md) provider.
```yaml tab="YAML"
## Dynamic configuration
http:
services:
app:
failover:
healthCheck: {}
service: main
fallback: backup
main:
loadBalancer:
healthCheck:
path: /status
interval: 10s
timeout: 3s
servers:
- url: "http://private-ip-server-1/"
backup:
loadBalancer:
healthCheck:
path: /status
interval: 10s
timeout: 3s
servers:
- url: "http://private-ip-server-2/"
```
```toml tab="TOML"
## Dynamic configuration
[http.services]
[http.services.app]
[http.services.app.failover.healthCheck]
[http.services.app.failover]
service = "main"
fallback = "backup"
[http.services.main]
[http.services.main.loadBalancer]
[http.services.main.loadBalancer.healthCheck]
path = "/health"
interval = "10s"
timeout = "3s"
[[http.services.main.loadBalancer.servers]]
url = "http://private-ip-server-1/"
[http.services.backup]
[http.services.backup.loadBalancer]
[http.services.backup.loadBalancer.healthCheck]
path = "/health"
interval = "10s"
timeout = "3s"
[[http.services.backup.loadBalancer.servers]]
url = "http://private-ip-server-2/"
```
## Configuring TCP Services
### General

View File

@@ -0,0 +1,295 @@
# Traefik Hub (Experimental)
## Overview
Once the Traefik Hub Experimental feature is enabled in Traefik,
Traefik and its local agent communicate together.
This agent can:
* get the Traefik metrics to display them in the Traefik Hub UI
* secure the Traefik routers
* provide ACME certificates to Traefik
* transfer requests from the SaaS Platform to Traefik (and then avoid the users to expose directly their infrastructure on the internet)
!!! warning "Traefik Hub EntryPoint"
When the Traefik Hub feature is enabled, Traefik exposes some services meant for the Traefik Hub Agent on a dedicated entryPoint (on port `9900` by default).
Given their sensitive nature, those services should not be publicly exposed.
Also this dedicated entryPoint, regardless of how it is created (default, or user-defined), should not be used by anything other than the Hub Agent.
!!! important "Learn More About Traefik Hub"
This section is intended only as a brief overview for Traefik users who are not familiar with Traefik Hub.
To explore all that Traefik Hub has to offer, please consult the [Traefik Hub Documentation](https://doc.traefik.io/traefik-hub).
!!! Note "Prerequisites"
* Traefik Hub is compatible with Traefik Proxy 2.7 or later.
* The Traefik Hub Agent must be installed to connect to the Traefik Hub platform.
* Activate this feature in the experimental section of the static configuration.
!!! example "Minimal Static Configuration to Activate Traefik Hub"
```yaml tab="File (YAML)"
experimental:
hub: true
hub:
tls:
insecure: true
metrics:
prometheus: {}
```
```toml tab="File (TOML)"
[experimental]
hub = true
[hub]
[hub.tls]
insecure = true
[metrics]
[metrics.prometheus]
```
```bash tab="CLI"
--experimental.hub
--hub.tls.insecure=true
--metrics.prometheus=true
```
## Configuration
### `entryPoint`
_Optional, Default="traefik-hub"_
Defines the entryPoint that exposes data for Traefik Hub Agent.
!!! info
* If no entryPoint is defined, a default `traefik-hub` entryPoint is created (on port `9900`).
* If defined, the value must match an existing entryPoint name.
* This dedicated Traefik Hub entryPoint should not be used by anything other than Traefik Hub.
```yaml tab="File (YAML)"
entryPoints:
hub-ep: ":8000"
hub:
entryPoint: "hub-ep"
```
```toml tab="File (TOML)"
[entryPoints.hub-ep]
address = ":8000"
[hub]
entryPoint = "hub-ep"
```
```bash tab="CLI"
--entrypoints.hub-ep.address=:8000
--hub.entrypoint=hub-ep
```
### `tls`
_Required, Default=None_
This section allows configuring mutual TLS connection between Traefik Proxy and the Traefik Hub Agent.
The key and the certificate are the credentials for Traefik Proxy as a TLS client.
The certificate authority authenticates the Traefik Hub Agent certificate.
!!! note "Certificate Domain"
The certificate must be valid for the `proxy.traefik` domain.
!!! note "Certificates Definition"
Certificates can be defined either by their content or their path.
!!! note "Insecure Mode"
The `insecure` option is mutually exclusive with any other option.
```yaml tab="File (YAML)"
hub:
tls:
ca: /path/to/ca.pem
cert: /path/to/cert.pem
key: /path/to/key.pem
```
```toml tab="File (TOML)"
[hub.tls]
ca= "/path/to/ca.pem"
cert= "/path/to/cert.pem"
key= "/path/to/key.pem"
```
```bash tab="CLI"
--hub.tls.ca=/path/to/ca.pem
--hub.tls.cert=/path/to/cert.pem
--hub.tls.key=/path/to/key.pem
```
### `tls.ca`
The certificate authority authenticates the Traefik Hub Agent certificate.
```yaml tab="File (YAML)"
hub:
tls:
ca: |-
-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----
```
```toml tab="File (TOML)"
[hub.tls]
ca = """-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----"""
```
```bash tab="CLI"
--hub.tls.ca=-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----
```
### `tls.cert`
The TLS certificate for Traefik Proxy as a TLS client.
!!! note "Certificate Domain"
The certificate must be valid for the `proxy.traefik` domain.
```yaml tab="File (YAML)"
hub:
tls:
cert: |-
-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----
```
```toml tab="File (TOML)"
[hub.tls]
cert = """-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----"""
```
```bash tab="CLI"
--hub.tls.cert=-----BEGIN CERTIFICATE-----
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
-----END CERTIFICATE-----
```
### `tls.key`
The TLS key for Traefik Proxy as a TLS client.
```yaml tab="File (YAML)"
hub:
tls:
key: |-
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea
O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV
AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4
-----END PRIVATE KEY-----
```
```toml tab="File (TOML)"
[hub.tls]
key = """-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea
O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV
AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4
-----END PRIVATE KEY-----"""
```
```bash tab="CLI"
--hub.tls.key=-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea
O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV
AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4
-----END PRIVATE KEY-----
```
### `tls.insecure`
_Optional, Default=false_
Enables an insecure TLS connection that uses default credentials,
and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent.
The `insecure` option is mutually exclusive with any other option.
!!! warning "Security Consideration"
Do not use this setup in production.
This option implies sensitive data can be exposed to potential malicious third-party programs.
```yaml tab="File (YAML)"
hub:
tls:
insecure: true
```
```toml tab="File (TOML)"
[hub.tls]
insecure = true
```
```bash tab="CLI"
--hub.tls.insecure=true
```

View File

@@ -26,7 +26,7 @@ spec:
serviceAccountName: traefik-ingress-controller
containers:
- name: traefik
image: traefik:v2.6
image: traefik:v2.7
args:
- --api.insecure
- --accesslog

View File

@@ -26,5 +26,5 @@ node:
- K3S_CLUSTER_SECRET=somethingtotallyrandom
volumes:
# this is where you would place a alternative traefik image (saved as a .tar file with
# 'docker save'), if you want to use it, instead of the traefik:v2.6 image.
# 'docker save'), if you want to use it, instead of the traefik:v2.7 image.
- /somewhere/on/your/host/custom-image:/var/lib/rancher/k3s/agent/images

View File

@@ -3,7 +3,7 @@ version: "3.3"
services:
traefik:
image: "traefik:v2.6"
image: "traefik:v2.7"
container_name: "traefik"
command:
#- "--log.level=DEBUG"

View File

@@ -13,7 +13,7 @@ secrets:
services:
traefik:
image: "traefik:v2.6"
image: "traefik:v2.7"
container_name: "traefik"
command:
#- "--log.level=DEBUG"

View File

@@ -3,7 +3,7 @@ version: "3.3"
services:
traefik:
image: "traefik:v2.6"
image: "traefik:v2.7"
container_name: "traefik"
command:
#- "--log.level=DEBUG"

View File

@@ -3,7 +3,7 @@ version: "3.3"
services:
traefik:
image: "traefik:v2.6"
image: "traefik:v2.7"
container_name: "traefik"
command:
#- "--log.level=DEBUG"

View File

@@ -3,7 +3,7 @@ version: "3.3"
services:
traefik:
image: "traefik:v2.6"
image: "traefik:v2.7"
container_name: "traefik"
command:
#- "--log.level=DEBUG"

View File

@@ -135,6 +135,7 @@ nav:
- 'InFlightConn': 'middlewares/tcp/inflightconn.md'
- 'IpWhitelist': 'middlewares/tcp/ipwhitelist.md'
- 'Plugins & Traefik Pilot': 'plugins/index.md'
- 'Traefik Hub': 'traefik-hub/index.md'
- 'Operations':
- 'CLI': 'operations/cli.md'
- 'Dashboard' : 'operations/dashboard.md'
@@ -147,6 +148,7 @@ nav:
- 'Overview': 'observability/metrics/overview.md'
- 'Datadog': 'observability/metrics/datadog.md'
- 'InfluxDB': 'observability/metrics/influxdb.md'
- 'InfluxDB2': 'observability/metrics/influxdb2.md'
- 'Prometheus': 'observability/metrics/prometheus.md'
- 'StatsD': 'observability/metrics/statsd.md'
- 'Tracing':
@@ -198,3 +200,4 @@ nav:
- 'Rancher': 'reference/dynamic-configuration/rancher.md'
- 'Deprecation Notices':
- 'Releases': 'deprecation/releases.md'
- 'Features': 'deprecation/features.md'

View File

@@ -8,8 +8,8 @@ COPY ./webui/ $WEBUI_DIR/
WORKDIR $WEBUI_DIR
RUN npm install
RUN npm run build
RUN yarn install
RUN yarn build
# BUILD
FROM golang:1.17-alpine as gobuild

4
go.mod
View File

@@ -21,7 +21,6 @@ require (
github.com/docker/docker v20.10.7+incompatible
github.com/docker/go-connections v0.4.0
github.com/donovanhide/eventsource v0.0.0-20170630084216-b8f31a59085e // indirect
github.com/eapache/channels v1.1.0
github.com/fatih/structs v1.1.0
github.com/gambol99/go-marathon v0.0.0-20180614232016-99a156b96fb2
github.com/go-acme/lego/v4 v4.6.0
@@ -36,11 +35,12 @@ require (
github.com/hashicorp/go-hclog v0.16.1
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.3.0
github.com/influxdata/influxdb-client-go/v2 v2.7.0
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d
github.com/instana/go-sensor v1.38.3
github.com/klauspost/compress v1.13.0
github.com/kvtools/valkeyrie v0.4.0
github.com/lucas-clemente/quic-go v0.23.0
github.com/lucas-clemente/quic-go v0.25.0
github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f
github.com/miekg/dns v1.1.45
github.com/mitchellh/copystructure v1.0.0

16
go.sum
View File

@@ -474,8 +474,9 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepmap/oapi-codegen v1.6.1 h1:2BvsmRb6pogGNtr8Ann+esAbSKFXx2CZN18VpAMecnw=
github.com/deepmap/oapi-codegen v1.6.1/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/denverdino/aliyungo v0.0.0-20170926055100-d3308649c661/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
@@ -551,8 +552,6 @@ github.com/donovanhide/eventsource v0.0.0-20170630084216-b8f31a59085e h1:rMOGp6H
github.com/donovanhide/eventsource v0.0.0-20170630084216-b8f31a59085e/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4k=
github.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0=
github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=
@@ -618,6 +617,7 @@ github.com/gambol99/go-marathon v0.0.0-20180614232016-99a156b96fb2 h1:df6OFl8WNX
github.com/gambol99/go-marathon v0.0.0-20180614232016-99a156b96fb2/go.mod h1:GLyXJD41gBO/NPKVPGQbhyyC06eugGy15QEZyUkE2/s=
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -991,8 +991,12 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb-client-go/v2 v2.7.0 h1:QgP5mlBE9sGnzplpnf96pr+p7uqlIlL4W2GAP3n+XZg=
github.com/influxdata/influxdb-client-go/v2 v2.7.0/go.mod h1:Y/0W1+TZir7ypoQZYd2IrnVOKB3Tq6oegAQeSVN/+EU=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZQPMZNsjZ7IlCpsLGdQBINg5bxKQ1K1sh6awxLtkA=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU=
github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
github.com/instana/go-sensor v1.38.3 h1:/PdHEDveLmUCvK+O6REvSv8kKljX8vaj9JMjMeCHAWk=
@@ -1120,8 +1124,8 @@ github.com/liquidweb/liquidweb-go v1.6.3/go.mod h1:SuXXp+thr28LnjEw18AYtWwIbWMHS
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
github.com/looplab/fsm v0.1.0 h1:Qte7Zdn/5hBNbXzP7yxVU4OIFHWXBovyTT2LaBTyC20=
github.com/looplab/fsm v0.1.0/go.mod h1:m2VaOfDHxqXBBMgc26m6yUOwkFn8H2AlJDE+jd/uafI=
github.com/lucas-clemente/quic-go v0.23.0 h1:5vFnKtZ6nHDFsc/F3uuiF4T3y/AXaQdxjUqiVw26GZE=
github.com/lucas-clemente/quic-go v0.23.0/go.mod h1:paZuzjXCE5mj6sikVLMvqXk8lJV2AsqtJ6bDhjEfxx0=
github.com/lucas-clemente/quic-go v0.25.0 h1:K+X9Gvd7JXsOHtU0N2icZ2Nw3rx82uBej3mP4CLgibc=
github.com/lucas-clemente/quic-go v0.25.0/go.mod h1:YtzP8bxRVCBlO77yRanE264+fY/T2U9ZlW1AaHOsMOg=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -1147,6 +1151,8 @@ github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2i
github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk=
github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8=
github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1 h1:EnzzN9fPUkUck/1CuY1FlzBaIYMoiBsdwTNmNGkwUUM=
github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1/go.mod h1:PUhIQk19LoFt2174H4+an8TYvWOGjb/hHwphBeaDHwI=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=

View File

@@ -446,26 +446,28 @@ func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase acmeTestCase) {
backend := startTestServer("9010", http.StatusOK, "")
defer backend.Close()
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// wait for traefik (generating acme account take some seconds)
err = try.Do(60*time.Second, func() error {
_, errGet := client.Get("https://127.0.0.1:5001")
return errGet
})
c.Assert(err, checker.IsNil)
for _, sub := range testCase.subCases {
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// wait for traefik (generating acme account take some seconds)
err = try.Do(60*time.Second, func() error {
_, errGet := client.Get("https://127.0.0.1:5001")
return errGet
})
c.Assert(err, checker.IsNil)
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
ServerName: sub.host,
},
// Needed so that each subcase redoes the SSL handshake
DisableKeepAlives: true,
},
}
@@ -479,10 +481,6 @@ func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase acmeTestCase) {
// Retry to send a Request which uses the LE generated certificate
err = try.Do(60*time.Second, func() error {
resp, err = client.Do(req)
// /!\ If connection is not closed, SSLHandshake will only be done during the first trial /!\
req.Close = true
if err != nil {
return err
}

View File

@@ -0,0 +1,105 @@
package integration
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"time"
"github.com/go-check/check"
"github.com/traefik/traefik/v2/integration/try"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
checker "github.com/vdemeester/shakers"
)
type ThrottlingSuite struct{ BaseSuite }
func (s *ThrottlingSuite) SetUpSuite(c *check.C) {
s.createComposeProject(c, "rest")
s.composeUp(c)
}
func (s *ThrottlingSuite) TestThrottleConfReload(c *check.C) {
cmd, display := s.traefikCmd(withConfigFile("fixtures/throttling/simple.toml"))
defer display(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer s.killCmd(cmd)
// wait for Traefik
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1000*time.Millisecond, try.BodyContains("rest@internal"))
c.Assert(err, checker.IsNil)
// Expected a 404 as we did not configure anything.
err = try.GetRequest("http://127.0.0.1:8000/", 1000*time.Millisecond, try.StatusCodeIs(http.StatusNotFound))
c.Assert(err, checker.IsNil)
config := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Services: map[string]*dynamic.Service{
"serviceHTTP": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://" + s.getComposeServiceIP(c, "whoami1") + ":80",
},
},
},
},
},
},
}
router := &dynamic.Router{
EntryPoints: []string{"web"},
Middlewares: []string{},
Service: "serviceHTTP",
Rule: "PathPrefix(`/`)",
}
confChanges := 10
for i := 0; i < confChanges; i++ {
config.HTTP.Routers[fmt.Sprintf("routerHTTP%d", i)] = router
data, err := json.Marshal(config)
c.Assert(err, checker.IsNil)
request, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8080/api/providers/rest", bytes.NewReader(data))
c.Assert(err, checker.IsNil)
response, err := http.DefaultClient.Do(request)
c.Assert(err, checker.IsNil)
c.Assert(response.StatusCode, checker.Equals, http.StatusOK)
time.Sleep(200 * time.Millisecond)
}
reloadsRegexp := regexp.MustCompile(`traefik_config_reloads_total (\d*)\n`)
resp, err := http.Get("http://127.0.0.1:8080/metrics")
c.Assert(err, checker.IsNil)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
c.Assert(err, checker.IsNil)
fields := reloadsRegexp.FindStringSubmatch(string(body))
c.Assert(len(fields), checker.Equals, 2)
reloads, err := strconv.Atoi(fields[1])
if err != nil {
panic(err)
}
// The test tries to trigger a config reload with the REST API every 200ms,
// 10 times (so for 2s in total).
// Therefore the throttling (set at 400ms for this test) should only let
// (2s / 400 ms =) 5 config reloads happen in theory.
// In addition, we have to take into account the extra config reload from the internal provider (5 + 1).
c.Assert(reloads, checker.LessOrEqualThan, 6)
}

View File

@@ -234,6 +234,80 @@ func (s *ConsulCatalogSuite) TestSimpleConfiguration(c *check.C) {
c.Assert(err, checker.IsNil)
}
func (s *ConsulCatalogSuite) TestSimpleConfigurationWithWatch(c *check.C) {
tempObjects := struct {
ConsulAddress string
DefaultRule string
}{
ConsulAddress: s.consulURL,
DefaultRule: "Host(`{{ normalize .Name }}.consul.localhost`)",
}
file := s.adaptFile(c, "fixtures/consul_catalog/simple_watch.toml", tempObjects)
defer os.Remove(file)
reg := &api.AgentServiceRegistration{
ID: "whoami1",
Name: "whoami",
Tags: []string{"traefik.enable=true"},
Port: 80,
Address: s.getComposeServiceIP(c, "whoami1"),
}
err := s.registerService(reg, false)
c.Assert(err, checker.IsNil)
cmd, display := s.traefikCmd(withConfigFile(file))
defer display(c)
err = cmd.Start()
c.Assert(err, checker.IsNil)
defer s.killCmd(cmd)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil)
req.Host = "whoami.consul.localhost"
err = try.Request(req, 2*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContainsOr("Hostname: whoami1"))
c.Assert(err, checker.IsNil)
err = s.deregisterService("whoami1", false)
c.Assert(err, checker.IsNil)
err = try.Request(req, 2*time.Second, try.StatusCodeIs(http.StatusNotFound))
c.Assert(err, checker.IsNil)
whoamiIP := s.getComposeServiceIP(c, "whoami1")
reg.Check = &api.AgentServiceCheck{
CheckID: "some-ok-check",
TCP: whoamiIP + ":80",
Name: "some-ok-check",
Interval: "1s",
Timeout: "1s",
}
err = s.registerService(reg, false)
c.Assert(err, checker.IsNil)
err = try.Request(req, 2*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContainsOr("Hostname: whoami1"))
c.Assert(err, checker.IsNil)
reg.Check = &api.AgentServiceCheck{
CheckID: "some-failing-check",
TCP: ":80",
Name: "some-failing-check",
Interval: "1s",
Timeout: "1s",
}
err = s.registerService(reg, false)
c.Assert(err, checker.IsNil)
err = try.Request(req, 2*time.Second, try.StatusCodeIs(http.StatusNotFound))
c.Assert(err, checker.IsNil)
err = s.deregisterService("whoami1", false)
c.Assert(err, checker.IsNil)
}
func (s *ConsulCatalogSuite) TestRegisterServiceWithoutIP(c *check.C) {
tempObjects := struct {
ConsulAddress string

View File

@@ -0,0 +1,22 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
[entryPoints]
[entryPoints.web]
address = ":8000"
[api]
insecure = true
[providers]
[providers.consulCatalog]
exposedByDefault = true
refreshInterval = "500ms"
defaultRule = "{{ .DefaultRule }}"
watch = true
[providers.consulCatalog.endpoint]
address = "{{ .ConsulAddress }}"

View File

@@ -260,6 +260,8 @@ spec:
- name
type: object
type: array
priority:
type: integer
services:
items:
description: ServiceTCP defines an upstream to proxy traffic.

View File

@@ -0,0 +1,22 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
[entryPoints]
[entryPoints.web]
address = ":8000"
[api]
insecure = true
[providers]
providersThrottleDuration = "400ms"
[providers.rest]
insecure = true
[metrics]
[metrics.prometheus]
buckets = [0.1,0.3,1.2,5.0]

View File

@@ -68,6 +68,7 @@ func Test(t *testing.T) {
check.Suite(&SimpleSuite{})
check.Suite(&TCPSuite{})
check.Suite(&TimeoutSuite{})
check.Suite(&ThrottlingSuite{})
check.Suite(&TLSClientHeadersSuite{})
check.Suite(&TracingSuite{})
check.Suite(&UDPSuite{})

View File

@@ -663,7 +663,7 @@ func (s *SimpleSuite) TestTCPRouterConfigErrors(c *check.C) {
c.Assert(err, checker.IsNil)
// router4 has an unsupported Rule
err = try.GetRequest("http://127.0.0.1:8080/api/tcp/routers/router4@file", 1000*time.Millisecond, try.BodyContains("unknown rule Host(`mydomain.com`)"))
err = try.GetRequest("http://127.0.0.1:8080/api/tcp/routers/router4@file", 1000*time.Millisecond, try.BodyContains("invalid rule: \\\"Host(`mydomain.com`)\\\""))
c.Assert(err, checker.IsNil)
}

View File

@@ -26,6 +26,7 @@ type features struct {
Tracing string `json:"tracing"`
Metrics string `json:"metrics"`
AccessLog bool `json:"accessLog"`
Hub bool `json:"hub"`
// TODO add certificates resolvers
}
@@ -247,6 +248,7 @@ func getFeatures(conf static.Configuration) features {
Tracing: getTracing(conf),
Metrics: getMetrics(conf),
AccessLog: conf.AccessLog != nil,
Hub: conf.Hub != nil,
}
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/traefik/traefik/v2/pkg/config/static"
"github.com/traefik/traefik/v2/pkg/provider/docker"
"github.com/traefik/traefik/v2/pkg/provider/file"
"github.com/traefik/traefik/v2/pkg/provider/hub"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/ingress"
"github.com/traefik/traefik/v2/pkg/provider/marathon"
@@ -265,6 +266,7 @@ func TestHandler_Overview(t *testing.T) {
Tracing: &static.Tracing{
Jaeger: &jaeger.Config{},
},
Hub: &hub.Provider{},
},
confDyn: runtime.Configuration{},
expected: expected{

View File

@@ -2,7 +2,8 @@
"features": {
"accessLog": false,
"metrics": "",
"tracing": ""
"tracing": "",
"hub": false
},
"http": {
"middlewares": {
@@ -50,4 +51,4 @@
"warnings": 0
}
}
}
}

View File

@@ -2,7 +2,8 @@
"features": {
"accessLog": false,
"metrics": "",
"tracing": ""
"tracing": "",
"hub": false
},
"http": {
"middlewares": {
@@ -50,4 +51,4 @@
"warnings": 0
}
}
}
}

View File

@@ -2,7 +2,8 @@
"features": {
"accessLog": false,
"metrics": "Prometheus",
"tracing": "Jaeger"
"tracing": "Jaeger",
"hub": true
},
"http": {
"middlewares": {
@@ -50,4 +51,4 @@
"warnings": 0
}
}
}
}

View File

@@ -2,7 +2,8 @@
"features": {
"accessLog": false,
"metrics": "",
"tracing": ""
"tracing": "",
"hub": false
},
"http": {
"middlewares": {
@@ -60,4 +61,4 @@
"warnings": 0
}
}
}
}

View File

@@ -35,6 +35,7 @@ type Service struct {
LoadBalancer *ServersLoadBalancer `json:"loadBalancer,omitempty" toml:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty" export:"true"`
Weighted *WeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-" export:"true"`
Mirroring *Mirroring `json:"mirroring,omitempty" toml:"mirroring,omitempty" yaml:"mirroring,omitempty" label:"-" export:"true"`
Failover *Failover `json:"failover,omitempty" toml:"failover,omitempty" yaml:"failover,omitempty" label:"-" export:"true"`
}
// +k8s:deepcopy-gen=true
@@ -76,6 +77,15 @@ func (m *Mirroring) SetDefaults() {
// +k8s:deepcopy-gen=true
// Failover holds the Failover configuration.
type Failover struct {
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`
Fallback string `json:"fallback,omitempty" toml:"fallback,omitempty" yaml:"fallback,omitempty" export:"true"`
HealthCheck *HealthCheck `json:"healthCheck,omitempty" toml:"healthCheck,omitempty" yaml:"healthCheck,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// +k8s:deepcopy-gen=true
// MirrorService holds the MirrorService configuration.
type MirrorService struct {
Name string `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty" export:"true"`
@@ -98,7 +108,7 @@ type WeightedRoundRobin struct {
// +k8s:deepcopy-gen=true
// WRRService is a reference to a service load-balanced with weighted round robin.
// WRRService is a reference to a service load-balanced with weighted round-robin.
type WRRService struct {
Name string `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty" export:"true"`
Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty" export:"true"`

View File

@@ -52,6 +52,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"`
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

@@ -285,6 +285,27 @@ func (in *ErrorPage) DeepCopy() *ErrorPage {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Failover) DeepCopyInto(out *Failover) {
*out = *in
if in.HealthCheck != nil {
in, out := &in.HealthCheck, &out.HealthCheck
*out = new(HealthCheck)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Failover.
func (in *Failover) DeepCopy() *Failover {
if in == nil {
return nil
}
out := new(Failover)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
*out = *in
@@ -1171,6 +1192,11 @@ func (in *Service) DeepCopyInto(out *Service) {
*out = new(Mirroring)
(*in).DeepCopyInto(*out)
}
if in.Failover != nil {
in, out := &in.Failover, &out.Failover
*out = new(Failover)
(*in).DeepCopyInto(*out)
}
return
}

View File

@@ -176,11 +176,13 @@ func TestDecodeConfiguration(t *testing.T) {
"traefik.tcp.middlewares.Middleware0.ipwhitelist.sourcerange": "foobar, fiibar",
"traefik.tcp.middlewares.Middleware2.inflightconn.amount": "42",
"traefik.tcp.routers.Router0.rule": "foobar",
"traefik.tcp.routers.Router0.priority": "42",
"traefik.tcp.routers.Router0.entrypoints": "foobar, fiibar",
"traefik.tcp.routers.Router0.service": "foobar",
"traefik.tcp.routers.Router0.tls.passthrough": "false",
"traefik.tcp.routers.Router0.tls.options": "foo",
"traefik.tcp.routers.Router1.rule": "foobar",
"traefik.tcp.routers.Router1.priority": "42",
"traefik.tcp.routers.Router1.entrypoints": "foobar, fiibar",
"traefik.tcp.routers.Router1.service": "foobar",
"traefik.tcp.routers.Router1.tls.options": "foo",
@@ -211,8 +213,9 @@ func TestDecodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@@ -223,8 +226,9 @@ func TestDecodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@@ -699,8 +703,9 @@ func TestEncodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@@ -711,8 +716,9 @@ func TestEncodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@@ -1333,11 +1339,13 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.TCP.Middlewares.Middleware0.IPWhiteList.SourceRange": "foobar, fiibar",
"traefik.TCP.Middlewares.Middleware2.InFlightConn.Amount": "42",
"traefik.TCP.Routers.Router0.Rule": "foobar",
"traefik.TCP.Routers.Router0.Priority": "42",
"traefik.TCP.Routers.Router0.EntryPoints": "foobar, fiibar",
"traefik.TCP.Routers.Router0.Service": "foobar",
"traefik.TCP.Routers.Router0.TLS.Passthrough": "false",
"traefik.TCP.Routers.Router0.TLS.Options": "foo",
"traefik.TCP.Routers.Router1.Rule": "foobar",
"traefik.TCP.Routers.Router1.Priority": "42",
"traefik.TCP.Routers.Router1.EntryPoints": "foobar, fiibar",
"traefik.TCP.Routers.Router1.Service": "foobar",
"traefik.TCP.Routers.Router1.TLS.Passthrough": "false",

View File

@@ -59,6 +59,11 @@ type HTTPConfig struct {
TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// HTTP3Config is the HTTP3 configuration of an entry point.
type HTTP3Config struct {
AdvertisedPort int32 `description:"UDP port to advertise, on which HTTP/3 is available." json:"advertisedPort,omitempty" toml:"advertisedPort,omitempty" yaml:"advertisedPort,omitempty" export:"true"`
}
// Redirections is a set of redirection for an entry point.
type Redirections struct {
EntryPoint *RedirectEntryPoint `description:"Set of redirection for an entry point." json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty" export:"true"`
@@ -72,11 +77,6 @@ type RedirectEntryPoint struct {
Priority int `description:"Priority of the generated router." json:"priority,omitempty" toml:"priority,omitempty" yaml:"priority,omitempty" export:"true"`
}
// HTTP3Config is the HTTP3 configuration of an entry point.
type HTTP3Config struct {
AdvertisedPort int32 `description:"UDP port to advertise, on which HTTP/3 is available." json:"advertisedPort,omitempty" toml:"advertisedPort,omitempty" yaml:"advertisedPort,omitempty" export:"true"`
}
// SetDefaults sets the default values.
func (r *RedirectEntryPoint) SetDefaults() {
r.Scheme = "https"

View File

@@ -9,4 +9,5 @@ type Experimental struct {
KubernetesGateway bool `description:"Allow the Kubernetes gateway api provider usage." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" export:"true"`
HTTP3 bool `description:"Enable HTTP3." json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty" export:"true"`
Hub bool `description:"Enable the Traefik Hub provider." json:"hub,omitempty" toml:"hub,omitempty" yaml:"hub,omitempty" export:"true"`
}

View File

@@ -1,6 +1,7 @@
package static
// Pilot Configuration related to Traefik Pilot.
// Deprecated.
type Pilot struct {
Token string `description:"Traefik Pilot token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
Dashboard bool `description:"Enable Traefik Pilot in the dashboard." json:"dashboard,omitempty" toml:"dashboard,omitempty" yaml:"dashboard,omitempty"`

View File

@@ -1,6 +1,7 @@
package static
import (
"errors"
"fmt"
stdlog "log"
"strings"
@@ -17,6 +18,7 @@ import (
"github.com/traefik/traefik/v2/pkg/provider/ecs"
"github.com/traefik/traefik/v2/pkg/provider/file"
"github.com/traefik/traefik/v2/pkg/provider/http"
"github.com/traefik/traefik/v2/pkg/provider/hub"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/gateway"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/ingress"
@@ -76,8 +78,11 @@ type Configuration struct {
CertificatesResolvers map[string]CertificateResolver `description:"Certificates resolvers configuration." json:"certificatesResolvers,omitempty" toml:"certificatesResolvers,omitempty" yaml:"certificatesResolvers,omitempty" export:"true"`
// Deprecated.
Pilot *Pilot `description:"Traefik Pilot configuration." json:"pilot,omitempty" toml:"pilot,omitempty" yaml:"pilot,omitempty" export:"true"`
Hub *hub.Provider `description:"Traefik Hub configuration." json:"hub,omitempty" toml:"hub,omitempty" yaml:"hub,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
Experimental *Experimental `description:"experimental features." json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" export:"true"`
}
@@ -196,10 +201,14 @@ type Providers struct {
// It also takes care of maintaining backwards compatibility.
func (c *Configuration) SetEffectiveConfiguration() {
// Creates the default entry point if needed
if len(c.EntryPoints) == 0 {
if len(c.EntryPoints) == 0 || (c.Hub != nil && len(c.EntryPoints) == 1 && c.EntryPoints[c.Hub.EntryPoint] != nil) {
ep := &EntryPoint{Address: ":80"}
ep.SetDefaults()
c.EntryPoints = EntryPoints{"http": ep}
// TODO: double check this tomorrow
if c.EntryPoints == nil {
c.EntryPoints = make(EntryPoints)
}
c.EntryPoints["http"] = ep
}
// Creates the internal traefik entry point if needed
@@ -214,6 +223,15 @@ func (c *Configuration) SetEffectiveConfiguration() {
}
}
if c.Hub != nil {
if err := c.initHubProvider(); err != nil {
c.Hub = nil
log.WithoutContext().Errorf("Unable to activate the Hub provider: %v", err)
} else {
log.WithoutContext().Debugf("Experimental Hub provider has been activated.")
}
}
if c.Providers.Docker != nil {
if c.Providers.Docker.SwarmModeRefreshSeconds <= 0 {
c.Providers.Docker.SwarmModeRefreshSeconds = ptypes.Duration(15 * time.Second)
@@ -279,6 +297,46 @@ func (c *Configuration) initACMEProvider() {
legolog.Logger = stdlog.New(log.WithoutContext().WriterLevel(logrus.DebugLevel), "legolog: ", 0)
}
func (c *Configuration) initHubProvider() error {
// Hub provider is an experimental feature. Require the experimental flag to be enabled before continuing.
if c.Experimental == nil || !c.Experimental.Hub {
return errors.New("experimental flag for Hub not set")
}
if c.Hub.TLS == nil {
return errors.New("no TLS configuration defined for Hub")
}
if c.Hub.TLS.Insecure && (c.Hub.TLS.CA != "" || c.Hub.TLS.Cert != "" || c.Hub.TLS.Key != "") {
return errors.New("mTLS configuration for Hub and insecure TLS for Hub are mutually exclusive")
}
if !c.Hub.TLS.Insecure && (c.Hub.TLS.CA == "" || c.Hub.TLS.Cert == "" || c.Hub.TLS.Key == "") {
return errors.New("incomplete mTLS configuration for Hub")
}
if c.Hub.TLS.Insecure {
log.WithoutContext().Warn("Hub is in `insecure` mode. Do not run in production with this setup.")
}
// Creates the internal Hub entry point if needed.
if c.Hub.EntryPoint == hub.DefaultEntryPointName {
if _, ok := c.EntryPoints[hub.DefaultEntryPointName]; !ok {
var ep EntryPoint
ep.SetDefaults()
ep.Address = ":9900"
c.EntryPoints[hub.DefaultEntryPointName] = &ep
log.WithoutContext().Infof("The entryPoint %q is created on port 9900 to allow Traefik to communicate with the Hub Agent for Traefik.", hub.DefaultEntryPointName)
}
}
c.EntryPoints[c.Hub.EntryPoint].HTTP.TLS = &TLSConfig{
Options: "traefik-hub",
}
return nil
}
// ValidateConfiguration validate that configuration is coherent.
func (c *Configuration) ValidateConfiguration() error {
var acmeEmail string

View File

@@ -98,7 +98,7 @@ func RegisterInfluxDB(ctx context.Context, config *types.InfluxDB) Registry {
return registry
}
// initInfluxDBTicker creates a influxDBClient.
// initInfluxDBClient creates a influxDBClient.
func initInfluxDBClient(ctx context.Context, config *types.InfluxDB) *influx.Influx {
logger := log.FromContext(ctx)

144
pkg/metrics/influxdb2.go Normal file
View File

@@ -0,0 +1,144 @@
package metrics
import (
"context"
"errors"
"time"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics/influx"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
influxdb2api "github.com/influxdata/influxdb-client-go/v2/api"
"github.com/influxdata/influxdb-client-go/v2/api/write"
influxdb2log "github.com/influxdata/influxdb-client-go/v2/log"
influxdb "github.com/influxdata/influxdb1-client/v2"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/safe"
"github.com/traefik/traefik/v2/pkg/types"
)
var (
influxDB2Ticker *time.Ticker
influxDB2Store *influx.Influx
influxDB2Client influxdb2.Client
)
// RegisterInfluxDB2 creates metrics exporter for InfluxDB2.
func RegisterInfluxDB2(ctx context.Context, config *types.InfluxDB2) Registry {
if influxDB2Client == nil {
var err error
if influxDB2Client, err = newInfluxDB2Client(config); err != nil {
log.FromContext(ctx).Error(err)
return nil
}
}
if influxDB2Store == nil {
influxDB2Store = influx.New(
config.AdditionalLabels,
influxdb.BatchPointsConfig{},
kitlog.LoggerFunc(func(kv ...interface{}) error {
log.FromContext(ctx).Error(kv)
return nil
}),
)
influxDB2Ticker = time.NewTicker(time.Duration(config.PushInterval))
safe.Go(func() {
wc := influxDB2Client.WriteAPIBlocking(config.Org, config.Bucket)
influxDB2Store.WriteLoop(ctx, influxDB2Ticker.C, influxDB2Writer{wc: wc})
})
}
registry := &standardRegistry{
configReloadsCounter: influxDB2Store.NewCounter(influxDBConfigReloadsName),
configReloadsFailureCounter: influxDB2Store.NewCounter(influxDBConfigReloadsFailureName),
lastConfigReloadSuccessGauge: influxDB2Store.NewGauge(influxDBLastConfigReloadSuccessName),
lastConfigReloadFailureGauge: influxDB2Store.NewGauge(influxDBLastConfigReloadFailureName),
tlsCertsNotAfterTimestampGauge: influxDB2Store.NewGauge(influxDBTLSCertsNotAfterTimestampName),
}
if config.AddEntryPointsLabels {
registry.epEnabled = config.AddEntryPointsLabels
registry.entryPointReqsCounter = influxDB2Store.NewCounter(influxDBEntryPointReqsName)
registry.entryPointReqsTLSCounter = influxDB2Store.NewCounter(influxDBEntryPointReqsTLSName)
registry.entryPointReqDurationHistogram, _ = NewHistogramWithScale(influxDB2Store.NewHistogram(influxDBEntryPointReqDurationName), time.Second)
registry.entryPointOpenConnsGauge = influxDB2Store.NewGauge(influxDBEntryPointOpenConnsName)
}
if config.AddRoutersLabels {
registry.routerEnabled = config.AddRoutersLabels
registry.routerReqsCounter = influxDB2Store.NewCounter(influxDBRouterReqsName)
registry.routerReqsTLSCounter = influxDB2Store.NewCounter(influxDBRouterReqsTLSName)
registry.routerReqDurationHistogram, _ = NewHistogramWithScale(influxDB2Store.NewHistogram(influxDBRouterReqsDurationName), time.Second)
registry.routerOpenConnsGauge = influxDB2Store.NewGauge(influxDBORouterOpenConnsName)
}
if config.AddServicesLabels {
registry.svcEnabled = config.AddServicesLabels
registry.serviceReqsCounter = influxDB2Store.NewCounter(influxDBServiceReqsName)
registry.serviceReqsTLSCounter = influxDB2Store.NewCounter(influxDBServiceReqsTLSName)
registry.serviceReqDurationHistogram, _ = NewHistogramWithScale(influxDB2Store.NewHistogram(influxDBServiceReqsDurationName), time.Second)
registry.serviceRetriesCounter = influxDB2Store.NewCounter(influxDBServiceRetriesTotalName)
registry.serviceOpenConnsGauge = influxDB2Store.NewGauge(influxDBServiceOpenConnsName)
registry.serviceServerUpGauge = influxDB2Store.NewGauge(influxDBServiceServerUpName)
}
return registry
}
// StopInfluxDB2 stops and resets InfluxDB2 client, ticker and store.
func StopInfluxDB2() {
if influxDB2Client != nil {
influxDB2Client.Close()
}
influxDB2Client = nil
if influxDB2Ticker != nil {
influxDB2Ticker.Stop()
}
influxDB2Ticker = nil
influxDB2Store = nil
}
// newInfluxDB2Client creates an influxdb2.Client.
func newInfluxDB2Client(config *types.InfluxDB2) (influxdb2.Client, error) {
if config.Token == "" || config.Org == "" || config.Bucket == "" {
return nil, errors.New("token, org or bucket property is missing")
}
// Disable InfluxDB2 logs.
// See https://github.com/influxdata/influxdb-client-go/blob/v2.7.0/options.go#L128
influxdb2log.Log = nil
return influxdb2.NewClient(config.Address, config.Token), nil
}
type influxDB2Writer struct {
wc influxdb2api.WriteAPIBlocking
}
func (w influxDB2Writer) Write(bp influxdb.BatchPoints) error {
ctx := log.With(context.Background(), log.Str(log.MetricsProviderName, "influxdb2"))
logger := log.FromContext(ctx)
wps := make([]*write.Point, 0, len(bp.Points()))
for _, p := range bp.Points() {
fields, err := p.Fields()
if err != nil {
logger.Errorf("Error while getting %s point fields: %s", p.Name(), err)
continue
}
wps = append(wps, influxdb2.NewPoint(
p.Name(),
p.Tags(),
fields,
p.Time(),
))
}
return w.wc.WritePoint(ctx, wps...)
}

View File

@@ -0,0 +1,145 @@
package metrics
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/types"
)
func TestInfluxDB2(t *testing.T) {
c := make(chan *string)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
bodyStr := string(body)
c <- &bodyStr
_, _ = fmt.Fprintln(w, "ok")
}))
defer ts.Close()
influxDB2Registry := RegisterInfluxDB2(context.Background(),
&types.InfluxDB2{
Address: ts.URL,
Token: "test-token",
PushInterval: ptypes.Duration(10 * time.Millisecond),
Org: "test-org",
Bucket: "test-bucket",
AddEntryPointsLabels: true,
AddRoutersLabels: true,
AddServicesLabels: true,
})
defer StopInfluxDB2()
if !influxDB2Registry.IsEpEnabled() || !influxDB2Registry.IsRouterEnabled() || !influxDB2Registry.IsSvcEnabled() {
t.Fatalf("InfluxDB2Registry should return true for IsEnabled(), IsRouterEnabled() and IsSvcEnabled()")
}
expectedServer := []string{
`(traefik\.config\.reload\.total count=1) [\d]{19}`,
`(traefik\.config\.reload\.total\.failure count=1) [\d]{19}`,
`(traefik\.config\.reload\.lastSuccessTimestamp value=1) [\d]{19}`,
`(traefik\.config\.reload\.lastFailureTimestamp value=1) [\d]{19}`,
}
influxDB2Registry.ConfigReloadsCounter().Add(1)
influxDB2Registry.ConfigReloadsFailureCounter().Add(1)
influxDB2Registry.LastConfigReloadSuccessGauge().Set(1)
influxDB2Registry.LastConfigReloadFailureGauge().Set(1)
msgServer := <-c
assertMessage(t, *msgServer, expectedServer)
expectedTLS := []string{
`(traefik\.tls\.certs\.notAfterTimestamp,key=value value=1) [\d]{19}`,
}
influxDB2Registry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1)
msgTLS := <-c
assertMessage(t, *msgTLS, expectedTLS)
expectedEntrypoint := []string{
`(traefik\.entrypoint\.requests\.total,code=200,entrypoint=test,method=GET count=1) [\d]{19}`,
`(traefik\.entrypoint\.requests\.tls\.total,entrypoint=test,tls_cipher=bar,tls_version=foo count=1) [\d]{19}`,
`(traefik\.entrypoint\.request\.duration(?:,code=[\d]{3})?,entrypoint=test p50=10000,p90=10000,p95=10000,p99=10000) [\d]{19}`,
`(traefik\.entrypoint\.connections\.open,entrypoint=test value=1) [\d]{19}`,
}
influxDB2Registry.EntryPointReqsCounter().With("entrypoint", "test", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
influxDB2Registry.EntryPointReqsTLSCounter().With("entrypoint", "test", "tls_version", "foo", "tls_cipher", "bar").Add(1)
influxDB2Registry.EntryPointReqDurationHistogram().With("entrypoint", "test").Observe(10000)
influxDB2Registry.EntryPointOpenConnsGauge().With("entrypoint", "test").Set(1)
msgEntrypoint := <-c
assertMessage(t, *msgEntrypoint, expectedEntrypoint)
expectedRouter := []string{
`(traefik\.router\.requests\.total,code=200,method=GET,router=demo,service=test count=1) [\d]{19}`,
`(traefik\.router\.requests\.total,code=404,method=GET,router=demo,service=test count=1) [\d]{19}`,
`(traefik\.router\.requests\.tls\.total,router=demo,service=test,tls_cipher=bar,tls_version=foo count=1) [\d]{19}`,
`(traefik\.router\.request\.duration,code=200,router=demo,service=test p50=10000,p90=10000,p95=10000,p99=10000) [\d]{19}`,
`(traefik\.router\.connections\.open,router=demo,service=test value=1) [\d]{19}`,
}
influxDB2Registry.RouterReqsCounter().With("router", "demo", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
influxDB2Registry.RouterReqsCounter().With("router", "demo", "service", "test", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
influxDB2Registry.RouterReqsTLSCounter().With("router", "demo", "service", "test", "tls_version", "foo", "tls_cipher", "bar").Add(1)
influxDB2Registry.RouterReqDurationHistogram().With("router", "demo", "service", "test", "code", strconv.Itoa(http.StatusOK)).Observe(10000)
influxDB2Registry.RouterOpenConnsGauge().With("router", "demo", "service", "test").Set(1)
msgRouter := <-c
assertMessage(t, *msgRouter, expectedRouter)
expectedService := []string{
`(traefik\.service\.requests\.total,code=200,method=GET,service=test count=1) [\d]{19}`,
`(traefik\.service\.requests\.total,code=404,method=GET,service=test count=1) [\d]{19}`,
`(traefik\.service\.requests\.tls\.total,service=test,tls_cipher=bar,tls_version=foo count=1) [\d]{19}`,
`(traefik\.service\.request\.duration,code=200,service=test p50=10000,p90=10000,p95=10000,p99=10000) [\d]{19}`,
`(traefik\.service\.server\.up,service=test,url=http://127.0.0.1 value=1) [\d]{19}`,
}
influxDB2Registry.ServiceReqsCounter().With("service", "test", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
influxDB2Registry.ServiceReqsCounter().With("service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
influxDB2Registry.ServiceReqsTLSCounter().With("service", "test", "tls_version", "foo", "tls_cipher", "bar").Add(1)
influxDB2Registry.ServiceReqDurationHistogram().With("service", "test", "code", strconv.Itoa(http.StatusOK)).Observe(10000)
influxDB2Registry.ServiceServerUpGauge().With("service", "test", "url", "http://127.0.0.1").Set(1)
msgService := <-c
assertMessage(t, *msgService, expectedService)
expectedServiceRetries := []string{
`(traefik\.service\.retries\.total,service=test count=2) [\d]{19}`,
`(traefik\.service\.retries\.total,service=foobar count=1) [\d]{19}`,
}
influxDB2Registry.ServiceRetriesCounter().With("service", "test").Add(1)
influxDB2Registry.ServiceRetriesCounter().With("service", "test").Add(1)
influxDB2Registry.ServiceRetriesCounter().With("service", "foobar").Add(1)
msgServiceRetries := <-c
assertMessage(t, *msgServiceRetries, expectedServiceRetries)
expectedServiceOpenConns := []string{
`(traefik\.service\.connections\.open,service=test value=2) [\d]{19}`,
`(traefik\.service\.connections\.open,service=foobar value=1) [\d]{19}`,
}
influxDB2Registry.ServiceOpenConnsGauge().With("service", "test").Add(1)
influxDB2Registry.ServiceOpenConnsGauge().With("service", "test").Add(1)
influxDB2Registry.ServiceOpenConnsGauge().With("service", "foobar").Add(1)
msgServiceOpenConns := <-c
assertMessage(t, *msgServiceOpenConns, expectedServiceOpenConns)
}

View File

@@ -11,6 +11,7 @@ import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stvp/go-udp-testing"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/types"
@@ -125,10 +126,8 @@ func TestInfluxDBHTTP(t *testing.T) {
c := make(chan *string)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "can't read body "+err.Error(), http.StatusBadRequest)
return
}
require.NoError(t, err)
bodyStr := string(body)
c <- &bodyStr
_, _ = fmt.Fprintln(w, "ok")
@@ -140,7 +139,7 @@ func TestInfluxDBHTTP(t *testing.T) {
&types.InfluxDB{
Address: ts.URL,
Protocol: "http",
PushInterval: ptypes.Duration(time.Second),
PushInterval: ptypes.Duration(10 * time.Millisecond),
Database: "test",
RetentionPolicy: "autogen",
AddEntryPointsLabels: true,

View File

@@ -1,4 +1,4 @@
package rules
package http
import (
"fmt"
@@ -10,11 +10,14 @@ import (
"github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v2/pkg/rules"
"github.com/vulcand/predicate"
)
var funcs = map[string]func(*mux.Route, ...string) error{
"Host": host,
const hostMatcher = "Host"
var httpFuncs = map[string]func(*mux.Route, ...string) error{
hostMatcher: host,
"HostHeader": host,
"HostRegexp": hostRegexp,
"ClientIP": clientIP,
@@ -26,33 +29,38 @@ var funcs = map[string]func(*mux.Route, ...string) error{
"Query": query,
}
// Router handle routing with rules.
type Router struct {
// Muxer handles routing with rules.
type Muxer struct {
*mux.Router
parser predicate.Parser
}
// NewRouter returns a new router instance.
func NewRouter() (*Router, error) {
parser, err := newParser()
// NewMuxer returns a new muxer instance.
func NewMuxer() (*Muxer, error) {
var matchers []string
for matcher := range httpFuncs {
matchers = append(matchers, matcher)
}
parser, err := rules.NewParser(matchers)
if err != nil {
return nil, err
}
return &Router{
return &Muxer{
Router: mux.NewRouter().SkipClean(true),
parser: parser,
}, nil
}
// AddRoute add a new route to the router.
func (r *Router) AddRoute(rule string, priority int, handler http.Handler) error {
func (r *Muxer) AddRoute(rule string, priority int, handler http.Handler) error {
parse, err := r.parser.Parse(rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
buildTree, ok := parse.(treeBuilder)
buildTree, ok := parse.(rules.TreeBuilder)
if !ok {
return fmt.Errorf("error while parsing rule %s", rule)
}
@@ -72,23 +80,40 @@ func (r *Router) AddRoute(rule string, priority int, handler http.Handler) error
return nil
}
type tree struct {
matcher string
not bool
value []string
ruleLeft *tree
ruleRight *tree
// ParseDomains extract domains from rule.
func ParseDomains(rule string) ([]string, error) {
var matchers []string
for matcher := range httpFuncs {
matchers = append(matchers, matcher)
}
parser, err := rules.NewParser(matchers)
if err != nil {
return nil, err
}
parse, err := parser.Parse(rule)
if err != nil {
return nil, err
}
buildTree, ok := parse.(rules.TreeBuilder)
if !ok {
return nil, fmt.Errorf("error while parsing rule %s", rule)
}
return buildTree().ParseMatchers([]string{hostMatcher}), nil
}
func path(route *mux.Route, paths ...string) error {
rt := route.Subrouter()
for _, path := range paths {
tmpRt := rt.Path(path)
if tmpRt.GetError() != nil {
return tmpRt.GetError()
if err := rt.Path(path).GetError(); err != nil {
return err
}
}
return nil
}
@@ -96,11 +121,11 @@ func pathPrefix(route *mux.Route, paths ...string) error {
rt := route.Subrouter()
for _, path := range paths {
tmpRt := rt.PathPrefix(path)
if tmpRt.GetError() != nil {
return tmpRt.GetError()
if err := rt.PathPrefix(path).GetError(); err != nil {
return err
}
}
return nil
}
@@ -220,33 +245,34 @@ func query(route *mux.Route, query ...string) error {
return route.GetError()
}
func addRuleOnRouter(router *mux.Router, rule *tree) error {
switch rule.matcher {
func addRuleOnRouter(router *mux.Router, rule *rules.Tree) error {
switch rule.Matcher {
case "and":
route := router.NewRoute()
err := addRuleOnRoute(route, rule.ruleLeft)
err := addRuleOnRoute(route, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRoute(route, rule.ruleRight)
return addRuleOnRoute(route, rule.RuleRight)
case "or":
err := addRuleOnRouter(router, rule.ruleLeft)
err := addRuleOnRouter(router, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRouter(router, rule.ruleRight)
return addRuleOnRouter(router, rule.RuleRight)
default:
err := checkRule(rule)
err := rules.CheckRule(rule)
if err != nil {
return err
}
if rule.not {
return not(funcs[rule.matcher])(router.NewRoute(), rule.value...)
if rule.Not {
return not(httpFuncs[rule.Matcher])(router.NewRoute(), rule.Value...)
}
return funcs[rule.matcher](router.NewRoute(), rule.value...)
return httpFuncs[rule.Matcher](router.NewRoute(), rule.Value...)
}
}
@@ -264,48 +290,36 @@ func not(m func(*mux.Route, ...string) error) func(*mux.Route, ...string) error
}
}
func addRuleOnRoute(route *mux.Route, rule *tree) error {
switch rule.matcher {
func addRuleOnRoute(route *mux.Route, rule *rules.Tree) error {
switch rule.Matcher {
case "and":
err := addRuleOnRoute(route, rule.ruleLeft)
err := addRuleOnRoute(route, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRoute(route, rule.ruleRight)
return addRuleOnRoute(route, rule.RuleRight)
case "or":
subRouter := route.Subrouter()
err := addRuleOnRouter(subRouter, rule.ruleLeft)
err := addRuleOnRouter(subRouter, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRouter(subRouter, rule.ruleRight)
return addRuleOnRouter(subRouter, rule.RuleRight)
default:
err := checkRule(rule)
err := rules.CheckRule(rule)
if err != nil {
return err
}
if rule.not {
return not(funcs[rule.matcher])(route, rule.value...)
if rule.Not {
return not(httpFuncs[rule.Matcher])(route, rule.Value...)
}
return funcs[rule.matcher](route, rule.value...)
}
}
func checkRule(rule *tree) error {
if len(rule.value) == 0 {
return fmt.Errorf("no args for matcher %s", rule.matcher)
return httpFuncs[rule.Matcher](route, rule.Value...)
}
for _, v := range rule.value {
if len(v) == 0 {
return fmt.Errorf("empty args for matcher %s, %v", rule.matcher, rule.value)
}
}
return nil
}
// IsASCII checks if the given string contains only ASCII characters.

View File

@@ -1,4 +1,4 @@
package rules
package http
import (
"net/http"
@@ -635,10 +635,10 @@ func Test_addRoute(t *testing.T) {
t.Parallel()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
router, err := NewRouter()
muxer, err := NewMuxer()
require.NoError(t, err)
err = router.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, 0, handler)
if test.expectedError {
require.Error(t, err)
} else {
@@ -659,7 +659,7 @@ func Test_addRoute(t *testing.T) {
for key, value := range test.headers {
req.Header.Set(key, value)
}
reqHost.ServeHTTP(w, req, router.ServeHTTP)
reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
results[calledURL] = w.Code
}
assert.Equal(t, test.expected, results)
@@ -787,7 +787,7 @@ func Test_addRoutePriority(t *testing.T) {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
router, err := NewRouter()
muxer, err := NewMuxer()
require.NoError(t, err)
for _, route := range test.cases {
@@ -796,16 +796,16 @@ func Test_addRoutePriority(t *testing.T) {
w.Header().Set("X-From", route.xFrom)
})
err := router.AddRoute(route.rule, route.priority, handler)
err := muxer.AddRoute(route.rule, route.priority, handler)
require.NoError(t, err, route.rule)
}
router.SortRoutes()
muxer.SortRoutes()
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, test.path, nil)
router.ServeHTTP(w, req)
muxer.ServeHTTP(w, req)
assert.Equal(t, test.expected, w.Header().Get("X-From"))
})
@@ -900,44 +900,42 @@ func TestParseDomains(t *testing.T) {
errorExpected bool
}{
{
description: "Many host rules",
expression: "Host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
errorExpected: false,
description: "Unknown rule",
expression: "Foobar(`foo.bar`,`test.bar`)",
errorExpected: true,
},
{
description: "Many host rules upper",
expression: "HOST(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
errorExpected: false,
description: "Several host rules",
expression: "Host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "Many host rules lower",
expression: "host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
errorExpected: false,
description: "Several host rules upper",
expression: "HOST(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "No host rule",
expression: "Path(`/test`)",
errorExpected: false,
description: "Several host rules lower",
expression: "host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "Host rule and another rule",
expression: "Host(`foo.bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
errorExpected: false,
description: "No host rule",
expression: "Path(`/test`)",
},
{
description: "Host rule to trim and another rule",
expression: "Host(`Foo.Bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
errorExpected: false,
description: "Host rule and another rule",
expression: "Host(`foo.bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
},
{
description: "Host rule with no domain",
expression: "Host() && Path(`/test`)",
errorExpected: false,
description: "Host rule to trim and another rule",
expression: "Host(`Foo.Bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
},
{
description: "Host rule with no domain",
expression: "Host() && Path(`/test`)",
},
}

452
pkg/muxer/tcp/mux.go Normal file
View File

@@ -0,0 +1,452 @@
package tcp
import (
"bytes"
"errors"
"fmt"
"net"
"regexp"
"sort"
"strconv"
"strings"
"github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/rules"
"github.com/traefik/traefik/v2/pkg/tcp"
"github.com/traefik/traefik/v2/pkg/types"
"github.com/vulcand/predicate"
)
var tcpFuncs = map[string]func(*matchersTree, ...string) error{
"HostSNI": hostSNI,
"HostSNIRegexp": hostSNIRegexp,
"ClientIP": clientIP,
}
// ParseHostSNI extracts the HostSNIs declared in a rule.
// This is a first naive implementation used in TCP routing.
func ParseHostSNI(rule string) ([]string, error) {
var matchers []string
for matcher := range tcpFuncs {
matchers = append(matchers, matcher)
}
parser, err := rules.NewParser(matchers)
if err != nil {
return nil, err
}
parse, err := parser.Parse(rule)
if err != nil {
return nil, err
}
buildTree, ok := parse.(rules.TreeBuilder)
if !ok {
return nil, fmt.Errorf("error while parsing rule %s", rule)
}
return buildTree().ParseMatchers([]string{"HostSNI"}), nil
}
// ConnData contains TCP connection metadata.
type ConnData struct {
serverName string
remoteIP string
}
// NewConnData builds a connData struct from the given parameters.
func NewConnData(serverName string, conn tcp.WriteCloser) (ConnData, error) {
remoteIP, _, err := net.SplitHostPort(conn.RemoteAddr().String())
if err != nil {
return ConnData{}, fmt.Errorf("error while parsing remote address %q: %w", conn.RemoteAddr().String(), err)
}
// as per https://datatracker.ietf.org/doc/html/rfc6066:
// > The hostname is represented as a byte string using ASCII encoding without a trailing dot.
// so there is no need to trim a potential trailing dot
serverName = types.CanonicalDomain(serverName)
return ConnData{
serverName: types.CanonicalDomain(serverName),
remoteIP: remoteIP,
}, nil
}
// Muxer defines a muxer that handles TCP routing with rules.
type Muxer struct {
routes []*route
parser predicate.Parser
}
// NewMuxer returns a TCP muxer.
func NewMuxer() (*Muxer, error) {
var matcherNames []string
for matcherName := range tcpFuncs {
matcherNames = append(matcherNames, matcherName)
}
parser, err := rules.NewParser(matcherNames)
if err != nil {
return nil, fmt.Errorf("error while creating rules parser: %w", err)
}
return &Muxer{parser: parser}, nil
}
// Match returns the handler of the first route matching the connection metadata.
func (m Muxer) Match(meta ConnData) tcp.Handler {
for _, route := range m.routes {
if route.matchers.match(meta) {
return route.handler
}
}
return nil
}
// 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 {
// Special case for when the catchAll fallback is present.
// When no user-defined priority is found, the lowest computable priority minus one is used,
// in order to make the fallback the last to be evaluated.
if priority == 0 && rule == "HostSNI(`*`)" {
priority = -1
}
// Default value, which means the user has not set it, so we'll compute it.
if priority == 0 {
priority = len(rule)
}
parse, err := m.parser.Parse(rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
buildTree, ok := parse.(rules.TreeBuilder)
if !ok {
return fmt.Errorf("error while parsing rule %s", rule)
}
var matchers matchersTree
err = addRule(&matchers, buildTree())
if err != nil {
return err
}
newRoute := &route{
handler: handler,
priority: priority,
matchers: matchers,
}
m.routes = append(m.routes, newRoute)
sort.Sort(routes(m.routes))
return nil
}
func addRule(tree *matchersTree, rule *rules.Tree) error {
switch rule.Matcher {
case "and", "or":
tree.operator = rule.Matcher
tree.left = &matchersTree{}
err := addRule(tree.left, rule.RuleLeft)
if err != nil {
return err
}
tree.right = &matchersTree{}
return addRule(tree.right, rule.RuleRight)
default:
err := rules.CheckRule(rule)
if err != nil {
return err
}
err = tcpFuncs[rule.Matcher](tree, rule.Value...)
if err != nil {
return err
}
if rule.Not {
matcherFunc := tree.matcher
tree.matcher = func(meta ConnData) bool {
return !matcherFunc(meta)
}
}
}
return nil
}
// HasRoutes returns whether the muxer has routes.
func (m *Muxer) HasRoutes() bool {
return len(m.routes) > 0
}
// routes implements sort.Interface.
type routes []*route
// Len implements sort.Interface.
func (r routes) Len() int { return len(r) }
// Swap implements sort.Interface.
func (r routes) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
// Less implements sort.Interface.
func (r routes) Less(i, j int) bool { return r[i].priority > r[j].priority }
// route holds the matchers to match TCP route,
// and the handler that will serve the connection.
type route struct {
// matchers tree structure reflecting the rule.
matchers matchersTree
// handler responsible for handling the route.
handler tcp.Handler
// Used to disambiguate between two (or more) rules that would both match for a
// given request.
// Computed from the matching rule length, if not user-set.
priority int
}
// matcher is a matcher func used to match connection properties.
type matcher func(meta ConnData) bool
// matchersTree represents the matchers tree structure.
type matchersTree struct {
// If matcher is not nil, it means that this matcherTree is a leaf of the tree.
// It is therefore mutually exclusive with left and right.
matcher matcher
// operator to combine the evaluation of left and right leaves.
operator string
// Mutually exclusive with matcher.
left *matchersTree
right *matchersTree
}
func (m *matchersTree) match(meta ConnData) bool {
if m == nil {
// This should never happen as it should have been detected during parsing.
log.WithoutContext().Warnf("Rule matcher is nil")
return false
}
if m.matcher != nil {
return m.matcher(meta)
}
switch m.operator {
case "or":
return m.left.match(meta) || m.right.match(meta)
case "and":
return m.left.match(meta) && m.right.match(meta)
default:
// This should never happen as it should have been detected during parsing.
log.WithoutContext().Warnf("Invalid rule operator %s", m.operator)
return false
}
}
func clientIP(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.WithoutContext().Warnf("\"ClientIP\" matcher: could not match remote address: %v", err)
return false
}
return ok
}
return nil
}
var almostFQDN = regexp.MustCompile(`^[[:alnum:]\.-]+$`)
// hostSNI checks if the SNI Host of the connection match the matcher host.
func hostSNI(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 !almostFQDN.MatchString(host) {
return fmt.Errorf("invalid value for \"HostSNI\" matcher, %q is not a valid hostname", 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
}
// hostSNIRegexp checks if the SNI Host of the connection matches the matcher host regexp.
func hostSNIRegexp(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
}
// TODO: expose more of containous/mux fork to get rid of the following copied code (https://github.com/containous/mux/blob/8ffa4f6d063c/regexp.go).
// 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
var err error
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)
// Append variable name and compiled pattern.
if err != nil {
return "", err
}
}
// 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
}

947
pkg/muxer/tcp/mux_test.go Normal file
View File

@@ -0,0 +1,947 @@
package tcp
import (
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/tcp"
)
type fakeConn struct {
call map[string]int
remoteAddr net.Addr
}
func (f *fakeConn) Read(b []byte) (n int, err error) {
panic("implement me")
}
func (f *fakeConn) Write(b []byte) (n int, err error) {
f.call[string(b)]++
return len(b), nil
}
func (f *fakeConn) Close() error {
panic("implement me")
}
func (f *fakeConn) LocalAddr() net.Addr {
panic("implement me")
}
func (f *fakeConn) RemoteAddr() net.Addr {
return f.remoteAddr
}
func (f *fakeConn) SetDeadline(t time.Time) error {
panic("implement me")
}
func (f *fakeConn) SetReadDeadline(t time.Time) error {
panic("implement me")
}
func (f *fakeConn) SetWriteDeadline(t time.Time) error {
panic("implement me")
}
func (f *fakeConn) CloseWrite() error {
panic("implement me")
}
func Test_addTCPRoute(t *testing.T) {
testCases := []struct {
desc string
rule string
serverName string
remoteAddr string
routeErr bool
matchErr bool
}{
{
desc: "no tree",
routeErr: true,
},
{
desc: "Rule with no matcher",
rule: "rulewithnotmatcher",
routeErr: true,
},
{
desc: "Empty HostSNI rule",
rule: "HostSNI()",
serverName: "foobar",
routeErr: true,
},
{
desc: "Empty HostSNI rule",
rule: "HostSNI(``)",
serverName: "foobar",
routeErr: true,
},
{
desc: "Valid HostSNI rule matching",
rule: "HostSNI(`foobar`)",
serverName: "foobar",
},
{
desc: "Valid negative HostSNI rule matching",
rule: "!HostSNI(`bar`)",
serverName: "foobar",
},
{
desc: "Valid HostSNI rule matching with alternative case",
rule: "hostsni(`foobar`)",
serverName: "foobar",
},
{
desc: "Valid HostSNI rule matching with alternative case",
rule: "HOSTSNI(`foobar`)",
serverName: "foobar",
},
{
desc: "Valid HostSNI rule not matching",
rule: "HostSNI(`foobar`)",
serverName: "bar",
matchErr: true,
},
{
desc: "Empty HostSNIRegexp rule",
rule: "HostSNIRegexp()",
serverName: "foobar",
routeErr: true,
},
{
desc: "Empty HostSNIRegexp rule",
rule: "HostSNIRegexp(``)",
serverName: "foobar",
routeErr: true,
},
{
desc: "Valid HostSNIRegexp rule matching",
rule: "HostSNIRegexp(`{subdomain:[a-z]+}.foobar`)",
serverName: "sub.foobar",
},
{
desc: "Valid negative HostSNIRegexp rule matching",
rule: "!HostSNIRegexp(`bar`)",
serverName: "foobar",
},
{
desc: "Valid HostSNIRegexp rule matching with alternative case",
rule: "hostsniregexp(`foobar`)",
serverName: "foobar",
},
{
desc: "Valid HostSNIRegexp rule matching with alternative case",
rule: "HOSTSNIREGEXP(`foobar`)",
serverName: "foobar",
},
{
desc: "Valid HostSNIRegexp rule not matching",
rule: "HostSNIRegexp(`foobar`)",
serverName: "bar",
matchErr: true,
},
{
desc: "Valid negative HostSNI rule not matching",
rule: "!HostSNI(`bar`)",
serverName: "bar",
matchErr: true,
},
{
desc: "Valid HostSNIRegexp rule matching empty servername",
rule: "HostSNIRegexp(`{subdomain:[a-z]*}`)",
serverName: "",
},
{
desc: "Valid HostSNIRegexp rule with one name",
rule: "HostSNIRegexp(`{dummy}`)",
serverName: "toto",
},
{
desc: "Valid HostSNIRegexp rule with one name 2",
rule: "HostSNIRegexp(`{dummy}`)",
serverName: "toto.com",
matchErr: true,
},
{
desc: "Empty ClientIP rule",
rule: "ClientIP()",
routeErr: true,
},
{
desc: "Empty ClientIP rule",
rule: "ClientIP(``)",
routeErr: true,
},
{
desc: "Invalid ClientIP",
rule: "ClientIP(`invalid`)",
routeErr: true,
},
{
desc: "Invalid remoteAddr",
rule: "ClientIP(`10.0.0.1`)",
remoteAddr: "not.an.IP:80",
matchErr: true,
},
{
desc: "Valid ClientIP rule matching",
rule: "ClientIP(`10.0.0.1`)",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative ClientIP rule matching",
rule: "!ClientIP(`20.0.0.1`)",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid ClientIP rule matching with alternative case",
rule: "clientip(`10.0.0.1`)",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid ClientIP rule matching with alternative case",
rule: "CLIENTIP(`10.0.0.1`)",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid ClientIP rule not matching",
rule: "ClientIP(`10.0.0.1`)",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid negative ClientIP rule not matching",
rule: "!ClientIP(`10.0.0.2`)",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid ClientIP rule matching IPv6",
rule: "ClientIP(`10::10`)",
remoteAddr: "[10::10]:80",
},
{
desc: "Valid negative ClientIP rule matching IPv6",
rule: "!ClientIP(`10::10`)",
remoteAddr: "[::1]:80",
},
{
desc: "Valid ClientIP rule not matching IPv6",
rule: "ClientIP(`10::10`)",
remoteAddr: "[::1]:80",
matchErr: true,
},
{
desc: "Valid ClientIP rule matching multiple IPs",
rule: "ClientIP(`10.0.0.1`, `10.0.0.0`)",
remoteAddr: "10.0.0.0:80",
},
{
desc: "Valid ClientIP rule matching CIDR",
rule: "ClientIP(`11.0.0.0/24`)",
remoteAddr: "11.0.0.0:80",
},
{
desc: "Valid ClientIP rule not matching CIDR",
rule: "ClientIP(`11.0.0.0/24`)",
remoteAddr: "10.0.0.0:80",
matchErr: true,
},
{
desc: "Valid ClientIP rule matching CIDR IPv6",
rule: "ClientIP(`11::/16`)",
remoteAddr: "[11::]:80",
},
{
desc: "Valid ClientIP rule not matching CIDR IPv6",
rule: "ClientIP(`11::/16`)",
remoteAddr: "[10::]:80",
matchErr: true,
},
{
desc: "Valid ClientIP rule matching multiple CIDR",
rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0/16`)",
remoteAddr: "10.0.0.0:80",
},
{
desc: "Valid ClientIP rule not matching CIDR and matching IP",
rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0`)",
remoteAddr: "10.0.0.0:80",
},
{
desc: "Valid ClientIP rule matching CIDR and not matching IP",
rule: "ClientIP(`11.0.0.0`, `10.0.0.0/16`)",
remoteAddr: "10.0.0.0:80",
},
{
desc: "Valid HostSNI and ClientIP rule matching",
rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative HostSNI and ClientIP rule matching",
rule: "!HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI and negative ClientIP rule matching",
rule: "HostSNI(`foobar`) && !ClientIP(`10.0.0.2`)",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative HostSNI and negative ClientIP rule matching",
rule: "!HostSNI(`bar`) && !ClientIP(`10.0.0.2`)",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative HostSNI or negative ClientIP rule matching",
rule: "!(HostSNI(`bar`) || ClientIP(`10.0.0.2`))",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative HostSNI and negative ClientIP rule matching",
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
serverName: "foobar",
remoteAddr: "10.0.0.2:80",
},
{
desc: "Valid negative HostSNI and negative ClientIP rule matching",
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative HostSNI and negative ClientIP rule matching",
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
serverName: "bar",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid negative HostSNI and negative ClientIP rule matching",
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI and ClientIP rule not matching",
rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP rule not matching",
rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid HostSNI or ClientIP rule matching",
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI or ClientIP rule matching",
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI or ClientIP rule matching",
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.2:80",
},
{
desc: "Valid HostSNI or ClientIP rule not matching",
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
serverName: "bar",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid HostSNI x 3 OR rule matching",
rule: "HostSNI(`foobar`) || HostSNI(`foo`) || HostSNI(`bar`)",
serverName: "foobar",
},
{
desc: "Valid HostSNI x 3 OR rule not matching",
rule: "HostSNI(`foobar`) || HostSNI(`foo`) || HostSNI(`bar`)",
serverName: "baz",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP Combined rule matching",
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.2:80",
},
{
desc: "Valid HostSNI and ClientIP Combined rule matching",
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI and ClientIP Combined rule not matching",
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
serverName: "bar",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP Combined rule not matching",
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
serverName: "baz",
remoteAddr: "10.0.0.1:80",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP complex combined rule matching",
rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI and ClientIP complex combined rule not matching",
rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
serverName: "baz",
remoteAddr: "10.0.0.1:80",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP complex combined rule not matching",
rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
serverName: "bar",
remoteAddr: "10.0.0.3:80",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP more complex (but absurd) combined rule matching",
rule: "(HostSNI(`foobar`) || (HostSNI(`bar`) && !HostSNI(`foobar`))) && ((ClientIP(`10.0.0.1`) && !ClientIP(`10.0.0.2`)) || ClientIP(`10.0.0.2`)) ",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
msg := "BYTES"
handler := tcp.HandlerFunc(func(conn tcp.WriteCloser) {
_, err := conn.Write([]byte(msg))
require.NoError(t, err)
})
router, err := NewMuxer()
require.NoError(t, err)
err = router.AddRoute(test.rule, 0, handler)
if test.routeErr {
require.Error(t, err)
return
}
require.NoError(t, err)
addr := "0.0.0.0:0"
if test.remoteAddr != "" {
addr = test.remoteAddr
}
conn := &fakeConn{
call: map[string]int{},
remoteAddr: fakeAddr{addr: addr},
}
connData, err := NewConnData(test.serverName, conn)
require.NoError(t, err)
matchingHandler := router.Match(connData)
if test.matchErr {
require.Nil(t, matchingHandler)
return
}
require.NotNil(t, matchingHandler)
matchingHandler.ServeTCP(conn)
n, ok := conn.call[msg]
assert.Equal(t, n, 1)
assert.True(t, ok)
})
}
}
type fakeAddr struct {
addr string
}
func (f fakeAddr) String() string {
return f.addr
}
func (f fakeAddr) Network() string {
panic("Implement me")
}
func TestParseHostSNI(t *testing.T) {
testCases := []struct {
description string
expression string
domain []string
errorExpected bool
}{
{
description: "Unknown rule",
expression: "Foobar(`foo.bar`,`test.bar`)",
errorExpected: true,
},
{
description: "Many hostSNI rules",
expression: "HostSNI(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "Many hostSNI rules upper",
expression: "HOSTSNI(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "Many hostSNI rules lower",
expression: "hostsni(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "No hostSNI rule",
expression: "ClientIP(`10.1`)",
},
{
description: "HostSNI rule and another rule",
expression: "HostSNI(`foo.bar`) && ClientIP(`10.1`)",
domain: []string{"foo.bar"},
},
{
description: "HostSNI rule to lower and another rule",
expression: "HostSNI(`Foo.Bar`) && ClientIP(`10.1`)",
domain: []string{"foo.bar"},
},
{
description: "HostSNI rule with no domain",
expression: "HostSNI() && ClientIP(`10.1`)",
},
}
for _, test := range testCases {
test := test
t.Run(test.expression, func(t *testing.T) {
t.Parallel()
domains, err := ParseHostSNI(test.expression)
if test.errorExpected {
require.Errorf(t, err, "unable to parse correctly the domains in the HostSNI rule from %q", test.expression)
} else {
require.NoError(t, err, "%s: Error while parsing domain.", test.expression)
}
assert.EqualValues(t, test.domain, domains, "%s: Error parsing domains from expression.", test.expression)
})
}
}
func Test_HostSNI(t *testing.T) {
testCases := []struct {
desc string
ruleHosts []string
serverName string
buildErr bool
matchErr bool
}{
{
desc: "Empty",
buildErr: true,
},
{
desc: "Non ASCII host",
ruleHosts: []string{"héhé"},
buildErr: true,
},
{
desc: "Not Matching hosts",
ruleHosts: []string{"foobar"},
serverName: "bar",
matchErr: true,
},
{
desc: "Matching globing host `*`",
ruleHosts: []string{"*"},
serverName: "foobar",
},
{
desc: "Matching globing host `*` and empty serverName",
ruleHosts: []string{"*"},
serverName: "",
},
{
desc: "Matching globing host `*` and another non matching host",
ruleHosts: []string{"foo", "*"},
serverName: "bar",
},
{
desc: "Matching globing host `*` and another non matching host, and empty servername",
ruleHosts: []string{"foo", "*"},
serverName: "",
matchErr: true,
},
{
desc: "Not Matching globing host with subdomain",
ruleHosts: []string{"*.bar"},
buildErr: true,
},
{
desc: "Not Matching host with trailing dot with ",
ruleHosts: []string{"foobar."},
serverName: "foobar.",
},
{
desc: "Matching host with trailing dot",
ruleHosts: []string{"foobar."},
serverName: "foobar",
},
{
desc: "Matching hosts",
ruleHosts: []string{"foobar"},
serverName: "foobar",
},
{
desc: "Matching hosts with subdomains",
ruleHosts: []string{"foo.bar"},
serverName: "foo.bar",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
matcherTree := &matchersTree{}
err := hostSNI(matcherTree, test.ruleHosts...)
if test.buildErr {
require.Error(t, err)
return
}
require.NoError(t, err)
meta := ConnData{
serverName: test.serverName,
}
assert.Equal(t, test.matchErr, !matcherTree.match(meta))
})
}
}
func Test_HostSNIRegexp(t *testing.T) {
testCases := []struct {
desc string
pattern string
serverNames map[string]bool
buildErr bool
}{
{
desc: "unbalanced braces",
pattern: "subdomain:(foo\\.)?bar\\.com}",
buildErr: true,
},
{
desc: "empty group name",
pattern: "{:(foo\\.)?bar\\.com}",
buildErr: true,
},
{
desc: "empty capturing group",
pattern: "{subdomain:}",
buildErr: true,
},
{
desc: "malformed capturing group",
pattern: "{subdomain:(foo\\.?bar\\.com}",
buildErr: true,
},
{
desc: "not interpreted as a regexp",
pattern: "bar.com",
serverNames: map[string]bool{
"bar.com": true,
"barucom": false,
},
},
{
desc: "capturing group",
pattern: "{subdomain:(foo\\.)?bar\\.com}",
serverNames: map[string]bool{
"foo.bar.com": true,
"bar.com": true,
"fooubar.com": false,
"barucom": false,
"barcom": false,
},
},
{
desc: "non capturing group",
pattern: "{subdomain:(?:foo\\.)?bar\\.com}",
serverNames: map[string]bool{
"foo.bar.com": true,
"bar.com": true,
"fooubar.com": false,
"barucom": false,
"barcom": false,
},
},
{
desc: "regex insensitive",
pattern: "{dummy:[A-Za-z-]+\\.bar\\.com}",
serverNames: map[string]bool{
"FOO.bar.com": true,
"foo.bar.com": true,
"fooubar.com": false,
"barucom": false,
"barcom": false,
},
},
{
desc: "insensitive host",
pattern: "{dummy:[a-z-]+\\.bar\\.com}",
serverNames: map[string]bool{
"FOO.bar.com": true,
"foo.bar.com": true,
"fooubar.com": false,
"barucom": false,
"barcom": false,
},
},
{
desc: "insensitive host simple",
pattern: "foo.bar.com",
serverNames: map[string]bool{
"FOO.bar.com": true,
"foo.bar.com": true,
"fooubar.com": false,
"barucom": false,
"barcom": false,
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
matchersTree := &matchersTree{}
err := hostSNIRegexp(matchersTree, test.pattern)
if test.buildErr {
require.Error(t, err)
return
}
require.NoError(t, err)
for serverName, match := range test.serverNames {
meta := ConnData{
serverName: serverName,
}
assert.Equal(t, match, matchersTree.match(meta))
}
})
}
}
func Test_ClientIP(t *testing.T) {
testCases := []struct {
desc string
ruleCIDRs []string
remoteIP string
buildErr bool
matchErr bool
}{
{
desc: "Empty",
buildErr: true,
},
{
desc: "Malformed CIDR",
ruleCIDRs: []string{"héhé"},
buildErr: true,
},
{
desc: "Not matching empty remote IP",
ruleCIDRs: []string{"20.20.20.20"},
matchErr: true,
},
{
desc: "Not matching IP",
ruleCIDRs: []string{"20.20.20.20"},
remoteIP: "10.10.10.10",
matchErr: true,
},
{
desc: "Matching IP",
ruleCIDRs: []string{"10.10.10.10"},
remoteIP: "10.10.10.10",
},
{
desc: "Not matching multiple IPs",
ruleCIDRs: []string{"20.20.20.20", "30.30.30.30"},
remoteIP: "10.10.10.10",
matchErr: true,
},
{
desc: "Matching multiple IPs",
ruleCIDRs: []string{"10.10.10.10", "20.20.20.20", "30.30.30.30"},
remoteIP: "20.20.20.20",
},
{
desc: "Not matching CIDR",
ruleCIDRs: []string{"20.0.0.0/24"},
remoteIP: "10.10.10.10",
matchErr: true,
},
{
desc: "Matching CIDR",
ruleCIDRs: []string{"20.0.0.0/8"},
remoteIP: "20.10.10.10",
},
{
desc: "Not matching multiple CIDRs",
ruleCIDRs: []string{"10.0.0.0/24", "20.0.0.0/24"},
remoteIP: "10.10.10.10",
matchErr: true,
},
{
desc: "Matching multiple CIDRs",
ruleCIDRs: []string{"10.0.0.0/8", "20.0.0.0/8"},
remoteIP: "20.10.10.10",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
matchersTree := &matchersTree{}
err := clientIP(matchersTree, test.ruleCIDRs...)
if test.buildErr {
require.Error(t, err)
return
}
require.NoError(t, err)
meta := ConnData{
remoteIP: test.remoteIP,
}
assert.Equal(t, test.matchErr, !matchersTree.match(meta))
})
}
}
func Test_Priority(t *testing.T) {
testCases := []struct {
desc string
rules map[string]int
serverName string
remoteIP string
expectedRule string
}{
{
desc: "One matching rule, calculated priority",
rules: map[string]int{
"HostSNI(`bar`)": 0,
"HostSNI(`foobar`)": 0,
},
expectedRule: "HostSNI(`bar`)",
serverName: "bar",
},
{
desc: "One matching rule, custom priority",
rules: map[string]int{
"HostSNI(`foobar`)": 0,
"HostSNI(`bar`)": 10000,
},
expectedRule: "HostSNI(`foobar`)",
serverName: "foobar",
},
{
desc: "Two matching rules, calculated priority",
rules: map[string]int{
"HostSNI(`foobar`)": 0,
"HostSNI(`foobar`, `bar`)": 0,
},
expectedRule: "HostSNI(`foobar`, `bar`)",
serverName: "foobar",
},
{
desc: "Two matching rules, custom priority",
rules: map[string]int{
"HostSNI(`foobar`)": 10000,
"HostSNI(`foobar`, `bar`)": 0,
},
expectedRule: "HostSNI(`foobar`)",
serverName: "foobar",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
require.NoError(t, err)
matchedRule := ""
for rule, priority := range test.rules {
rule := rule
err := muxer.AddRoute(rule, priority, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
matchedRule = rule
}))
require.NoError(t, err)
}
handler := muxer.Match(ConnData{
serverName: test.serverName,
remoteIP: test.remoteIP,
})
require.NotNil(t, handler)
handler.ServeTCP(nil)
assert.Equal(t, test.expectedRule, matchedRule)
})
}
}

View File

@@ -17,8 +17,6 @@ const providerNameALPN = "tlsalpn.acme"
// ChallengeTLSALPN TLSALPN challenge provider implements challenge.Provider.
type ChallengeTLSALPN struct {
Timeout time.Duration
chans map[string]chan struct{}
muChans sync.Mutex
@@ -29,11 +27,10 @@ type ChallengeTLSALPN struct {
}
// NewChallengeTLSALPN creates a new ChallengeTLSALPN.
func NewChallengeTLSALPN(timeout time.Duration) *ChallengeTLSALPN {
func NewChallengeTLSALPN() *ChallengeTLSALPN {
return &ChallengeTLSALPN{
Timeout: timeout,
chans: make(map[string]chan struct{}),
certs: make(map[string]*Certificate),
chans: make(map[string]chan struct{}),
certs: make(map[string]*Certificate),
}
}
@@ -61,12 +58,13 @@ func (c *ChallengeTLSALPN) Present(domain, _, keyAuth string) error {
c.configurationChan <- conf
timer := time.NewTimer(c.Timeout)
// Present should return when its dynamic configuration has been received and applied by Traefik.
// The timer exists in case the above does not happen, to ensure the challenge cleanup.
timer := time.NewTimer(time.Minute)
defer timer.Stop()
select {
case t := <-timer.C:
timer.Stop()
c.muChans.Lock()
c.cleanChan(string(certPEMBlock))
c.muChans.Unlock()
@@ -103,6 +101,11 @@ func (c *ChallengeTLSALPN) Init() error {
return nil
}
// ThrottleDuration returns the throttle duration.
func (c *ChallengeTLSALPN) ThrottleDuration() time.Duration {
return 0
}
// Provide allows the provider to provide configurations to traefik using the given configuration channel.
func (c *ChallengeTLSALPN) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
c.configurationChan = configurationChan

View File

@@ -21,7 +21,8 @@ import (
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/rules"
httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http"
tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp"
"github.com/traefik/traefik/v2/pkg/safe"
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
"github.com/traefik/traefik/v2/pkg/types"
@@ -180,6 +181,11 @@ func isAccountMatchingCaServer(ctx context.Context, accountURI, serverURI string
return cau.Hostname() == aru.Hostname()
}
// ThrottleDuration returns the throttle duration.
func (p *Provider) ThrottleDuration() time.Duration {
return 0
}
// Provide allows the file provider to provide configurations to traefik
// using the given Configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
@@ -418,7 +424,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
})
}
} else {
domains, err := rules.ParseHostSNI(route.Rule)
domains, err := tcpmuxer.ParseHostSNI(route.Rule)
if err != nil {
logger.Errorf("Error parsing domains in provider ACME: %v", err)
continue
@@ -447,7 +453,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
})
}
} else {
domains, err := rules.ParseDomains(route.Rule)
domains, err := httpmuxer.ParseDomains(route.Rule)
if err != nil {
log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
continue

View File

@@ -1,6 +1,9 @@
package aggregator
import (
"context"
"time"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/config/static"
"github.com/traefik/traefik/v2/pkg/log"
@@ -11,16 +14,63 @@ import (
"github.com/traefik/traefik/v2/pkg/safe"
)
// throttled defines what kind of config refresh throttling the aggregator should
// set up for a given provider.
// If a provider implements throttled, the configuration changes it sends will be
// taken into account no more often than the frequency inferred from ThrottleDuration().
// If ThrottleDuration returns zero, no throttling will take place.
// If throttled is not implemented, the throttling will be set up in accordance
// with the global providersThrottleDuration option.
type throttled interface {
ThrottleDuration() time.Duration
}
// maybeThrottledProvide returns the Provide method of the given provider,
// potentially augmented with some throttling depending on whether and how the
// provider implements the throttled interface.
func maybeThrottledProvide(prd provider.Provider, defaultDuration time.Duration) func(chan<- dynamic.Message, *safe.Pool) error {
providerThrottleDuration := defaultDuration
if throttled, ok := prd.(throttled); ok {
// per-provider throttling
providerThrottleDuration = throttled.ThrottleDuration()
}
if providerThrottleDuration == 0 {
// throttling disabled
return prd.Provide
}
return func(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
rc := newRingChannel()
pool.GoCtx(func(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case msg := <-rc.out():
configurationChan <- msg
time.Sleep(providerThrottleDuration)
}
}
})
return prd.Provide(rc.in(), pool)
}
}
// ProviderAggregator aggregates providers.
type ProviderAggregator struct {
internalProvider provider.Provider
fileProvider provider.Provider
providers []provider.Provider
internalProvider provider.Provider
fileProvider provider.Provider
providers []provider.Provider
providersThrottleDuration time.Duration
}
// NewProviderAggregator returns an aggregate of all the providers configured in the static configuration.
func NewProviderAggregator(conf static.Providers) ProviderAggregator {
p := ProviderAggregator{}
p := ProviderAggregator{
providersThrottleDuration: time.Duration(conf.ProvidersThrottleDuration),
}
if conf.File != nil {
p.quietAddProvider(conf.File)
@@ -119,26 +169,26 @@ func (p ProviderAggregator) Init() error {
// Provide calls the provide method of every providers.
func (p ProviderAggregator) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
if p.fileProvider != nil {
launchProvider(configurationChan, pool, p.fileProvider)
p.launchProvider(configurationChan, pool, p.fileProvider)
}
for _, prd := range p.providers {
prd := prd
safe.Go(func() {
launchProvider(configurationChan, pool, prd)
p.launchProvider(configurationChan, pool, prd)
})
}
// internal provider must be the last because we use it to know if all the providers are loaded.
// ConfigurationWatcher will wait for this requiredProvider before applying configurations.
if p.internalProvider != nil {
launchProvider(configurationChan, pool, p.internalProvider)
p.launchProvider(configurationChan, pool, p.internalProvider)
}
return nil
}
func launchProvider(configurationChan chan<- dynamic.Message, pool *safe.Pool, prd provider.Provider) {
func (p ProviderAggregator) launchProvider(configurationChan chan<- dynamic.Message, pool *safe.Pool, prd provider.Provider) {
jsonConf, err := redactor.RemoveCredentials(prd)
if err != nil {
log.WithoutContext().Debugf("Cannot marshal the provider configuration %T: %v", prd, err)
@@ -147,9 +197,8 @@ func launchProvider(configurationChan chan<- dynamic.Message, pool *safe.Pool, p
log.WithoutContext().Infof("Starting provider %T", prd)
log.WithoutContext().Debugf("%T provider configuration: %s", prd, jsonConf)
currentProvider := prd
err = currentProvider.Provide(configurationChan, pool)
if err != nil {
if err := maybeThrottledProvide(prd, p.providersThrottleDuration)(configurationChan, pool); err != nil {
log.WithoutContext().Errorf("Cannot start the provider %T: %v", prd, err)
return
}
}

View File

@@ -0,0 +1,71 @@
package aggregator
import (
"github.com/traefik/traefik/v2/pkg/config/dynamic"
)
// RingChannel implements a channel in a way that never blocks the writer.
// Specifically, if a value is written to a RingChannel when its buffer is full then the oldest
// value in the buffer is discarded to make room (just like a standard ring-buffer).
// Note that Go's scheduler can cause discarded values when they could be avoided, simply by scheduling
// the writer before the reader, so caveat emptor.
type RingChannel struct {
input, output chan dynamic.Message
buffer *dynamic.Message
}
func newRingChannel() *RingChannel {
ch := &RingChannel{
input: make(chan dynamic.Message),
output: make(chan dynamic.Message),
}
go ch.ringBuffer()
return ch
}
func (ch *RingChannel) in() chan<- dynamic.Message {
return ch.input
}
func (ch *RingChannel) out() <-chan dynamic.Message {
return ch.output
}
// for all buffered cases.
func (ch *RingChannel) ringBuffer() {
var input, output chan dynamic.Message
var next dynamic.Message
input = ch.input
for input != nil || output != nil {
select {
// Prefer to write if possible, which is surprisingly effective in reducing
// dropped elements due to overflow. The naive read/write select chooses randomly
// when both channels are ready, which produces unnecessary drops 50% of the time.
case output <- next:
ch.buffer = nil
default:
select {
case elem, open := <-input:
if !open {
input = nil
break
}
ch.buffer = &elem
case output <- next:
ch.buffer = nil
}
}
if ch.buffer == nil {
output = nil
continue
}
output = ch.output
next = *ch.buffer
}
close(ch.output)
}

View File

@@ -12,8 +12,6 @@ import (
type connectCert struct {
root []string
leaf keyPair
// err is used to propagate to the caller (Provide) any error occurring within the certificate watcher goroutines.
err error
}
func (c *connectCert) getRoot() []traefiktls.FileOrContent {

View File

@@ -56,10 +56,12 @@ type Provider struct {
ConnectByDefault bool `description:"Consider every service as Connect capable by default." json:"connectByDefault,omitempty" toml:"connectByDefault,omitempty" yaml:"connectByDefault,omitempty" export:"true"`
ServiceName string `description:"Name of the Traefik service in Consul Catalog (needs to be registered via the orchestrator or manually)." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"`
Namespace string `description:"Sets the namespace used to discover services (Consul Enterprise only)." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty" export:"true"`
Watch bool `description:"Watch Consul API events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"`
client *api.Client
defaultRuleTpl *template.Template
certChan chan *connectCert
client *api.Client
defaultRuleTpl *template.Template
certChan chan *connectCert
watchServicesChan chan struct{}
}
// EndpointConfig holds configurations of the endpoint.
@@ -98,7 +100,9 @@ func (p *Provider) Init() error {
}
p.defaultRuleTpl = defaultRuleTpl
p.certChan = make(chan *connectCert)
p.certChan = make(chan *connectCert, 1)
p.watchServicesChan = make(chan struct{}, 1)
return nil
}
@@ -107,27 +111,31 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
var err error
p.client, err = createClient(p.Namespace, p.Endpoint)
if err != nil {
return fmt.Errorf("unable to create consul client: %w", err)
return fmt.Errorf("failed to create consul client: %w", err)
}
if p.ConnectAware {
leafWatcher, rootWatcher, err := p.createConnectTLSWatchers()
if err != nil {
return fmt.Errorf("unable to create consul watch plans: %w", err)
}
pool.GoCtx(func(routineCtx context.Context) {
p.watchConnectTLS(routineCtx, leafWatcher, rootWatcher)
})
}
var certInfo *connectCert
pool.GoCtx(func(routineCtx context.Context) {
ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "consulcatalog"))
logger := log.FromContext(ctxLog)
operation := func() error {
var err error
ctx, cancel := context.WithCancel(ctxLog)
// When the operation terminates, we want to clean up the
// goroutines in watchConnectTLS and watchServices.
defer cancel()
errChan := make(chan error, 2)
if p.ConnectAware {
go func() {
if err := p.watchConnectTLS(ctx); err != nil {
errChan <- fmt.Errorf("failed to watch connect certificates: %w", err)
}
}()
}
var certInfo *connectCert
// If we are running in connect aware mode then we need to
// make sure that we obtain the certificates before starting
@@ -137,37 +145,46 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
if p.ConnectAware && !certInfo.isReady() {
logger.Infof("Waiting for Connect certificate before building first configuration")
select {
case <-routineCtx.Done():
case <-ctx.Done():
return nil
case err = <-errChan:
return err
case certInfo = <-p.certChan:
if certInfo.err != nil {
return backoff.Permanent(err)
}
}
}
// get configuration at the provider's startup.
err = p.loadConfiguration(ctxLog, certInfo, configurationChan)
if err != nil {
if err = p.loadConfiguration(ctx, certInfo, configurationChan); err != nil {
return fmt.Errorf("failed to get consul catalog data: %w", err)
}
// Periodic refreshes.
ticker := time.NewTicker(time.Duration(p.RefreshInterval))
defer ticker.Stop()
go func() {
// Periodic refreshes.
if !p.Watch {
repeatSend(ctx, time.Duration(p.RefreshInterval), p.watchServicesChan)
return
}
if err := p.watchServices(ctx); err != nil {
errChan <- fmt.Errorf("failed to watch services: %w", err)
}
}()
for {
select {
case <-routineCtx.Done():
case <-ctx.Done():
return nil
case <-ticker.C:
case err = <-errChan:
return err
case certInfo = <-p.certChan:
if certInfo.err != nil {
return backoff.Permanent(err)
}
case <-p.watchServicesChan:
}
err = p.loadConfiguration(ctxLog, certInfo, configurationChan)
if err != nil {
if err = p.loadConfiguration(ctx, certInfo, configurationChan); err != nil {
return fmt.Errorf("failed to refresh consul catalog data: %w", err)
}
}
@@ -330,6 +347,67 @@ func (p *Provider) fetchService(ctx context.Context, name string, connectEnabled
return consulServices, statuses, err
}
// watchServices watches for update events of the services list and statuses,
// and transmits them to the caller through the p.watchServicesChan.
func (p *Provider) watchServices(ctx context.Context) error {
servicesWatcher, err := watch.Parse(map[string]interface{}{"type": "services"})
if err != nil {
return fmt.Errorf("failed to create services watcher plan: %w", err)
}
servicesWatcher.HybridHandler = func(_ watch.BlockingParamVal, _ interface{}) {
select {
case <-ctx.Done():
case p.watchServicesChan <- struct{}{}:
default:
// Event chan is full, discard event.
}
}
checksWatcher, err := watch.Parse(map[string]interface{}{"type": "checks"})
if err != nil {
return fmt.Errorf("failed to create checks watcher plan: %w", err)
}
checksWatcher.HybridHandler = func(_ watch.BlockingParamVal, _ interface{}) {
select {
case <-ctx.Done():
case p.watchServicesChan <- struct{}{}:
default:
// Event chan is full, discard event.
}
}
logger := hclog.New(&hclog.LoggerOptions{
Name: "consulcatalog",
Level: hclog.LevelFromString(logrus.GetLevel().String()),
JSONFormat: true,
})
errChan := make(chan error, 2)
defer func() {
servicesWatcher.Stop()
checksWatcher.Stop()
}()
go func() {
errChan <- servicesWatcher.RunWithClientAndHclog(p.client, logger)
}()
go func() {
errChan <- checksWatcher.RunWithClientAndHclog(p.client, logger)
}()
select {
case <-ctx.Done():
return nil
case err = <-errChan:
return fmt.Errorf("services or checks watcher terminated: %w", err)
}
}
func rootsWatchHandler(ctx context.Context, dest chan<- []string) func(watch.BlockingParamVal, interface{}) {
return func(_ watch.BlockingParamVal, raw interface{}) {
if raw == nil {
@@ -348,7 +426,10 @@ func rootsWatchHandler(ctx context.Context, dest chan<- []string) func(watch.Blo
roots = append(roots, root.RootCertPEM)
}
dest <- roots
select {
case <-ctx.Done():
case dest <- roots:
}
}
}
@@ -370,65 +451,59 @@ func leafWatcherHandler(ctx context.Context, dest chan<- keyPair) func(watch.Blo
return
}
dest <- keyPair{
kp := keyPair{
cert: v.CertPEM,
key: v.PrivateKeyPEM,
}
select {
case <-ctx.Done():
case dest <- kp:
}
}
}
func (p *Provider) createConnectTLSWatchers() (*watch.Plan, *watch.Plan, error) {
// watchConnectTLS watches for updates of the root certificate or the leaf
// certificate, and transmits them to the caller via p.certChan.
func (p *Provider) watchConnectTLS(ctx context.Context) error {
leafChan := make(chan keyPair)
leafWatcher, err := watch.Parse(map[string]interface{}{
"type": "connect_leaf",
"service": p.ServiceName,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to create leaf cert watcher plan: %w", err)
return fmt.Errorf("failed to create leaf cert watcher plan: %w", err)
}
leafWatcher.HybridHandler = leafWatcherHandler(ctx, leafChan)
rootWatcher, err := watch.Parse(map[string]interface{}{
rootsChan := make(chan []string)
rootsWatcher, err := watch.Parse(map[string]interface{}{
"type": "connect_roots",
})
if err != nil {
return nil, nil, fmt.Errorf("failed to create root cert watcher plan: %w", err)
return fmt.Errorf("failed to create roots cert watcher plan: %w", err)
}
rootsWatcher.HybridHandler = rootsWatchHandler(ctx, rootsChan)
return leafWatcher, rootWatcher, nil
}
// watchConnectTLS watches for updates of the root certificate or the leaf
// certificate, and transmits them to the caller via p.certChan. Any error is also
// propagated up through p.certChan, in connectCert.err.
func (p *Provider) watchConnectTLS(ctx context.Context, leafWatcher *watch.Plan, rootWatcher *watch.Plan) {
ctxLog := log.With(ctx, log.Str(log.ProviderName, "consulcatalog"))
logger := log.FromContext(ctxLog)
leafChan := make(chan keyPair)
rootChan := make(chan []string)
leafWatcher.HybridHandler = leafWatcherHandler(ctx, leafChan)
rootWatcher.HybridHandler = rootsWatchHandler(ctx, rootChan)
logOpts := &hclog.LoggerOptions{
hclogger := hclog.New(&hclog.LoggerOptions{
Name: "consulcatalog",
Level: hclog.LevelFromString(logrus.GetLevel().String()),
JSONFormat: true,
}
})
hclogger := hclog.New(logOpts)
errChan := make(chan error, 2)
go func() {
err := leafWatcher.RunWithClientAndHclog(p.client, hclogger)
if err != nil {
p.certChan <- &connectCert{err: err}
}
defer func() {
leafWatcher.Stop()
rootsWatcher.Stop()
}()
go func() {
err := rootWatcher.RunWithClientAndHclog(p.client, hclogger)
if err != nil {
p.certChan <- &connectCert{err: err}
}
errChan <- leafWatcher.RunWithClientAndHclog(p.client, hclogger)
}()
go func() {
errChan <- rootsWatcher.RunWithClientAndHclog(p.client, hclogger)
}()
var (
@@ -440,20 +515,28 @@ func (p *Provider) watchConnectTLS(ctx context.Context, leafWatcher *watch.Plan,
for {
select {
case <-ctx.Done():
leafWatcher.Stop()
rootWatcher.Stop()
return
case rootCerts = <-rootChan:
return nil
case err := <-errChan:
return fmt.Errorf("leaf or roots watcher terminated: %w", err)
case rootCerts = <-rootsChan:
case leafCerts = <-leafChan:
}
newCertInfo := &connectCert{
root: rootCerts,
leaf: leafCerts,
}
if newCertInfo.isReady() && !newCertInfo.equals(certInfo) {
logger.Debugf("Updating connect certs for service %s", p.ServiceName)
log.FromContext(ctx).Debugf("Updating connect certs for service %s", p.ServiceName)
certInfo = newCertInfo
p.certChan <- newCertInfo
select {
case <-ctx.Done():
case p.certChan <- newCertInfo:
}
}
}
}
@@ -487,3 +570,25 @@ func createClient(namespace string, endpoint *EndpointConfig) (*api.Client, erro
return api.NewClient(&config)
}
func repeatSend(ctx context.Context, interval time.Duration, c chan<- struct{}) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
select {
case <-ctx.Done():
return
case c <- struct{}{}:
default:
// Chan is full, discard event.
}
}
}
}

146
pkg/provider/hub/handler.go Normal file
View File

@@ -0,0 +1,146 @@
package hub
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sync/atomic"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log"
)
type handler struct {
mux *http.ServeMux
client http.Client
entryPoint string
port int
tlsCfg *TLS
// Accessed atomically.
lastCfgUnixNano int64
cfgChan chan<- dynamic.Message
}
func newHandler(entryPoint string, port int, cfgChan chan<- dynamic.Message, tlsCfg *TLS, client http.Client) http.Handler {
h := &handler{
mux: http.NewServeMux(),
entryPoint: entryPoint,
port: port,
cfgChan: cfgChan,
tlsCfg: tlsCfg,
client: client,
}
h.mux.HandleFunc("/config", h.handleConfig)
h.mux.HandleFunc("/discover-ip", h.handleDiscoverIP)
h.mux.HandleFunc("/state", h.handleState)
return h
}
type configRequest struct {
UnixNano int64 `json:"unixNano"`
Configuration *dynamic.Configuration `json:"configuration"`
}
func (h *handler) handleConfig(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
payload := &configRequest{Configuration: emptyDynamicConfiguration()}
if err := json.NewDecoder(req.Body).Decode(payload); err != nil {
err = fmt.Errorf("decoding config request: %w", err)
log.WithoutContext().Errorf("Handling config: %v", err)
http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
cfg := payload.Configuration
patchDynamicConfiguration(cfg, h.entryPoint, h.port, h.tlsCfg)
// We can safely drop messages here if the other end is not ready to receive them
// as the agent will re-apply the same configuration.
select {
case h.cfgChan <- dynamic.Message{ProviderName: "hub", Configuration: cfg}:
atomic.StoreInt64(&h.lastCfgUnixNano, payload.UnixNano)
default:
}
}
func (h *handler) handleDiscoverIP(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
xff := req.Header.Get("X-Forwarded-For")
port := req.URL.Query().Get("port")
nonce := req.URL.Query().Get("nonce")
if err := h.doDiscoveryReq(req.Context(), xff, port, nonce); err != nil {
err = fmt.Errorf("doing discovery request: %w", err)
log.WithoutContext().Errorf("Handling IP discovery: %v", err)
http.Error(rw, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
return
}
if err := json.NewEncoder(rw).Encode(xff); err != nil {
err = fmt.Errorf("encoding discover ip response: %w", err)
log.WithoutContext().Errorf("Handling IP discovery: %v", err)
http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
func (h *handler) doDiscoveryReq(ctx context.Context, ip, port, nonce string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s:%s", ip, port), http.NoBody)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
q := make(url.Values)
q.Set("nonce", nonce)
req.URL.RawQuery = q.Encode()
req.Host = "agent.traefik"
resp, err := h.client.Do(req)
if err != nil {
return fmt.Errorf("doing request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
return nil
}
type stateResponse struct {
LastConfigUnixNano int64 `json:"lastConfigUnixNano"`
}
func (h *handler) handleState(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
resp := stateResponse{
LastConfigUnixNano: atomic.LoadInt64(&h.lastCfgUnixNano),
}
if err := json.NewEncoder(rw).Encode(resp); err != nil {
err = fmt.Errorf("encoding last config received response: %w", err)
log.WithoutContext().Errorf("Handling state: %v", err)
http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
}
func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
h.mux.ServeHTTP(rw, req)
}

View File

@@ -0,0 +1,168 @@
package hub
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/tls/generate"
)
func TestHandleConfig(t *testing.T) {
cfgChan := make(chan dynamic.Message, 1)
client, err := createAgentClient(&TLS{Insecure: true})
require.NoError(t, err)
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
cfg := emptyDynamicConfiguration()
cfg.HTTP.Routers["foo"] = &dynamic.Router{
EntryPoints: []string{"ep"},
Service: "bar",
Rule: "Host(`foo.com`)",
}
req := configRequest{Configuration: cfg}
b, err := json.Marshal(req)
require.NoError(t, err)
server := httptest.NewServer(h)
t.Cleanup(server.Close)
resp, err := http.Post(server.URL+"/config", "application/json", bytes.NewReader(b))
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
select {
case gotCfgRaw := <-cfgChan:
patchDynamicConfiguration(cfg, "traefik-hub-ep", 42, nil)
assert.Equal(t, cfg, gotCfgRaw.Configuration)
case <-time.After(time.Second):
t.Fatal("Configuration not received")
}
}
func TestHandle_Config_MethodNotAllowed(t *testing.T) {
cfgChan := make(chan dynamic.Message, 1)
client, err := createAgentClient(&TLS{Insecure: true})
require.NoError(t, err)
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
server := httptest.NewServer(h)
t.Cleanup(server.Close)
resp, err := http.Get(server.URL + "/config")
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
}
func TestHandle_DiscoverIP(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
port := listener.Addr().(*net.TCPAddr).Port
nonce := "XVlBzgbaiCMRAjWw"
mux := http.NewServeMux()
var handlerCallCount int
mux.HandleFunc("/", func(_ http.ResponseWriter, req *http.Request) {
handlerCallCount++
assert.Equal(t, nonce, req.URL.Query().Get("nonce"))
})
certificate, err := generate.DefaultCertificate()
require.NoError(t, err)
agentServer := &http.Server{
Handler: mux,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{*certificate},
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
},
}
t.Cleanup(func() { _ = agentServer.Close() })
rdy := make(chan struct{})
go func(s *http.Server) {
close(rdy)
if err = s.ServeTLS(listener, "", ""); errors.Is(err, http.ErrServerClosed) {
return
}
}(agentServer)
<-rdy
cfgChan := make(chan dynamic.Message, 1)
client, err := createAgentClient(&TLS{Insecure: true})
require.NoError(t, err)
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
traefikServer := httptest.NewServer(h)
t.Cleanup(traefikServer.Close)
req, err := http.NewRequest(http.MethodGet, traefikServer.URL+"/discover-ip", http.NoBody)
require.NoError(t, err)
q := make(url.Values)
q.Set("port", strconv.Itoa(port))
q.Set("nonce", nonce)
req.URL.RawQuery = q.Encode()
// Simulate a call from behind different proxies.
req.Header.Add("X-Forwarded-For", "127.0.0.1")
req.Header.Add("X-Forwarded-For", "10.10.0.13")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer func() {
err = resp.Body.Close()
require.NoError(t, err)
}()
assert.Equal(t, 1, handlerCallCount)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var ip string
err = json.NewDecoder(resp.Body).Decode(&ip)
require.NoError(t, err)
assert.Equal(t, "127.0.0.1", ip)
}
func TestHandle_DiscoverIP_MethodNotAllowed(t *testing.T) {
cfgChan := make(chan dynamic.Message, 1)
client, err := createAgentClient(&TLS{Insecure: true})
require.NoError(t, err)
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
server := httptest.NewServer(h)
t.Cleanup(server.Close)
resp, err := http.Post(server.URL+"/discover-ip", "", http.NoBody)
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
}

215
pkg/provider/hub/hub.go Normal file
View File

@@ -0,0 +1,215 @@
package hub
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/safe"
ttls "github.com/traefik/traefik/v2/pkg/tls"
)
var _ provider.Provider = (*Provider)(nil)
// DefaultEntryPointName is the name of the default internal entry point.
const DefaultEntryPointName = "traefik-hub"
// Provider holds configurations of the provider.
type Provider struct {
EntryPoint string `description:"Entrypoint that exposes data for Traefik Hub. It should be a dedicated one, and not used by any router." json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty" export:"true"`
TLS *TLS `description:"TLS configuration for mTLS communication between Traefik and Hub Agent." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
server *http.Server
}
// TLS configures the mTLS connection between Traefik Proxy and the Traefik Hub Agent.
type TLS struct {
Insecure bool `description:"Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
CA ttls.FileOrContent `description:"The certificate authority authenticates the Traefik Hub Agent certificate." json:"ca,omitempty" toml:"ca,omitempty" yaml:"ca,omitempty" loggable:"false"`
Cert ttls.FileOrContent `description:"The TLS certificate for Traefik Proxy as a TLS client." json:"cert,omitempty" toml:"cert,omitempty" yaml:"cert,omitempty" loggable:"false"`
Key ttls.FileOrContent `description:"The TLS key for Traefik Proxy as a TLS client." json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty" loggable:"false"`
}
// SetDefaults sets the default values.
func (p *Provider) SetDefaults() {
p.EntryPoint = DefaultEntryPointName
}
// Init the provider.
func (p *Provider) Init() error {
return nil
}
// Provide allows the hub provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return fmt.Errorf("listener: %w", err)
}
port := listener.Addr().(*net.TCPAddr).Port
client, err := createAgentClient(p.TLS)
if err != nil {
return fmt.Errorf("creating Hub Agent HTTP client: %w", err)
}
p.server = &http.Server{Handler: newHandler(p.EntryPoint, port, configurationChan, p.TLS, client)}
// TODO: this is going to be leaky (because no context to make it terminate)
// if/when Provide lifecycle differs with Traefik lifecycle.
go func() {
if err = p.server.Serve(listener); err != nil {
log.WithoutContext().WithField(log.ProviderName, "hub").Errorf("Unexpected error while running server: %v", err)
return
}
}()
exposeAPIAndMetrics(configurationChan, p.EntryPoint, port, p.TLS)
return nil
}
func exposeAPIAndMetrics(cfgChan chan<- dynamic.Message, ep string, port int, tlsCfg *TLS) {
cfg := emptyDynamicConfiguration()
patchDynamicConfiguration(cfg, ep, port, tlsCfg)
cfgChan <- dynamic.Message{ProviderName: "hub", Configuration: cfg}
}
func patchDynamicConfiguration(cfg *dynamic.Configuration, ep string, port int, tlsCfg *TLS) {
cfg.HTTP.Routers["traefik-hub-agent-api"] = &dynamic.Router{
EntryPoints: []string{ep},
Service: "api@internal",
Rule: "Host(`proxy.traefik`) && PathPrefix(`/api`)",
}
cfg.HTTP.Routers["traefik-hub-agent-metrics"] = &dynamic.Router{
EntryPoints: []string{ep},
Service: "prometheus@internal",
Rule: "Host(`proxy.traefik`) && PathPrefix(`/metrics`)",
}
cfg.HTTP.Routers["traefik-hub-agent-service"] = &dynamic.Router{
EntryPoints: []string{ep},
Service: "traefik-hub-agent-service",
Rule: "Host(`proxy.traefik`) && PathPrefix(`/config`, `/discover-ip`, `/state`)",
}
cfg.HTTP.Services["traefik-hub-agent-service"] = &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: fmt.Sprintf("http://127.0.0.1:%d", port),
},
},
},
}
if tlsCfg == nil {
return
}
if tlsCfg.Insecure {
cfg.TLS.Options["traefik-hub"] = ttls.Options{
MinVersion: "VersionTLS13",
}
return
}
cfg.TLS.Options["traefik-hub"] = ttls.Options{
ClientAuth: ttls.ClientAuth{
CAFiles: []ttls.FileOrContent{tlsCfg.CA},
ClientAuthType: "RequireAndVerifyClientCert",
},
SniStrict: true,
MinVersion: "VersionTLS13",
}
cfg.TLS.Certificates = append(cfg.TLS.Certificates, &ttls.CertAndStores{
Certificate: ttls.Certificate{
CertFile: tlsCfg.Cert,
KeyFile: tlsCfg.Key,
},
})
}
func emptyDynamicConfiguration() *dynamic.Configuration {
return &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
ServersTransports: make(map[string]*dynamic.ServersTransport),
},
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]ttls.Store),
Options: make(map[string]ttls.Options),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
}
}
func createAgentClient(tlsCfg *TLS) (http.Client, error) {
var client http.Client
if tlsCfg.Insecure {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
},
}
return client, nil
}
caContent, err := tlsCfg.CA.Read()
if err != nil {
return client, fmt.Errorf("reading CA: %w", err)
}
roots := x509.NewCertPool()
if ok := roots.AppendCertsFromPEM(caContent); !ok {
return client, errors.New("appending CA error")
}
certContent, err := tlsCfg.Cert.Read()
if err != nil {
return client, fmt.Errorf("reading Cert: %w", err)
}
keyContent, err := tlsCfg.Key.Read()
if err != nil {
return client, fmt.Errorf("reading Key: %w", err)
}
certificate, err := tls.X509KeyPair(certContent, keyContent)
if err != nil {
return client, fmt.Errorf("creating key pair: %w", err)
}
// mTLS
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: roots,
Certificates: []tls.Certificate{certificate},
ServerName: "agent.traefik",
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS13,
},
}
return client, nil
}

View File

@@ -229,3 +229,26 @@ subsets:
ports:
- name: web
port: 80
---
apiVersion: v1
kind: Service
metadata:
name: whoami-without-endpoints-subsets
namespace: default
spec:
ports:
- name: myapp
port: 80
selector:
app: traefiklabs
task: whoami
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoami-without-endpoints-subsets
namespace: default

View File

@@ -238,3 +238,26 @@ subsets:
ports:
- name: myapp
port: 8000
---
apiVersion: v1
kind: Service
metadata:
name: whoamitcp-without-endpoints-subsets
namespace: default
spec:
ports:
- name: myapp
port: 8000
selector:
app: traefiklabs
task: whoamitcp
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoamitcp-without-endpoints-subsets
namespace: default

View File

@@ -0,0 +1,15 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: HostSNI(`foo.com`)
services:
- name: whoamitcp-without-endpoints-subsets
port: 8000

View File

@@ -197,3 +197,26 @@ subsets:
ports:
- name: myapp
port: 8000
---
apiVersion: v1
kind: Service
metadata:
name: whoamiudp-without-endpoints-subsets
namespace: default
spec:
ports:
- name: myapp
port: 8000
selector:
app: traefiklabs
task: whoamiudp
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoamiudp-without-endpoints-subsets
namespace: default

View File

@@ -0,0 +1,14 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteUDP
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- services:
- name: whoamiudp-without-endpoints-subsets
port: 8000

View File

@@ -0,0 +1,17 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: Host(`foo.com`) && PathPrefix(`/bar`)
kind: Rule
priority: 12
services:
- name: whoami-without-endpoints-subsets
port: 80

View File

@@ -53,6 +53,7 @@ type Provider struct {
LabelSelector string `description:"Kubernetes label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
AllowEmptyServices bool `description:"Allow the creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
lastConfiguration safe.Safe
}

View File

@@ -50,7 +50,12 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
ingressName = ingressRoute.GenerateName
}
cb := configBuilder{client: client, allowCrossNamespace: p.AllowCrossNamespace, allowExternalNameServices: p.AllowExternalNameServices}
cb := configBuilder{
client: client,
allowCrossNamespace: p.AllowCrossNamespace,
allowExternalNameServices: p.AllowExternalNameServices,
allowEmptyServices: p.AllowEmptyServices,
}
for _, route := range ingressRoute.Spec.Routes {
if route.Kind != "Rule" {
@@ -193,6 +198,7 @@ type configBuilder struct {
client Client
allowCrossNamespace bool
allowExternalNameServices bool
allowEmptyServices bool
}
// buildTraefikService creates the configuration for the traefik service defined in tService,
@@ -387,7 +393,8 @@ func (c configBuilder) loadServers(parentNamespace string, svc v1alpha1.LoadBala
if !endpointsExists {
return nil, fmt.Errorf("endpoints not found for %s/%s", namespace, sanitizedName)
}
if len(endpoints.Subsets) == 0 {
if len(endpoints.Subsets) == 0 && !c.allowEmptyServices {
return nil, fmt.Errorf("subset not found for %s/%s", namespace, sanitizedName)
}

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