Merge branch 'v2.1' into master
This commit is contained in:
commit
da3d814c8b
56 changed files with 2162 additions and 558 deletions
32
CHANGELOG.md
32
CHANGELOG.md
|
@ -1,3 +1,35 @@
|
||||||
|
## [v2.1.2](https://github.com/containous/traefik/tree/v2.1.2) (2020-01-07)
|
||||||
|
[All Commits](https://github.com/containous/traefik/compare/v2.1.1...v2.1.2)
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
- **[authentication,middleware,tracing]** fix(tracing): makes sure tracing headers are being propagated when using forwardAuth ([#6072](https://github.com/containous/traefik/pull/6072) by [jcchavezs](https://github.com/jcchavezs))
|
||||||
|
- **[cli]** fix: invalid label/flag parsing. ([#6028](https://github.com/containous/traefik/pull/6028) by [ldez](https://github.com/ldez))
|
||||||
|
- **[consulcatalog]** Query consul catalog for service health separately ([#6046](https://github.com/containous/traefik/pull/6046) by [SantoDE](https://github.com/SantoDE))
|
||||||
|
- **[k8s,k8s/crd]** Restore ExternalName https support for Kubernetes CRD ([#6037](https://github.com/containous/traefik/pull/6037) by [kpeiruza](https://github.com/kpeiruza))
|
||||||
|
- **[k8s,k8s/crd]** Log the ignored namespace only when needed ([#6087](https://github.com/containous/traefik/pull/6087) by [jbdoumenjou](https://github.com/jbdoumenjou))
|
||||||
|
- **[k8s,k8s/ingress]** k8s Ingress: fix crash on rules with nil http ([#6121](https://github.com/containous/traefik/pull/6121) by [grimmy](https://github.com/grimmy))
|
||||||
|
- **[logs]** Improves error message when a configuration file is empty. ([#6135](https://github.com/containous/traefik/pull/6135) by [ldez](https://github.com/ldez))
|
||||||
|
- **[server]** Handle respondingTimeout and better shutdown tests. ([#6115](https://github.com/containous/traefik/pull/6115) by [juliens](https://github.com/juliens))
|
||||||
|
- **[server]** Don't set user-agent to Go-http-client/1.1 ([#6030](https://github.com/containous/traefik/pull/6030) by [sh7dm](https://github.com/sh7dm))
|
||||||
|
- **[tracing]** fix: Malformed x-b3-traceid Header ([#6079](https://github.com/containous/traefik/pull/6079) by [ldez](https://github.com/ldez))
|
||||||
|
- **[webui]** fix: dashboard redirect loop ([#6078](https://github.com/containous/traefik/pull/6078) by [ldez](https://github.com/ldez))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- **[acme]** Use consistent name in ACME documentation ([#6019](https://github.com/containous/traefik/pull/6019) by [ldez](https://github.com/ldez))
|
||||||
|
- **[api,k8s/crd]** Add a documentation example for dashboard and api for kubernetes CRD ([#6022](https://github.com/containous/traefik/pull/6022) by [dduportal](https://github.com/dduportal))
|
||||||
|
- **[cli]** Fix examples for the use of websecure via CLI ([#6116](https://github.com/containous/traefik/pull/6116) by [tiagoboeing](https://github.com/tiagoboeing))
|
||||||
|
- **[k8s,k8s/crd]** Improve documentation about Kubernetes IngressRoute ([#6058](https://github.com/containous/traefik/pull/6058) by [jbdoumenjou](https://github.com/jbdoumenjou))
|
||||||
|
- **[middleware]** Improve sourceRange explanation for ipWhiteList ([#6070](https://github.com/containous/traefik/pull/6070) by [der-domi](https://github.com/der-domi))
|
||||||
|
|
||||||
|
## [v2.1.1](https://github.com/containous/traefik/tree/v2.1.1) (2019-12-12)
|
||||||
|
[All Commits](https://github.com/containous/traefik/compare/v2.1.0...v2.1.1)
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
- **[logs,middleware,metrics]** CloseNotifier: return pointer instead of value ([#6010](https://github.com/containous/traefik/pull/6010) by [mpl](https://github.com/mpl))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- Add Migration Guide for Traefik v2.1 ([#6017](https://github.com/containous/traefik/pull/6017) by [SantoDE](https://github.com/SantoDE))
|
||||||
|
|
||||||
## [v2.1.0](https://github.com/containous/traefik/tree/v2.1.0) (2019-12-10)
|
## [v2.1.0](https://github.com/containous/traefik/tree/v2.1.0) (2019-12-10)
|
||||||
[All Commits](https://github.com/containous/traefik/compare/v2.0.0-rc1...v2.1.0)
|
[All Commits](https://github.com/containous/traefik/compare/v2.0.0-rc1...v2.1.0)
|
||||||
|
|
||||||
|
|
|
@ -172,7 +172,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
|
||||||
|
|
||||||
acmeProviders := initACMEProvider(staticConfiguration, &providerAggregator, tlsManager)
|
acmeProviders := initACMEProvider(staticConfiguration, &providerAggregator, tlsManager)
|
||||||
|
|
||||||
serverEntryPointsTCP, err := server.NewTCPEntryPoints(*staticConfiguration)
|
serverEntryPointsTCP, err := server.NewTCPEntryPoints(staticConfiguration.EntryPoints)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ Requirements:
|
||||||
|
|
||||||
- `go` v1.13+
|
- `go` v1.13+
|
||||||
- environment variable `GO111MODULE=on`
|
- environment variable `GO111MODULE=on`
|
||||||
- go-bindata `GO111MODULE=off go get -u github.com/containous/go-bindata/...`
|
- [go-bindata](https://github.com/containous/go-bindata) `GO111MODULE=off go get -u github.com/containous/go-bindata/...`
|
||||||
|
|
||||||
!!! tip "Source Directory"
|
!!! tip "Source Directory"
|
||||||
|
|
||||||
|
@ -100,30 +100,32 @@ Requirements:
|
||||||
#### Build Traefik
|
#### Build Traefik
|
||||||
|
|
||||||
Once you've set up your go environment and cloned the source repository, you can build Traefik.
|
Once you've set up your go environment and cloned the source repository, you can build Traefik.
|
||||||
Beforehand, you need to get `go-bindata` (the first time) in order to be able to use the `go generate` command (which is part of the build process).
|
|
||||||
|
Beforehand, you need to get [go-bindata](https://github.com/containous/go-bindata) (the first time) in order to be able to use the `go generate` command (which is part of the build process).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/go/src/github.com/containous/traefik
|
cd ~/go/src/github.com/containous/traefik
|
||||||
|
|
||||||
# Get go-bindata. (Important: the ellipses are required.)
|
# Get go-bindata. (Important: the ellipses are required.)
|
||||||
GO111MODULE=off go get github.com/containous/go-bindata/...
|
GO111MODULE=off go get github.com/containous/go-bindata/...
|
||||||
|
```
|
||||||
|
|
||||||
# Let's build
|
```bash
|
||||||
|
# Generate UI static files
|
||||||
|
rm -rf static/ autogen/; make generate-webui
|
||||||
|
|
||||||
# generate
|
# required to merge non-code components into the final binary,
|
||||||
# (required to merge non-code components into the final binary, such as the web dashboard and the provider's templates)
|
# such as the web dashboard/UI
|
||||||
go generate
|
go generate
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
# Standard go build
|
# Standard go build
|
||||||
go build ./cmd/traefik
|
go build ./cmd/traefik
|
||||||
```
|
```
|
||||||
|
|
||||||
You will find the Traefik executable (`traefik`) in the `~/go/src/github.com/containous/traefik` directory.
|
You will find the Traefik executable (`traefik`) in the `~/go/src/github.com/containous/traefik` directory.
|
||||||
|
|
||||||
### Updating the templates
|
|
||||||
|
|
||||||
If you happen to update the provider's templates (located in `/templates`), you must run `go generate` to update the `autogen` package.
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
### Method 1: `Docker` and `make`
|
### Method 1: `Docker` and `make`
|
||||||
|
|
|
@ -59,10 +59,10 @@ Please check the [configuration examples below](#configuration-examples) for mor
|
||||||
[entryPoints.web-secure]
|
[entryPoints.web-secure]
|
||||||
address = ":443"
|
address = ":443"
|
||||||
|
|
||||||
[certificatesResolvers.sample.acme]
|
[certificatesResolvers.le.acme]
|
||||||
email = "your-email@your-domain.org"
|
email = "your-email@your-domain.org"
|
||||||
storage = "acme.json"
|
storage = "acme.json"
|
||||||
[certificatesResolvers.sample.acme.httpChallenge]
|
[certificatesResolvers.le.acme.httpChallenge]
|
||||||
# used during the challenge
|
# used during the challenge
|
||||||
entryPoint = "web"
|
entryPoint = "web"
|
||||||
```
|
```
|
||||||
|
@ -89,10 +89,10 @@ Please check the [configuration examples below](#configuration-examples) for mor
|
||||||
--entryPoints.web.address=:80
|
--entryPoints.web.address=:80
|
||||||
--entryPoints.websecure.address=:443
|
--entryPoints.websecure.address=:443
|
||||||
# ...
|
# ...
|
||||||
--certificatesResolvers.sample.acme.email=your-email@your-domain.org
|
--certificatesResolvers.le.acme.email=your-email@your-domain.org
|
||||||
--certificatesResolvers.sample.acme.storage=acme.json
|
--certificatesResolvers.le.acme.storage=acme.json
|
||||||
# used during the challenge
|
# used during the challenge
|
||||||
--certificatesResolvers.sample.acme.httpChallenge.entryPoint=web
|
--certificatesResolvers.le.acme.httpChallenge.entryPoint=web
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! important "Defining a certificates resolver does not result in all routers automatically using it. Each router that is supposed to use the resolver must [reference](../routing/routers/index.md#certresolver) it."
|
!!! important "Defining a certificates resolver does not result in all routers automatically using it. Each router that is supposed to use the resolver must [reference](../routing/routers/index.md#certresolver) it."
|
||||||
|
@ -164,9 +164,9 @@ when using the `TLS-ALPN-01` challenge, Traefik must be reachable by Let's Encry
|
||||||
??? example "Configuring the `tlsChallenge`"
|
??? example "Configuring the `tlsChallenge`"
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
```toml tab="File (TOML)"
|
||||||
[certificatesResolvers.sample.acme]
|
[certificatesResolvers.le.acme]
|
||||||
# ...
|
# ...
|
||||||
[certificatesResolvers.sample.acme.tlsChallenge]
|
[certificatesResolvers.le.acme.tlsChallenge]
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml tab="File (YAML)"
|
```yaml tab="File (YAML)"
|
||||||
|
@ -179,7 +179,7 @@ when using the `TLS-ALPN-01` challenge, Traefik must be reachable by Let's Encry
|
||||||
|
|
||||||
```bash tab="CLI"
|
```bash tab="CLI"
|
||||||
# ...
|
# ...
|
||||||
--certificatesResolvers.sample.acme.tlsChallenge=true
|
--certificatesResolvers.le.acme.tlsChallenge=true
|
||||||
```
|
```
|
||||||
|
|
||||||
### `httpChallenge`
|
### `httpChallenge`
|
||||||
|
@ -187,7 +187,7 @@ when using the `TLS-ALPN-01` challenge, Traefik must be reachable by Let's Encry
|
||||||
Use the `HTTP-01` challenge to generate and renew ACME certificates by provisioning an HTTP resource under a well-known URI.
|
Use the `HTTP-01` challenge to generate and renew ACME certificates by provisioning an HTTP resource under a well-known URI.
|
||||||
|
|
||||||
As described on the Let's Encrypt [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72),
|
As described on the Let's Encrypt [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72),
|
||||||
when using the `HTTP-01` challenge, `certificatesResolvers.sample.acme.httpChallenge.entryPoint` must be reachable by Let's Encrypt through port 80.
|
when using the `HTTP-01` challenge, `certificatesResolvers.le.acme.httpChallenge.entryPoint` must be reachable by Let's Encrypt through port 80.
|
||||||
|
|
||||||
??? example "Using an EntryPoint Called http for the `httpChallenge`"
|
??? example "Using an EntryPoint Called http for the `httpChallenge`"
|
||||||
|
|
||||||
|
@ -199,9 +199,9 @@ when using the `HTTP-01` challenge, `certificatesResolvers.sample.acme.httpChall
|
||||||
[entryPoints.web-secure]
|
[entryPoints.web-secure]
|
||||||
address = ":443"
|
address = ":443"
|
||||||
|
|
||||||
[certificatesResolvers.sample.acme]
|
[certificatesResolvers.le.acme]
|
||||||
# ...
|
# ...
|
||||||
[certificatesResolvers.sample.acme.httpChallenge]
|
[certificatesResolvers.le.acme.httpChallenge]
|
||||||
entryPoint = "web"
|
entryPoint = "web"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ when using the `HTTP-01` challenge, `certificatesResolvers.sample.acme.httpChall
|
||||||
--entryPoints.web.address=:80
|
--entryPoints.web.address=:80
|
||||||
--entryPoints.websecure.address=:443
|
--entryPoints.websecure.address=:443
|
||||||
# ...
|
# ...
|
||||||
--certificatesResolvers.sample.acme.httpChallenge.entryPoint=web
|
--certificatesResolvers.le.acme.httpChallenge.entryPoint=web
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! info ""
|
!!! info ""
|
||||||
|
@ -238,9 +238,9 @@ Use the `DNS-01` challenge to generate and renew ACME certificates by provisioni
|
||||||
??? example "Configuring a `dnsChallenge` with the DigitalOcean Provider"
|
??? example "Configuring a `dnsChallenge` with the DigitalOcean Provider"
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
```toml tab="File (TOML)"
|
||||||
[certificatesResolvers.sample.acme]
|
[certificatesResolvers.le.acme]
|
||||||
# ...
|
# ...
|
||||||
[certificatesResolvers.sample.acme.dnsChallenge]
|
[certificatesResolvers.le.acme.dnsChallenge]
|
||||||
provider = "digitalocean"
|
provider = "digitalocean"
|
||||||
delayBeforeCheck = 0
|
delayBeforeCheck = 0
|
||||||
# ...
|
# ...
|
||||||
|
@ -259,8 +259,8 @@ Use the `DNS-01` challenge to generate and renew ACME certificates by provisioni
|
||||||
|
|
||||||
```bash tab="CLI"
|
```bash tab="CLI"
|
||||||
# ...
|
# ...
|
||||||
--certificatesResolvers.sample.acme.dnsChallenge.provider=digitalocean
|
--certificatesResolvers.le.acme.dnsChallenge.provider=digitalocean
|
||||||
--certificatesResolvers.sample.acme.dnsChallenge.delayBeforeCheck=0
|
--certificatesResolvers.le.acme.dnsChallenge.delayBeforeCheck=0
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -357,9 +357,9 @@ For example, `CF_API_EMAIL_FILE=/run/secrets/traefik_cf-api-email` could be used
|
||||||
Use custom DNS servers to resolve the FQDN authority.
|
Use custom DNS servers to resolve the FQDN authority.
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
```toml tab="File (TOML)"
|
||||||
[certificatesResolvers.sample.acme]
|
[certificatesResolvers.le.acme]
|
||||||
# ...
|
# ...
|
||||||
[certificatesResolvers.sample.acme.dnsChallenge]
|
[certificatesResolvers.le.acme.dnsChallenge]
|
||||||
# ...
|
# ...
|
||||||
resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
|
resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
|
||||||
```
|
```
|
||||||
|
@ -378,7 +378,7 @@ certificatesResolvers:
|
||||||
|
|
||||||
```bash tab="CLI"
|
```bash tab="CLI"
|
||||||
# ...
|
# ...
|
||||||
--certificatesResolvers.sample.acme.dnsChallenge.resolvers:=1.1.1.1:53,8.8.8.8:53
|
--certificatesResolvers.le.acme.dnsChallenge.resolvers:=1.1.1.1:53,8.8.8.8:53
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Wildcard Domains
|
#### Wildcard Domains
|
||||||
|
@ -393,7 +393,7 @@ As described in [Let's Encrypt's post](https://community.letsencrypt.org/t/stagi
|
||||||
??? example "Using the Let's Encrypt staging server"
|
??? example "Using the Let's Encrypt staging server"
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
```toml tab="File (TOML)"
|
||||||
[certificatesResolvers.sample.acme]
|
[certificatesResolvers.le.acme]
|
||||||
# ...
|
# ...
|
||||||
caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||||
# ...
|
# ...
|
||||||
|
@ -410,7 +410,7 @@ As described in [Let's Encrypt's post](https://community.letsencrypt.org/t/stagi
|
||||||
|
|
||||||
```bash tab="CLI"
|
```bash tab="CLI"
|
||||||
# ...
|
# ...
|
||||||
--certificatesResolvers.sample.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory
|
--certificatesResolvers.le.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -419,7 +419,7 @@ As described in [Let's Encrypt's post](https://community.letsencrypt.org/t/stagi
|
||||||
The `storage` option sets the location where your ACME certificates are saved to.
|
The `storage` option sets the location where your ACME certificates are saved to.
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
```toml tab="File (TOML)"
|
||||||
[certificatesResolvers.sample.acme]
|
[certificatesResolvers.le.acme]
|
||||||
# ...
|
# ...
|
||||||
storage = "acme.json"
|
storage = "acme.json"
|
||||||
# ...
|
# ...
|
||||||
|
@ -436,7 +436,7 @@ certificatesResolvers:
|
||||||
|
|
||||||
```bash tab="CLI"
|
```bash tab="CLI"
|
||||||
# ...
|
# ...
|
||||||
--certificatesResolvers.sample.acme.storage=acme.json
|
--certificatesResolvers.le.acme.storage=acme.json
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,9 @@ labels:
|
||||||
deploy:
|
deploy:
|
||||||
labels:
|
labels:
|
||||||
- traefik.http.routers.blog.rule=(Host(`company.com`) && Path(`/blog`)) || Host(`blog.company.org`)
|
- traefik.http.routers.blog.rule=(Host(`company.com`) && Path(`/blog`)) || Host(`blog.company.org`)
|
||||||
- traefik.http.services.blog-svc.loadbalancer.server.port=8080"
|
|
||||||
- traefik.http.routers.blog.tls=true
|
- traefik.http.routers.blog.tls=true
|
||||||
- traefik.http.routers.blog.tls.certresolver=le
|
- traefik.http.routers.blog.tls.certresolver=le
|
||||||
|
- traefik.http.services.blog-svc.loadbalancer.server.port=8080"
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml tab="Kubernetes"
|
```yaml tab="Kubernetes"
|
||||||
|
|
|
@ -12,9 +12,9 @@ labels:
|
||||||
deploy:
|
deploy:
|
||||||
labels:
|
labels:
|
||||||
- traefik.http.routers.blog.rule=Host(`company.com`) && Path(`/blog`)
|
- traefik.http.routers.blog.rule=Host(`company.com`) && Path(`/blog`)
|
||||||
- traefik.http.services.blog-svc.loadbalancer.server.port=8080"
|
|
||||||
- traefik.http.routers.blog.tls=true
|
- traefik.http.routers.blog.tls=true
|
||||||
- traefik.http.routers.blog.tls.certresolver=le
|
- traefik.http.routers.blog.tls.certresolver=le
|
||||||
|
- traefik.http.services.blog-svc.loadbalancer.server.port=8080"
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml tab="Kubernetes"
|
```yaml tab="Kubernetes"
|
||||||
|
|
|
@ -35,13 +35,13 @@
|
||||||
#
|
#
|
||||||
# Optional (but recommended)
|
# Optional (but recommended)
|
||||||
#
|
#
|
||||||
[certificatesResolvers.sample.acme.tlsChallenge]
|
[certificatesResolvers.le.acme.tlsChallenge]
|
||||||
|
|
||||||
# Use a HTTP-01 ACME challenge.
|
# Use a HTTP-01 ACME challenge.
|
||||||
#
|
#
|
||||||
# Optional
|
# Optional
|
||||||
#
|
#
|
||||||
# [certificatesResolvers.sample.acme.httpChallenge]
|
# [certificatesResolvers.le.acme.httpChallenge]
|
||||||
|
|
||||||
# EntryPoint to use for the HTTP-01 challenges.
|
# EntryPoint to use for the HTTP-01 challenges.
|
||||||
#
|
#
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
#
|
#
|
||||||
# Optional
|
# Optional
|
||||||
#
|
#
|
||||||
# [certificatesResolvers.sample.acme.dnsChallenge]
|
# [certificatesResolvers.le.acme.dnsChallenge]
|
||||||
|
|
||||||
# DNS provider used.
|
# DNS provider used.
|
||||||
#
|
#
|
||||||
|
|
|
@ -4,13 +4,13 @@
|
||||||
#
|
#
|
||||||
# Required
|
# Required
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.email=test@traefik.io
|
--certificatesResolvers.le.acme.email=test@traefik.io
|
||||||
|
|
||||||
# File or key used for certificates storage.
|
# File or key used for certificates storage.
|
||||||
#
|
#
|
||||||
# Required
|
# Required
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.storage=acme.json
|
--certificatesResolvers.le.acme.storage=acme.json
|
||||||
|
|
||||||
# CA server to use.
|
# CA server to use.
|
||||||
# Uncomment the line to use Let's Encrypt's staging server,
|
# Uncomment the line to use Let's Encrypt's staging server,
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
# Optional
|
# Optional
|
||||||
# Default: "https://acme-v02.api.letsencrypt.org/directory"
|
# Default: "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory
|
--certificatesResolvers.le.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
|
|
||||||
# KeyType to use.
|
# KeyType to use.
|
||||||
#
|
#
|
||||||
|
@ -28,38 +28,38 @@
|
||||||
#
|
#
|
||||||
# Available values : "EC256", "EC384", "RSA2048", "RSA4096", "RSA8192"
|
# Available values : "EC256", "EC384", "RSA2048", "RSA4096", "RSA8192"
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.keyType=RSA4096
|
--certificatesResolvers.le.acme.keyType=RSA4096
|
||||||
|
|
||||||
# Use a TLS-ALPN-01 ACME challenge.
|
# Use a TLS-ALPN-01 ACME challenge.
|
||||||
#
|
#
|
||||||
# Optional (but recommended)
|
# Optional (but recommended)
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.tlsChallenge=true
|
--certificatesResolvers.le.acme.tlsChallenge=true
|
||||||
|
|
||||||
# Use a HTTP-01 ACME challenge.
|
# Use a HTTP-01 ACME challenge.
|
||||||
#
|
#
|
||||||
# Optional
|
# Optional
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.httpChallenge=true
|
--certificatesResolvers.le.acme.httpChallenge=true
|
||||||
|
|
||||||
# EntryPoint to use for the HTTP-01 challenges.
|
# EntryPoint to use for the HTTP-01 challenges.
|
||||||
#
|
#
|
||||||
# Required
|
# Required
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.httpChallenge.entryPoint=web
|
--certificatesResolvers.le.acme.httpChallenge.entryPoint=web
|
||||||
|
|
||||||
# Use a DNS-01 ACME challenge rather than HTTP-01 challenge.
|
# Use a DNS-01 ACME challenge rather than HTTP-01 challenge.
|
||||||
# Note: mandatory for wildcard certificate generation.
|
# Note: mandatory for wildcard certificate generation.
|
||||||
#
|
#
|
||||||
# Optional
|
# Optional
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.dnsChallenge=true
|
--certificatesResolvers.le.acme.dnsChallenge=true
|
||||||
|
|
||||||
# DNS provider used.
|
# DNS provider used.
|
||||||
#
|
#
|
||||||
# Required
|
# Required
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.dnsChallenge.provider=digitalocean
|
--certificatesResolvers.le.acme.dnsChallenge.provider=digitalocean
|
||||||
|
|
||||||
# By default, the provider will verify the TXT DNS challenge record before letting ACME verify.
|
# By default, the provider will verify the TXT DNS challenge record before letting ACME verify.
|
||||||
# If delayBeforeCheck is greater than zero, this check is delayed for the configured duration in seconds.
|
# If delayBeforeCheck is greater than zero, this check is delayed for the configured duration in seconds.
|
||||||
|
@ -68,14 +68,14 @@
|
||||||
# Optional
|
# Optional
|
||||||
# Default: 0
|
# Default: 0
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.dnsChallenge.delayBeforeCheck=0
|
--certificatesResolvers.le.acme.dnsChallenge.delayBeforeCheck=0
|
||||||
|
|
||||||
# Use following DNS servers to resolve the FQDN authority.
|
# Use following DNS servers to resolve the FQDN authority.
|
||||||
#
|
#
|
||||||
# Optional
|
# Optional
|
||||||
# Default: empty
|
# Default: empty
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.dnsChallenge.resolvers=1.1.1.1:53,8.8.8.8:53
|
--certificatesResolvers.le.acme.dnsChallenge.resolvers=1.1.1.1:53,8.8.8.8:53
|
||||||
|
|
||||||
# Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready.
|
# Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready.
|
||||||
#
|
#
|
||||||
|
@ -85,4 +85,4 @@
|
||||||
# Optional
|
# Optional
|
||||||
# Default: false
|
# Default: false
|
||||||
#
|
#
|
||||||
--certificatesResolvers.sample.acme.dnsChallenge.disablePropagationCheck=true
|
--certificatesResolvers.le.acme.dnsChallenge.disablePropagationCheck=true
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
certificatesResolvers:
|
certificatesResolvers:
|
||||||
sample:
|
le:
|
||||||
# Enable ACME (Let's Encrypt): automatic SSL.
|
# Enable ACME (Let's Encrypt): automatic SSL.
|
||||||
acme:
|
acme:
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ tls:
|
||||||
|
|
||||||
In the above example, we've used the [file provider](../providers/file.md) to handle these definitions.
|
In the above example, we've used the [file provider](../providers/file.md) to handle these definitions.
|
||||||
It is the only available method to configure the certificates (as well as the options and the stores).
|
It is the only available method to configure the certificates (as well as the options and the stores).
|
||||||
However, in [Kubernetes](../providers/kubernetes-crd.md), the certificates can and must be provided by [secrets](../routing/providers/kubernetes-crd.md#tls).
|
However, in [Kubernetes](../providers/kubernetes-crd.md), the certificates can and must be provided by [secrets](https://kubernetes.io/docs/concepts/configuration/secret/).
|
||||||
|
|
||||||
## Certificates Stores
|
## Certificates Stores
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ http:
|
||||||
|
|
||||||
### `sourceRange`
|
### `sourceRange`
|
||||||
|
|
||||||
The `sourceRange` option sets the allowed IPs (or ranges of allowed IPs).
|
The `sourceRange` option sets the allowed IPs (or ranges of allowed IPs by using CIDR notation).
|
||||||
|
|
||||||
### `ipStrategy`
|
### `ipStrategy`
|
||||||
|
|
||||||
|
|
|
@ -104,7 +104,7 @@ Then any router can refer to an instance of the wanted middleware.
|
||||||
|
|
||||||
```yaml tab="K8s IngressRoute"
|
```yaml tab="K8s IngressRoute"
|
||||||
# The definitions below require the definitions for the Middleware and IngressRoute kinds.
|
# The definitions below require the definitions for the Middleware and IngressRoute kinds.
|
||||||
# https://docs.traefik.io/v2.0/providers/kubernetes-crd/#traefik-ingressroute-definition
|
# https://docs.traefik.io/v2.1/reference/dynamic-configuration/kubernetes-crd/#definitions
|
||||||
apiVersion: traefik.containo.us/v1alpha1
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
kind: Middleware
|
kind: Middleware
|
||||||
metadata:
|
metadata:
|
||||||
|
@ -278,7 +278,7 @@ Then, a [router's TLS field](../routing/routers/index.md#tls) can refer to one o
|
||||||
|
|
||||||
```yaml tab="K8s IngressRoute"
|
```yaml tab="K8s IngressRoute"
|
||||||
# The definitions below require the definitions for the TLSOption and IngressRoute kinds.
|
# The definitions below require the definitions for the TLSOption and IngressRoute kinds.
|
||||||
# https://docs.traefik.io/v2.0/providers/kubernetes-crd/#traefik-ingressroute-definition
|
# https://docs.traefik.io/v2.1/reference/dynamic-configuration/kubernetes-crd/#definitions
|
||||||
apiVersion: traefik.containo.us/v1alpha1
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
kind: TLSOption
|
kind: TLSOption
|
||||||
metadata:
|
metadata:
|
||||||
|
|
99
docs/content/migration/v2.md
Normal file
99
docs/content/migration/v2.md
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
# Migration: Steps needed between the versions
|
||||||
|
|
||||||
|
## v2.0 to v2.1
|
||||||
|
|
||||||
|
In v2.1, a new CRD called `TraefikService` was added. While updating an installation to v2.1,
|
||||||
|
it is required to apply that CRD before as well as enhance the existing `ClusterRole` definition to allow Traefik to use that CRD.
|
||||||
|
|
||||||
|
To add that CRD and enhance the permissions, following definitions need to be applied to the cluster.
|
||||||
|
|
||||||
|
```yaml tab="TraefikService"
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: traefikservices.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: TraefikService
|
||||||
|
plural: traefikservices
|
||||||
|
singular: traefikservice
|
||||||
|
scope: Namespaced
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="ClusterRole"
|
||||||
|
kind: ClusterRole
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: traefik-ingress-controller
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
- endpoints
|
||||||
|
- secrets
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- ingresses
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- ingresses/status
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- traefik.containo.us
|
||||||
|
resources:
|
||||||
|
- middlewares
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- traefik.containo.us
|
||||||
|
resources:
|
||||||
|
- ingressroutes
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- traefik.containo.us
|
||||||
|
resources:
|
||||||
|
- ingressroutetcps
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- traefik.containo.us
|
||||||
|
resources:
|
||||||
|
- tlsoptions
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- traefik.containo.us
|
||||||
|
resources:
|
||||||
|
- traefikservices
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
```
|
||||||
|
|
||||||
|
After having both resources applied, Traefik will work properly.
|
|
@ -19,6 +19,28 @@ deploy:
|
||||||
- "traefik.http.services.dummy-svc.loadbalancer.server.port=9999"
|
- "traefik.http.services.dummy-svc.loadbalancer.server.port=9999"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes CRD"
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: traefik-dashboard
|
||||||
|
spec:
|
||||||
|
routes:
|
||||||
|
- match: Host(`traefik.domain.com`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: api@internal
|
||||||
|
kind: TraefikService
|
||||||
|
---
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: auth
|
||||||
|
spec:
|
||||||
|
basicAuth:
|
||||||
|
secret: secretName # Kubernetes secret named "secretName"
|
||||||
|
```
|
||||||
|
|
||||||
```yaml tab="Consul Catalog"
|
```yaml tab="Consul Catalog"
|
||||||
# Dynamic Configuration
|
# Dynamic Configuration
|
||||||
- "traefik.http.routers.api.rule=Host(`traefik.domain.com`)"
|
- "traefik.http.routers.api.rule=Host(`traefik.domain.com`)"
|
||||||
|
|
|
@ -86,7 +86,7 @@ and [Docker Swarm Mode](https://docs.docker.com/engine/swarm/).
|
||||||
## Routing Configuration
|
## Routing Configuration
|
||||||
|
|
||||||
When using Docker as a [provider](https://docs.traefik.io/providers/overview/),
|
When using Docker as a [provider](https://docs.traefik.io/providers/overview/),
|
||||||
Trafik uses [container labels](https://docs.docker.com/engine/reference/commandline/run/#set-metadata-on-container--l---label---label-file) to retrieve its routing configuration.
|
Traefik uses [container labels](https://docs.docker.com/engine/reference/commandline/run/#set-metadata-on-container--l---label---label-file) to retrieve its routing configuration.
|
||||||
|
|
||||||
See the list of labels in the dedicated [routing](../routing/providers/docker.md) section.
|
See the list of labels in the dedicated [routing](../routing/providers/docker.md) section.
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,43 @@ Traefik used to support Kubernetes only through the [Kubernetes Ingress provider
|
||||||
However, as the community expressed the need to benefit from Traefik features without resorting to (lots of) annotations,
|
However, as the community expressed the need to benefit from Traefik features without resorting to (lots of) annotations,
|
||||||
we ended up writing a [Custom Resource Definition](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) (alias CRD in the following) for an IngressRoute type, defined below, in order to provide a better way to configure access to a Kubernetes cluster.
|
we ended up writing a [Custom Resource Definition](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) (alias CRD in the following) for an IngressRoute type, defined below, in order to provide a better way to configure access to a Kubernetes cluster.
|
||||||
|
|
||||||
|
## Configuration Requirements
|
||||||
|
|
||||||
|
!!! tip "All Steps for a Successful Deployment"
|
||||||
|
|
||||||
|
* Add/update **all** the Traefik resources [definitions](../reference/dynamic-configuration/kubernetes-crd.md#definitions)
|
||||||
|
* Add/update the [RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) for the Traefik custom resources
|
||||||
|
* Use [Helm Chart](../getting-started/install-traefik.md#use-the-helm-chart) or use a custom Traefik Deployment
|
||||||
|
* Enable the kubernetesCRD provider
|
||||||
|
* Apply the needed kubernetesCRD provider [configuration](#provider-configuration)
|
||||||
|
* Add all needed traefik custom [resources](../reference/dynamic-configuration/kubernetes-crd.md#resources)
|
||||||
|
|
||||||
|
??? example "Initializing Resource Definition and RBAC"
|
||||||
|
|
||||||
|
```yaml tab="Traefik Resource Definition"
|
||||||
|
# All resources definition must be declared
|
||||||
|
--8<-- "content/reference/dynamic-configuration/kubernetes-crd-definition.yml"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="RBAC for Traefik CRD"
|
||||||
|
--8<-- "content/reference/dynamic-configuration/kubernetes-crd-rbac.yml"
|
||||||
|
```
|
||||||
|
|
||||||
## Resource Configuration
|
## Resource Configuration
|
||||||
|
|
||||||
See the dedicated section in [routing](../routing/providers/kubernetes-crd.md).
|
When using KubernetesCRD as a provider,
|
||||||
|
Traefik uses [Custom Resource Definition](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) to retrieve its routing configuration.
|
||||||
|
Traefik Custom Resource Definitions are a Kubernetes implementation of the Traefik concepts. The main particularities are:
|
||||||
|
|
||||||
|
* The usage of `name` **and** `namespace` to refer to another Kubernetes resource.
|
||||||
|
* The usage of [secret](https://kubernetes.io/docs/concepts/configuration/secret/) for sensible data like:
|
||||||
|
* TLS certificate.
|
||||||
|
* Authentication data.
|
||||||
|
* The structure of the configuration.
|
||||||
|
* The obligation to declare all the [definitions](../reference/dynamic-configuration/kubernetes-crd.md#definitions).
|
||||||
|
|
||||||
|
The Traefik CRD are building blocks which you can assemble according to your needs.
|
||||||
|
See the list of CRDs in the dedicated [routing section](../routing/providers/kubernetes-crd.md).
|
||||||
|
|
||||||
## LetsEncrypt Support with the Custom Resource Definition Provider
|
## LetsEncrypt Support with the Custom Resource Definition Provider
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: ingressroutes.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: IngressRoute
|
||||||
|
plural: ingressroutes
|
||||||
|
singular: ingressroute
|
||||||
|
scope: Namespaced
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: middlewares.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: Middleware
|
||||||
|
plural: middlewares
|
||||||
|
singular: middleware
|
||||||
|
scope: Namespaced
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: ingressroutetcps.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: IngressRouteTCP
|
||||||
|
plural: ingressroutetcps
|
||||||
|
singular: ingressroutetcp
|
||||||
|
scope: Namespaced
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: tlsoptions.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: TLSOption
|
||||||
|
plural: tlsoptions
|
||||||
|
singular: tlsoption
|
||||||
|
scope: Namespaced
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: traefikservices.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: TraefikService
|
||||||
|
plural: traefikservices
|
||||||
|
singular: traefikservice
|
||||||
|
scope: Namespaced
|
|
@ -0,0 +1,13 @@
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: ingressroutetcps.traefik.containo.us
|
||||||
|
|
||||||
|
spec:
|
||||||
|
group: traefik.containo.us
|
||||||
|
version: v1alpha1
|
||||||
|
names:
|
||||||
|
kind: IngressRouteTCP
|
||||||
|
plural: ingressroutetcps
|
||||||
|
singular: ingressroutetcp
|
||||||
|
scope: Namespaced
|
|
@ -0,0 +1,57 @@
|
||||||
|
kind: ClusterRole
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: traefik-ingress-controller
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
- endpoints
|
||||||
|
- secrets
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- ingresses
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- ingresses/status
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- traefik.containo.us
|
||||||
|
resources:
|
||||||
|
- middlewares
|
||||||
|
- ingressroutes
|
||||||
|
- traefikservices
|
||||||
|
- ingressroutetcps
|
||||||
|
- tlsoptions
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: traefik-ingress-controller
|
||||||
|
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: traefik-ingress-controller
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: traefik-ingress-controller
|
||||||
|
namespace: default
|
|
@ -0,0 +1,157 @@
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: TraefikService
|
||||||
|
metadata:
|
||||||
|
name: wrr2
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
spec:
|
||||||
|
weighted:
|
||||||
|
services:
|
||||||
|
- name: s1
|
||||||
|
weight: 1
|
||||||
|
port: 80
|
||||||
|
# Optional, as it is the default value
|
||||||
|
kind: Service
|
||||||
|
- name: s3
|
||||||
|
weight: 1
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: TraefikService
|
||||||
|
metadata:
|
||||||
|
name: wrr1
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
spec:
|
||||||
|
weighted:
|
||||||
|
services:
|
||||||
|
- name: wrr2
|
||||||
|
kind: TraefikService
|
||||||
|
weight: 1
|
||||||
|
- name: s3
|
||||||
|
weight: 1
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: TraefikService
|
||||||
|
metadata:
|
||||||
|
name: mirror1
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
spec:
|
||||||
|
mirroring:
|
||||||
|
name: s1
|
||||||
|
port: 80
|
||||||
|
mirrors:
|
||||||
|
- name: s3
|
||||||
|
percent: 20
|
||||||
|
port: 80
|
||||||
|
- name: mirror2
|
||||||
|
kind: TraefikService
|
||||||
|
percent: 20
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: TraefikService
|
||||||
|
metadata:
|
||||||
|
name: mirror2
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
spec:
|
||||||
|
mirroring:
|
||||||
|
name: wrr2
|
||||||
|
kind: TraefikService
|
||||||
|
mirrors:
|
||||||
|
- name: s2
|
||||||
|
# Optional, as it is the default value
|
||||||
|
kind: Service
|
||||||
|
percent: 20
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: ingressroute
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
- web-secure
|
||||||
|
routes:
|
||||||
|
- match: Host(`foo.com`) && PathPrefix(`/bar`)
|
||||||
|
kind: Rule
|
||||||
|
priority: 12
|
||||||
|
# defining several services is possible and allowed, but for now the servers of
|
||||||
|
# all the services (for a given route) get merged altogether under the same
|
||||||
|
# load-balancing strategy.
|
||||||
|
services:
|
||||||
|
- name: s1
|
||||||
|
port: 80
|
||||||
|
healthCheck:
|
||||||
|
path: /health
|
||||||
|
host: baz.com
|
||||||
|
intervalSeconds: 7
|
||||||
|
timeoutSeconds: 60
|
||||||
|
# strategy defines the load balancing strategy between the servers. It defaults
|
||||||
|
# to Round Robin, and for now only Round Robin is supported anyway.
|
||||||
|
strategy: RoundRobin
|
||||||
|
- name: s2
|
||||||
|
port: 433
|
||||||
|
healthCheck:
|
||||||
|
path: /health
|
||||||
|
host: baz.com
|
||||||
|
intervalSeconds: 7
|
||||||
|
timeoutSeconds: 60
|
||||||
|
- match: PathPrefix(`/misc`)
|
||||||
|
services:
|
||||||
|
- name: s3
|
||||||
|
port: 80
|
||||||
|
middlewares:
|
||||||
|
- name: stripprefix
|
||||||
|
- name: addprefix
|
||||||
|
- match: PathPrefix(`/misc`)
|
||||||
|
services:
|
||||||
|
- name: s3
|
||||||
|
# Optional, as it is the default value
|
||||||
|
kind: Service
|
||||||
|
port: 8443
|
||||||
|
# scheme allow to override the scheme for the service. (ex: https or h2c)
|
||||||
|
scheme: https
|
||||||
|
- match: PathPrefix(`/lb`)
|
||||||
|
services:
|
||||||
|
- name: wrr1
|
||||||
|
kind: TraefikService
|
||||||
|
- match: PathPrefix(`/mirrored`)
|
||||||
|
services:
|
||||||
|
- name: mirror1
|
||||||
|
kind: TraefikService
|
||||||
|
# use an empty tls object for TLS with Let's Encrypt
|
||||||
|
tls:
|
||||||
|
secretName: supersecret
|
||||||
|
options:
|
||||||
|
name: myTLSOption
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRouteTCP
|
||||||
|
metadata:
|
||||||
|
name: ingressroutetcp.crd
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- footcp
|
||||||
|
routes:
|
||||||
|
- match: HostSNI(`bar.com`)
|
||||||
|
services:
|
||||||
|
- name: whoamitcp
|
||||||
|
port: 8080
|
||||||
|
tls:
|
||||||
|
secretName: foosecret
|
||||||
|
passthrough: false
|
||||||
|
options:
|
||||||
|
name: myTLSOption
|
||||||
|
namespace: default
|
|
@ -3,6 +3,20 @@
|
||||||
Dynamic configuration with Kubernetes Custom Resource
|
Dynamic configuration with Kubernetes Custom Resource
|
||||||
{: .subtitle }
|
{: .subtitle }
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
--8<-- "content/reference/dynamic-configuration/kubernetes-crd.yml"
|
--8<-- "content/reference/dynamic-configuration/kubernetes-crd-definition.yml"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
--8<-- "content/reference/dynamic-configuration/kubernetes-crd-resource.yml"
|
||||||
|
```
|
||||||
|
|
||||||
|
## RBAC
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
--8<-- "content/reference/dynamic-configuration/kubernetes-crd-rbac.yml"
|
||||||
```
|
```
|
||||||
|
|
|
@ -41,7 +41,7 @@ They define the port which will receive the requests (whether HTTP or TCP).
|
||||||
[entryPoints.web]
|
[entryPoints.web]
|
||||||
address = ":80"
|
address = ":80"
|
||||||
|
|
||||||
[entryPoints.web-secure]
|
[entryPoints.websecure]
|
||||||
address = ":443"
|
address = ":443"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -51,18 +51,18 @@ They define the port which will receive the requests (whether HTTP or TCP).
|
||||||
web:
|
web:
|
||||||
address: ":80"
|
address: ":80"
|
||||||
|
|
||||||
web-secure:
|
websecure:
|
||||||
address: ":443"
|
address: ":443"
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash tab="CLI"
|
```bash tab="CLI"
|
||||||
## Static configuration
|
## Static configuration
|
||||||
--entryPoints.web.address=:80
|
--entryPoints.web.address=:80
|
||||||
--entryPoints.web-secure.address=:443
|
--entryPoints.websecure.address=:443
|
||||||
```
|
```
|
||||||
|
|
||||||
- Two entrypoints are defined: one called `web`, and the other called `web-secure`.
|
- Two entrypoints are defined: one called `web`, and the other called `websecure`.
|
||||||
- `web` listens on port `80`, and `web-secure` on port `443`.
|
- `web` listens on port `80`, and `websecure` on port `443`.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -160,6 +160,7 @@ nav:
|
||||||
- 'HTTP Challenge': 'user-guides/docker-compose/acme-http/index.md'
|
- 'HTTP Challenge': 'user-guides/docker-compose/acme-http/index.md'
|
||||||
- 'DNS Challenge': 'user-guides/docker-compose/acme-dns/index.md'
|
- 'DNS Challenge': 'user-guides/docker-compose/acme-dns/index.md'
|
||||||
- 'Migration':
|
- 'Migration':
|
||||||
|
- 'Traefik v2 minor migrations': 'migration/v2.md'
|
||||||
- 'Traefik v1 to v2': 'migration/v1-to-v2.md'
|
- 'Traefik v1 to v2': 'migration/v1-to-v2.md'
|
||||||
- 'Contributing':
|
- 'Contributing':
|
||||||
- 'Thank You!': 'contributing/thank-you.md'
|
- 'Thank You!': 'contributing/thank-you.md'
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -81,7 +81,7 @@ require (
|
||||||
github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154
|
github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154
|
||||||
github.com/tinylib/msgp v1.0.2 // indirect
|
github.com/tinylib/msgp v1.0.2 // indirect
|
||||||
github.com/transip/gotransip v5.8.2+incompatible // indirect
|
github.com/transip/gotransip v5.8.2+incompatible // indirect
|
||||||
github.com/uber/jaeger-client-go v2.20.1+incompatible
|
github.com/uber/jaeger-client-go v2.21.1+incompatible
|
||||||
github.com/uber/jaeger-lib v2.2.0+incompatible
|
github.com/uber/jaeger-lib v2.2.0+incompatible
|
||||||
github.com/unrolled/render v1.0.1
|
github.com/unrolled/render v1.0.1
|
||||||
github.com/unrolled/secure v1.0.6
|
github.com/unrolled/secure v1.0.6
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -599,8 +599,8 @@ github.com/transip/gotransip v5.8.2+incompatible/go.mod h1:uacMoJVmrfOcscM4Bi5NV
|
||||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||||
github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo=
|
github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo=
|
||||||
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
||||||
github.com/uber/jaeger-client-go v2.20.1+incompatible h1:HgqpYBng0n7tLJIlyT4kPCIv5XgCsF+kai1NnnrJzEU=
|
github.com/uber/jaeger-client-go v2.21.1+incompatible h1:oozboeZmWz+tyh3VZttJWlF3K73mHgbokieceqKccLo=
|
||||||
github.com/uber/jaeger-client-go v2.20.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
|
github.com/uber/jaeger-client-go v2.21.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
|
||||||
github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/GfSYVCjK7dyaw=
|
github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/GfSYVCjK7dyaw=
|
||||||
github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||||
github.com/unrolled/render v1.0.1 h1:VDDnQQVfBMsOsp3VaCJszSO0nkBIVEYoPWeRThk9spY=
|
github.com/unrolled/render v1.0.1 h1:VDDnQQVfBMsOsp3VaCJszSO0nkBIVEYoPWeRThk9spY=
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/integration/try"
|
"github.com/containous/traefik/v2/integration/try"
|
||||||
|
@ -62,23 +61,13 @@ func (s *ConsulCatalogSuite) TearDownSuite(c *check.C) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConsulCatalogSuite) registerService(id, name, address, port string, tags []string, onAgent bool) error {
|
func (s *ConsulCatalogSuite) registerService(reg *api.AgentServiceRegistration, onAgent bool) error {
|
||||||
iPort, err := strconv.Atoi(port)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
client := s.consulClient
|
client := s.consulClient
|
||||||
if onAgent {
|
if onAgent {
|
||||||
client = s.consulAgentClient
|
client = s.consulAgentClient
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.Agent().ServiceRegister(&api.AgentServiceRegistration{
|
return client.Agent().ServiceRegister(reg)
|
||||||
ID: id,
|
|
||||||
Name: name,
|
|
||||||
Address: address,
|
|
||||||
Port: iPort,
|
|
||||||
Tags: tags,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConsulCatalogSuite) deregisterService(id string, onAgent bool) error {
|
func (s *ConsulCatalogSuite) deregisterService(id string, onAgent bool) error {
|
||||||
|
@ -90,11 +79,34 @@ func (s *ConsulCatalogSuite) deregisterService(id string, onAgent bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConsulCatalogSuite) TestWithNotExposedByDefaultAndDefaultsSettings(c *check.C) {
|
func (s *ConsulCatalogSuite) TestWithNotExposedByDefaultAndDefaultsSettings(c *check.C) {
|
||||||
err := s.registerService("whoami1", "whoami", s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "80", []string{"traefik.enable=true"}, false)
|
reg1 := &api.AgentServiceRegistration{
|
||||||
|
ID: "whoami1",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: []string{"traefik.enable=true"},
|
||||||
|
Port: 80,
|
||||||
|
Address: s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress,
|
||||||
|
}
|
||||||
|
err := s.registerService(reg1, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
err = s.registerService("whoami2", "whoami", s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress, "80", []string{"traefik.enable=true"}, false)
|
|
||||||
|
reg2 := &api.AgentServiceRegistration{
|
||||||
|
ID: "whoami2",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: []string{"traefik.enable=true"},
|
||||||
|
Port: 80,
|
||||||
|
Address: s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress,
|
||||||
|
}
|
||||||
|
err = s.registerService(reg2, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
err = s.registerService("whoami3", "whoami", s.composeProject.Container(c, "whoami3").NetworkSettings.IPAddress, "80", []string{"traefik.enable=true"}, false)
|
|
||||||
|
reg3 := &api.AgentServiceRegistration{
|
||||||
|
ID: "whoami3",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: []string{"traefik.enable=true"},
|
||||||
|
Port: 80,
|
||||||
|
Address: s.composeProject.Container(c, "whoami3").NetworkSettings.IPAddress,
|
||||||
|
}
|
||||||
|
err = s.registerService(reg3, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
tempObjects := struct {
|
tempObjects := struct {
|
||||||
|
@ -128,14 +140,21 @@ func (s *ConsulCatalogSuite) TestWithNotExposedByDefaultAndDefaultsSettings(c *c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConsulCatalogSuite) TestByLabels(c *check.C) {
|
func (s *ConsulCatalogSuite) TestByLabels(c *check.C) {
|
||||||
labels := []string{
|
containerIP := s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress
|
||||||
"traefik.enable=true",
|
|
||||||
"traefik.http.routers.router1.rule=Path(`/whoami`)",
|
|
||||||
"traefik.http.routers.router1.service=service1",
|
|
||||||
"traefik.http.services.service1.loadBalancer.server.url=http://" + s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.registerService("whoami1", "whoami", s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "80", labels, false)
|
reg := &api.AgentServiceRegistration{
|
||||||
|
ID: "whoami1",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: []string{
|
||||||
|
"traefik.enable=true",
|
||||||
|
"traefik.http.routers.router1.rule=Path(`/whoami`)",
|
||||||
|
"traefik.http.routers.router1.service=service1",
|
||||||
|
"traefik.http.services.service1.loadBalancer.server.url=http://" + containerIP,
|
||||||
|
},
|
||||||
|
Port: 80,
|
||||||
|
Address: containerIP,
|
||||||
|
}
|
||||||
|
err := s.registerService(reg, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
tempObjects := struct {
|
tempObjects := struct {
|
||||||
|
@ -172,7 +191,14 @@ func (s *ConsulCatalogSuite) TestSimpleConfiguration(c *check.C) {
|
||||||
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
||||||
defer os.Remove(file)
|
defer os.Remove(file)
|
||||||
|
|
||||||
err := s.registerService("whoami1", "whoami", s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "80", []string{"traefik.enable=true"}, false)
|
reg := &api.AgentServiceRegistration{
|
||||||
|
ID: "whoami1",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: []string{"traefik.enable=true"},
|
||||||
|
Port: 80,
|
||||||
|
Address: s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress,
|
||||||
|
}
|
||||||
|
err := s.registerService(reg, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
cmd, display := s.traefikCmd(withConfigFile(file))
|
cmd, display := s.traefikCmd(withConfigFile(file))
|
||||||
|
@ -204,7 +230,14 @@ func (s *ConsulCatalogSuite) TestRegisterServiceWithoutIP(c *check.C) {
|
||||||
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
||||||
defer os.Remove(file)
|
defer os.Remove(file)
|
||||||
|
|
||||||
err := s.registerService("whoami1", "whoami", "", "80", []string{"traefik.enable=true"}, false)
|
reg := &api.AgentServiceRegistration{
|
||||||
|
ID: "whoami1",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: []string{"traefik.enable=true"},
|
||||||
|
Port: 80,
|
||||||
|
Address: "",
|
||||||
|
}
|
||||||
|
err := s.registerService(reg, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
cmd, display := s.traefikCmd(withConfigFile(file))
|
cmd, display := s.traefikCmd(withConfigFile(file))
|
||||||
|
@ -236,7 +269,13 @@ func (s *ConsulCatalogSuite) TestDefaultConsulService(c *check.C) {
|
||||||
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
||||||
defer os.Remove(file)
|
defer os.Remove(file)
|
||||||
|
|
||||||
err := s.registerService("whoami1", "whoami", s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "80", nil, false)
|
reg := &api.AgentServiceRegistration{
|
||||||
|
ID: "whoami1",
|
||||||
|
Name: "whoami",
|
||||||
|
Port: 80,
|
||||||
|
Address: s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress,
|
||||||
|
}
|
||||||
|
err := s.registerService(reg, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
// Start traefik
|
// Start traefik
|
||||||
|
@ -269,14 +308,20 @@ func (s *ConsulCatalogSuite) TestConsulServiceWithTCPLabels(c *check.C) {
|
||||||
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
||||||
defer os.Remove(file)
|
defer os.Remove(file)
|
||||||
|
|
||||||
// Start a container with some labels
|
// Start a container with some tags
|
||||||
labels := []string{
|
reg := &api.AgentServiceRegistration{
|
||||||
"traefik.tcp.Routers.Super.Rule=HostSNI(`my.super.host`)",
|
ID: "whoamitcp",
|
||||||
"traefik.tcp.Routers.Super.tls=true",
|
Name: "whoamitcp",
|
||||||
"traefik.tcp.Services.Super.Loadbalancer.server.port=8080",
|
Tags: []string{
|
||||||
|
"traefik.tcp.Routers.Super.Rule=HostSNI(`my.super.host`)",
|
||||||
|
"traefik.tcp.Routers.Super.tls=true",
|
||||||
|
"traefik.tcp.Services.Super.Loadbalancer.server.port=8080",
|
||||||
|
},
|
||||||
|
Port: 8080,
|
||||||
|
Address: s.composeProject.Container(c, "whoamitcp").NetworkSettings.IPAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.registerService("whoamitcp", "whoamitcp", s.composeProject.Container(c, "whoamitcp").NetworkSettings.IPAddress, "8080", labels, false)
|
err := s.registerService(reg, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
// Start traefik
|
// Start traefik
|
||||||
|
@ -310,18 +355,31 @@ func (s *ConsulCatalogSuite) TestConsulServiceWithLabels(c *check.C) {
|
||||||
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
||||||
defer os.Remove(file)
|
defer os.Remove(file)
|
||||||
|
|
||||||
// Start a container with some labels
|
// Start a container with some tags
|
||||||
labels := []string{
|
reg1 := &api.AgentServiceRegistration{
|
||||||
"traefik.http.Routers.Super.Rule=Host(`my.super.host`)",
|
ID: "whoami1",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: []string{
|
||||||
|
"traefik.http.Routers.Super.Rule=Host(`my.super.host`)",
|
||||||
|
},
|
||||||
|
Port: 80,
|
||||||
|
Address: s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress,
|
||||||
}
|
}
|
||||||
err := s.registerService("whoami1", "whoami", s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "80", labels, false)
|
|
||||||
|
err := s.registerService(reg1, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
// Start another container by replacing a '.' by a '-'
|
// Start another container by replacing a '.' by a '-'
|
||||||
labels = []string{
|
reg2 := &api.AgentServiceRegistration{
|
||||||
"traefik.http.Routers.SuperHost.Rule=Host(`my-super.host`)",
|
ID: "whoami2",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: []string{
|
||||||
|
"traefik.http.Routers.SuperHost.Rule=Host(`my-super.host`)",
|
||||||
|
},
|
||||||
|
Port: 80,
|
||||||
|
Address: s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress,
|
||||||
}
|
}
|
||||||
err = s.registerService("whoami2", "whoami", s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress, "80", labels, false)
|
err = s.registerService(reg2, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
// Start traefik
|
// Start traefik
|
||||||
|
@ -364,16 +422,31 @@ func (s *ConsulCatalogSuite) TestSameServiceIDOnDifferentConsulAgent(c *check.C)
|
||||||
file := s.adaptFile(c, "fixtures/consul_catalog/default_not_exposed.toml", tempObjects)
|
file := s.adaptFile(c, "fixtures/consul_catalog/default_not_exposed.toml", tempObjects)
|
||||||
defer os.Remove(file)
|
defer os.Remove(file)
|
||||||
|
|
||||||
// Start a container with some labels
|
// Start a container with some tags
|
||||||
labels := []string{
|
tags := []string{
|
||||||
"traefik.enable=true",
|
"traefik.enable=true",
|
||||||
"traefik.http.Routers.Super.service=whoami",
|
"traefik.http.Routers.Super.service=whoami",
|
||||||
"traefik.http.Routers.Super.Rule=Host(`my.super.host`)",
|
"traefik.http.Routers.Super.Rule=Host(`my.super.host`)",
|
||||||
}
|
}
|
||||||
err := s.registerService("whoami", "whoami", s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "80", labels, false)
|
|
||||||
|
reg1 := &api.AgentServiceRegistration{
|
||||||
|
ID: "whoami",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: tags,
|
||||||
|
Port: 80,
|
||||||
|
Address: s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress,
|
||||||
|
}
|
||||||
|
err := s.registerService(reg1, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
err = s.registerService("whoami", "whoami", s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress, "80", labels, true)
|
reg2 := &api.AgentServiceRegistration{
|
||||||
|
ID: "whoami",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: tags,
|
||||||
|
Port: 80,
|
||||||
|
Address: s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress,
|
||||||
|
}
|
||||||
|
err = s.registerService(reg2, true)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
// Start traefik
|
// Start traefik
|
||||||
|
@ -417,11 +490,18 @@ func (s *ConsulCatalogSuite) TestConsulServiceWithOneMissingLabels(c *check.C) {
|
||||||
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
||||||
defer os.Remove(file)
|
defer os.Remove(file)
|
||||||
|
|
||||||
// Start a container with some labels
|
// Start a container with some tags
|
||||||
labels := []string{
|
reg := &api.AgentServiceRegistration{
|
||||||
"traefik.random.value=my.super.host",
|
ID: "whoami1",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: []string{
|
||||||
|
"traefik.random.value=my.super.host",
|
||||||
|
},
|
||||||
|
Port: 80,
|
||||||
|
Address: s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress,
|
||||||
}
|
}
|
||||||
err := s.registerService("whoami1", "whoami", s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "80", labels, false)
|
|
||||||
|
err := s.registerService(reg, false)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
// Start traefik
|
// Start traefik
|
||||||
|
@ -441,3 +521,82 @@ func (s *ConsulCatalogSuite) TestConsulServiceWithOneMissingLabels(c *check.C) {
|
||||||
err = try.Request(req, 1500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound))
|
err = try.Request(req, 1500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ConsulCatalogSuite) TestConsulServiceWithHealthCheck(c *check.C) {
|
||||||
|
tags := []string{
|
||||||
|
"traefik.enable=true",
|
||||||
|
"traefik.http.routers.router1.rule=Path(`/whoami`)",
|
||||||
|
"traefik.http.routers.router1.service=service1",
|
||||||
|
"traefik.http.services.service1.loadBalancer.server.url=http://" + s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
reg1 := &api.AgentServiceRegistration{
|
||||||
|
ID: "whoami1",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: tags,
|
||||||
|
Port: 80,
|
||||||
|
Address: s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress,
|
||||||
|
Check: &api.AgentServiceCheck{
|
||||||
|
CheckID: "some-failed-check",
|
||||||
|
TCP: "127.0.0.1:1234",
|
||||||
|
Name: "some-failed-check",
|
||||||
|
Interval: "1s",
|
||||||
|
Timeout: "1s",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.registerService(reg1, false)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
tempObjects := struct {
|
||||||
|
ConsulAddress string
|
||||||
|
}{
|
||||||
|
ConsulAddress: s.consulAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects)
|
||||||
|
defer os.Remove(file)
|
||||||
|
|
||||||
|
cmd, display := s.traefikCmd(withConfigFile(file))
|
||||||
|
defer display(c)
|
||||||
|
err = cmd.Start()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
|
err = try.GetRequest("http://127.0.0.1:8000/whoami", 2*time.Second, try.StatusCodeIs(http.StatusNotFound))
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
err = s.deregisterService("whoami1", false)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
containerIP := s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress
|
||||||
|
|
||||||
|
reg2 := &api.AgentServiceRegistration{
|
||||||
|
ID: "whoami2",
|
||||||
|
Name: "whoami",
|
||||||
|
Tags: tags,
|
||||||
|
Port: 80,
|
||||||
|
Address: containerIP,
|
||||||
|
Check: &api.AgentServiceCheck{
|
||||||
|
CheckID: "some-ok-check",
|
||||||
|
TCP: containerIP + ":80",
|
||||||
|
Name: "some-ok-check",
|
||||||
|
Interval: "1s",
|
||||||
|
Timeout: "1s",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.registerService(reg2, false)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
req.Host = "whoami"
|
||||||
|
|
||||||
|
// FIXME Need to wait for up to 10 seconds (for consul discovery or traefik to boot up ?)
|
||||||
|
err = try.Request(req, 10*time.Second, try.StatusCodeIs(200), try.BodyContainsOr("Hostname: whoami2"))
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
err = s.deregisterService("whoami2", false)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
}
|
||||||
|
|
2
integration/testdata/rawdata-consul.json
vendored
2
integration/testdata/rawdata-consul.json
vendored
|
@ -86,7 +86,7 @@
|
||||||
},
|
},
|
||||||
"dashboard_redirect@internal": {
|
"dashboard_redirect@internal": {
|
||||||
"redirectRegex": {
|
"redirectRegex": {
|
||||||
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
|
"regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$",
|
||||||
"replacement": "${1}/dashboard/",
|
"replacement": "${1}/dashboard/",
|
||||||
"permanent": true
|
"permanent": true
|
||||||
},
|
},
|
||||||
|
|
2
integration/testdata/rawdata-etcd.json
vendored
2
integration/testdata/rawdata-etcd.json
vendored
|
@ -86,7 +86,7 @@
|
||||||
},
|
},
|
||||||
"dashboard_redirect@internal": {
|
"dashboard_redirect@internal": {
|
||||||
"redirectRegex": {
|
"redirectRegex": {
|
||||||
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
|
"regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$",
|
||||||
"replacement": "${1}/dashboard/",
|
"replacement": "${1}/dashboard/",
|
||||||
"permanent": true
|
"permanent": true
|
||||||
},
|
},
|
||||||
|
|
2
integration/testdata/rawdata-ingress.json
vendored
2
integration/testdata/rawdata-ingress.json
vendored
|
@ -60,7 +60,7 @@
|
||||||
"middlewares": {
|
"middlewares": {
|
||||||
"dashboard_redirect@internal": {
|
"dashboard_redirect@internal": {
|
||||||
"redirectRegex": {
|
"redirectRegex": {
|
||||||
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
|
"regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$",
|
||||||
"replacement": "${1}/dashboard/",
|
"replacement": "${1}/dashboard/",
|
||||||
"permanent": true
|
"permanent": true
|
||||||
},
|
},
|
||||||
|
|
2
integration/testdata/rawdata-redis.json
vendored
2
integration/testdata/rawdata-redis.json
vendored
|
@ -86,7 +86,7 @@
|
||||||
},
|
},
|
||||||
"dashboard_redirect@internal": {
|
"dashboard_redirect@internal": {
|
||||||
"redirectRegex": {
|
"redirectRegex": {
|
||||||
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
|
"regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$",
|
||||||
"replacement": "${1}/dashboard/",
|
"replacement": "${1}/dashboard/",
|
||||||
"permanent": true
|
"permanent": true
|
||||||
},
|
},
|
||||||
|
|
2
integration/testdata/rawdata-zk.json
vendored
2
integration/testdata/rawdata-zk.json
vendored
|
@ -86,7 +86,7 @@
|
||||||
},
|
},
|
||||||
"dashboard_redirect@internal": {
|
"dashboard_redirect@internal": {
|
||||||
"redirectRegex": {
|
"redirectRegex": {
|
||||||
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
|
"regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$",
|
||||||
"replacement": "${1}/dashboard/",
|
"replacement": "${1}/dashboard/",
|
||||||
"permanent": true
|
"permanent": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,8 +13,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// decodeFileToNode decodes the configuration in filePath in a tree of untyped nodes.
|
// decodeFileToNode decodes the configuration in filePath in a tree of untyped nodes.
|
||||||
// If filters is not empty, it skips any configuration element whose name is
|
// If filters is not empty, it skips any configuration element whose name is not among filters.
|
||||||
// not among filters.
|
|
||||||
func decodeFileToNode(filePath string, filters ...string) (*parser.Node, error) {
|
func decodeFileToNode(filePath string, filters ...string) (*parser.Node, error) {
|
||||||
content, err := ioutil.ReadFile(filePath)
|
content, err := ioutil.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -40,7 +39,20 @@ func decodeFileToNode(filePath string, filters ...string) (*parser.Node, error)
|
||||||
return nil, fmt.Errorf("unsupported file extension: %s", filePath)
|
return nil, fmt.Errorf("unsupported file extension: %s", filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return decodeRawToNode(data, parser.DefaultRootName, filters...)
|
if len(data) == 0 {
|
||||||
|
return nil, fmt.Errorf("no configuration found in file: %s", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := decodeRawToNode(data, parser.DefaultRootName, filters...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(node.Children) == 0 {
|
||||||
|
return nil, fmt.Errorf("no valid configuration found in file: %s", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return node, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRootFieldNames(element interface{}) []string {
|
func getRootFieldNames(element interface{}) []string {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/parser"
|
"github.com/containous/traefik/v2/pkg/config/parser"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_getRootFieldNames(t *testing.T) {
|
func Test_getRootFieldNames(t *testing.T) {
|
||||||
|
@ -42,17 +43,43 @@ func Test_getRootFieldNames(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_decodeFileToNode_errors(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
confFile string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "non existing file",
|
||||||
|
confFile: "./fixtures/not_existing.toml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "file without content",
|
||||||
|
confFile: "./fixtures/empty.toml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "file without any valid configuration",
|
||||||
|
confFile: "./fixtures/no_conf.toml",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
node, err := decodeFileToNode(test.confFile,
|
||||||
|
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, node)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_decodeFileToNode_compare(t *testing.T) {
|
func Test_decodeFileToNode_compare(t *testing.T) {
|
||||||
nodeToml, err := decodeFileToNode("./fixtures/sample.toml",
|
nodeToml, err := decodeFileToNode("./fixtures/sample.toml",
|
||||||
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
|
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeYaml, err := decodeFileToNode("./fixtures/sample.yml")
|
nodeYaml, err := decodeFileToNode("./fixtures/sample.yml")
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, nodeToml, nodeYaml)
|
assert.Equal(t, nodeToml, nodeYaml)
|
||||||
}
|
}
|
||||||
|
@ -60,9 +87,7 @@ func Test_decodeFileToNode_compare(t *testing.T) {
|
||||||
func Test_decodeFileToNode_Toml(t *testing.T) {
|
func Test_decodeFileToNode_Toml(t *testing.T) {
|
||||||
node, err := decodeFileToNode("./fixtures/sample.toml",
|
node, err := decodeFileToNode("./fixtures/sample.toml",
|
||||||
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
|
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := &parser.Node{
|
expected := &parser.Node{
|
||||||
Name: "traefik",
|
Name: "traefik",
|
||||||
|
@ -294,9 +319,7 @@ func Test_decodeFileToNode_Toml(t *testing.T) {
|
||||||
|
|
||||||
func Test_decodeFileToNode_Yaml(t *testing.T) {
|
func Test_decodeFileToNode_Yaml(t *testing.T) {
|
||||||
node, err := decodeFileToNode("./fixtures/sample.yml")
|
node, err := decodeFileToNode("./fixtures/sample.yml")
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := &parser.Node{
|
expected := &parser.Node{
|
||||||
Name: "traefik",
|
Name: "traefik",
|
||||||
|
|
0
pkg/config/file/fixtures/empty.toml
Normal file
0
pkg/config/file/fixtures/empty.toml
Normal file
2
pkg/config/file/fixtures/no_conf.toml
Normal file
2
pkg/config/file/fixtures/no_conf.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[foo]
|
||||||
|
bar = "test"
|
|
@ -21,6 +21,10 @@ func DecodeToNode(labels map[string]string, rootName string, filters ...string)
|
||||||
|
|
||||||
var parts []string
|
var parts []string
|
||||||
for _, v := range split {
|
for _, v := range split {
|
||||||
|
if v == "" {
|
||||||
|
return nil, fmt.Errorf("invalid element: %s", key)
|
||||||
|
}
|
||||||
|
|
||||||
if v[0] == '[' {
|
if v[0] == '[' {
|
||||||
return nil, fmt.Errorf("invalid leading character '[' in field name (bracket is a slice delimiter): %s", v)
|
return nil, fmt.Errorf("invalid leading character '[' in field name (bracket is a slice delimiter): %s", v)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,15 @@ func TestDecodeToNode(t *testing.T) {
|
||||||
in: map[string]string{},
|
in: map[string]string{},
|
||||||
expected: expected{node: nil},
|
expected: expected{node: nil},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid label, ending by a dot",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.http.": "bar",
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
error: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "level 1",
|
desc: "level 1",
|
||||||
in: map[string]string{
|
in: map[string]string{
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ middlewares.Stateful = &captureResponseWriter{}
|
_ middlewares.Stateful = &captureResponseWriterWithCloseNotify{}
|
||||||
)
|
)
|
||||||
|
|
||||||
type capturer interface {
|
type capturer interface {
|
||||||
|
@ -24,7 +24,7 @@ func newCaptureResponseWriter(rw http.ResponseWriter) capturer {
|
||||||
if _, ok := rw.(http.CloseNotifier); !ok {
|
if _, ok := rw.(http.CloseNotifier); !ok {
|
||||||
return capt
|
return capt
|
||||||
}
|
}
|
||||||
return captureResponseWriterWithCloseNotify{capt}
|
return &captureResponseWriterWithCloseNotify{capt}
|
||||||
}
|
}
|
||||||
|
|
||||||
// captureResponseWriter is a wrapper of type http.ResponseWriter
|
// captureResponseWriter is a wrapper of type http.ResponseWriter
|
||||||
|
@ -76,13 +76,6 @@ func (crw *captureResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)
|
||||||
return nil, nil, fmt.Errorf("not a hijacker: %T", crw.rw)
|
return nil, nil, fmt.Errorf("not a hijacker: %T", crw.rw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (crw *captureResponseWriter) CloseNotify() <-chan bool {
|
|
||||||
if c, ok := crw.rw.(http.CloseNotifier); ok {
|
|
||||||
return c.CloseNotify()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (crw *captureResponseWriter) Status() int {
|
func (crw *captureResponseWriter) Status() int {
|
||||||
return crw.status
|
return crw.status
|
||||||
}
|
}
|
||||||
|
|
50
pkg/middlewares/accesslog/capture_response_writer_test.go
Normal file
50
pkg/middlewares/accesslog/capture_response_writer_test.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package accesslog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rwWithCloseNotify struct {
|
||||||
|
*httptest.ResponseRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rwWithCloseNotify) CloseNotify() <-chan bool {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloseNotifier(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
rw http.ResponseWriter
|
||||||
|
desc string
|
||||||
|
implementsCloseNotifier bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
rw: httptest.NewRecorder(),
|
||||||
|
desc: "does not implement CloseNotifier",
|
||||||
|
implementsCloseNotifier: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rw: &rwWithCloseNotify{httptest.NewRecorder()},
|
||||||
|
desc: "implements CloseNotifier",
|
||||||
|
implementsCloseNotifier: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, ok := test.rw.(http.CloseNotifier)
|
||||||
|
assert.Equal(t, test.implementsCloseNotifier, ok)
|
||||||
|
|
||||||
|
rw := newCaptureResponseWriter(test.rw)
|
||||||
|
_, impl := rw.(http.CloseNotifier)
|
||||||
|
assert.Equal(t, test.implementsCloseNotifier, impl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -88,9 +88,11 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeHeader(req, forwardReq, fa.trustForwardHeader)
|
// Ensure tracing headers are in the request before we copy the headers to the
|
||||||
|
// forwardReq.
|
||||||
|
tracing.InjectRequestHeaders(req)
|
||||||
|
|
||||||
tracing.InjectRequestHeaders(forwardReq)
|
writeHeader(req, forwardReq, fa.trustForwardHeader)
|
||||||
|
|
||||||
forwardResponse, forwardErr := httpClient.Do(forwardReq)
|
forwardResponse, forwardErr := httpClient.Do(forwardReq)
|
||||||
if forwardErr != nil {
|
if forwardErr != nil {
|
||||||
|
|
|
@ -3,13 +3,18 @@ package auth
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
|
tracingMiddleware "github.com/containous/traefik/v2/pkg/middlewares/tracing"
|
||||||
"github.com/containous/traefik/v2/pkg/testhelpers"
|
"github.com/containous/traefik/v2/pkg/testhelpers"
|
||||||
|
"github.com/containous/traefik/v2/pkg/tracing"
|
||||||
|
"github.com/opentracing/opentracing-go"
|
||||||
|
"github.com/opentracing/opentracing-go/mocktracer"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/vulcand/oxy/forward"
|
"github.com/vulcand/oxy/forward"
|
||||||
|
@ -394,3 +399,44 @@ func Test_writeHeader(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForwardAuthUsesTracing(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Header.Get("Mockpfx-Ids-Traceid") == "" {
|
||||||
|
t.Errorf("expected Mockpfx-Ids-Traceid header to be present in request")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
next := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||||
|
|
||||||
|
auth := dynamic.ForwardAuth{
|
||||||
|
Address: server.URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
tracer := mocktracer.New()
|
||||||
|
opentracing.SetGlobalTracer(tracer)
|
||||||
|
|
||||||
|
tr, _ := tracing.NewTracing("testApp", 100, &mockBackend{tracer})
|
||||||
|
|
||||||
|
next, err := NewForward(context.Background(), next, auth, "authTest")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
next = tracingMiddleware.NewEntryPoint(context.Background(), tr, "tracingTest", next)
|
||||||
|
|
||||||
|
ts := httptest.NewServer(next)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockBackend struct {
|
||||||
|
opentracing.Tracer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *mockBackend) Setup(componentName string) (opentracing.Tracer, io.Closer, error) {
|
||||||
|
return b.Tracer, ioutil.NopCloser(nil), nil
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-kit/kit/metrics"
|
"github.com/go-kit/kit/metrics"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CollectingCounter is a metrics.Counter implementation that enables access to the CounterValue and LastLabelValues.
|
// CollectingCounter is a metrics.Counter implementation that enables access to the CounterValue and LastLabelValues.
|
||||||
|
@ -56,3 +57,44 @@ func newCollectingRetryMetrics() *collectingRetryMetrics {
|
||||||
func (m *collectingRetryMetrics) ServiceRetriesCounter() metrics.Counter {
|
func (m *collectingRetryMetrics) ServiceRetriesCounter() metrics.Counter {
|
||||||
return m.retriesCounter
|
return m.retriesCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rwWithCloseNotify struct {
|
||||||
|
*httptest.ResponseRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rwWithCloseNotify) CloseNotify() <-chan bool {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloseNotifier(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
rw http.ResponseWriter
|
||||||
|
desc string
|
||||||
|
implementsCloseNotifier bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
rw: httptest.NewRecorder(),
|
||||||
|
desc: "does not implement CloseNotifier",
|
||||||
|
implementsCloseNotifier: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rw: &rwWithCloseNotify{httptest.NewRecorder()},
|
||||||
|
desc: "implements CloseNotifier",
|
||||||
|
implementsCloseNotifier: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, ok := test.rw.(http.CloseNotifier)
|
||||||
|
assert.Equal(t, test.implementsCloseNotifier, ok)
|
||||||
|
|
||||||
|
rw := newResponseRecorder(test.rw)
|
||||||
|
_, impl := rw.(http.CloseNotifier)
|
||||||
|
assert.Equal(t, test.implementsCloseNotifier, impl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ func newResponseRecorder(rw http.ResponseWriter) recorder {
|
||||||
if _, ok := rw.(http.CloseNotifier); !ok {
|
if _, ok := rw.(http.CloseNotifier); !ok {
|
||||||
return rec
|
return rec
|
||||||
}
|
}
|
||||||
return responseRecorderWithCloseNotify{rec}
|
return &responseRecorderWithCloseNotify{rec}
|
||||||
}
|
}
|
||||||
|
|
||||||
// responseRecorder captures information from the response and preserves it for
|
// responseRecorder captures information from the response and preserves it for
|
||||||
|
@ -55,13 +55,6 @@ func (r *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
return r.ResponseWriter.(http.Hijacker).Hijack()
|
return r.ResponseWriter.(http.Hijacker).Hijack()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloseNotify returns a channel that receives at most a
|
|
||||||
// single value (true) when the client connection has gone
|
|
||||||
// away.
|
|
||||||
func (r *responseRecorder) CloseNotify() <-chan bool {
|
|
||||||
return r.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush sends any buffered data to the client.
|
// Flush sends any buffered data to the client.
|
||||||
func (r *responseRecorder) Flush() {
|
func (r *responseRecorder) Flush() {
|
||||||
if f, ok := r.ResponseWriter.(http.Flusher); ok {
|
if f, ok := r.ResponseWriter.(http.Flusher); ok {
|
||||||
|
|
|
@ -152,12 +152,12 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error
|
||||||
|
|
||||||
var data []itemData
|
var data []itemData
|
||||||
for name := range consulServiceNames {
|
for name := range consulServiceNames {
|
||||||
consulServices, err := p.fetchService(ctx, name)
|
consulServices, healthServices, err := p.fetchService(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, consulService := range consulServices {
|
for i, consulService := range consulServices {
|
||||||
address := consulService.ServiceAddress
|
address := consulService.ServiceAddress
|
||||||
if address == "" {
|
if address == "" {
|
||||||
address = consulService.Address
|
address = consulService.Address
|
||||||
|
@ -171,7 +171,7 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error
|
||||||
Port: strconv.Itoa(consulService.ServicePort),
|
Port: strconv.Itoa(consulService.ServicePort),
|
||||||
Labels: tagsToNeutralLabels(consulService.ServiceTags, p.Prefix),
|
Labels: tagsToNeutralLabels(consulService.ServiceTags, p.Prefix),
|
||||||
Tags: consulService.ServiceTags,
|
Tags: consulService.ServiceTags,
|
||||||
Status: consulService.Checks.AggregatedStatus(),
|
Status: healthServices[i].Checks.AggregatedStatus(),
|
||||||
}
|
}
|
||||||
|
|
||||||
extraConf, err := p.getConfiguration(item)
|
extraConf, err := p.getConfiguration(item)
|
||||||
|
@ -187,15 +187,21 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) fetchService(ctx context.Context, name string) ([]*api.CatalogService, error) {
|
func (p *Provider) fetchService(ctx context.Context, name string) ([]*api.CatalogService, []*api.ServiceEntry, error) {
|
||||||
var tagFilter string
|
var tagFilter string
|
||||||
if !p.ExposedByDefault {
|
if !p.ExposedByDefault {
|
||||||
tagFilter = p.Prefix + ".enable=true"
|
tagFilter = p.Prefix + ".enable=true"
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &api.QueryOptions{AllowStale: p.Stale, RequireConsistent: p.RequireConsistent, UseCache: p.Cache}
|
opts := &api.QueryOptions{AllowStale: p.Stale, RequireConsistent: p.RequireConsistent, UseCache: p.Cache}
|
||||||
|
|
||||||
consulServices, _, err := p.client.Catalog().Service(name, tagFilter, opts)
|
consulServices, _, err := p.client.Catalog().Service(name, tagFilter, opts)
|
||||||
return consulServices, err
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
healthServices, _, err := p.client.Health().Service(name, tagFilter, false, opts)
|
||||||
|
return consulServices, healthServices, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) fetchServices(ctx context.Context) (map[string][]string, error) {
|
func (p *Provider) fetchServices(ctx context.Context) (map[string][]string, error) {
|
||||||
|
|
|
@ -307,8 +307,13 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa
|
||||||
|
|
||||||
var servers []dynamic.Server
|
var servers []dynamic.Server
|
||||||
if service.Spec.Type == corev1.ServiceTypeExternalName {
|
if service.Spec.Type == corev1.ServiceTypeExternalName {
|
||||||
|
protocol := "http"
|
||||||
|
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {
|
||||||
|
protocol = "https"
|
||||||
|
}
|
||||||
|
|
||||||
return append(servers, dynamic.Server{
|
return append(servers, dynamic.Server{
|
||||||
URL: fmt.Sprintf("http://%s:%d", service.Spec.ExternalName, portSpec.Port),
|
URL: fmt.Sprintf("%s://%s:%d", protocol, service.Spec.ExternalName, portSpec.Port),
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,11 +380,11 @@ func (c configBuilder) nameAndService(ctx context.Context, namespaceService stri
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fullName := fullServiceName(svcCtx, namespace, service.Name, service.Port)
|
fullName := fullServiceName(svcCtx, namespace, service, service.Port)
|
||||||
|
|
||||||
return fullName, serversLB, nil
|
return fullName, serversLB, nil
|
||||||
case service.Kind == "TraefikService":
|
case service.Kind == "TraefikService":
|
||||||
return fullServiceName(svcCtx, namespace, service.Name, 0), nil, nil
|
return fullServiceName(svcCtx, namespace, service, 0), nil, nil
|
||||||
default:
|
default:
|
||||||
return "", nil, fmt.Errorf("unsupported service kind %s", service.Kind)
|
return "", nil, fmt.Errorf("unsupported service kind %s", service.Kind)
|
||||||
}
|
}
|
||||||
|
@ -394,27 +399,22 @@ func splitSvcNameProvider(name string) (string, string) {
|
||||||
return svc, pvd
|
return svc, pvd
|
||||||
}
|
}
|
||||||
|
|
||||||
func fullServiceName(ctx context.Context, namespace, serviceName string, port int32) string {
|
func fullServiceName(ctx context.Context, namespace string, service v1alpha1.LoadBalancerSpec, port int32) string {
|
||||||
if port != 0 {
|
if port != 0 {
|
||||||
return provider.Normalize(fmt.Sprintf("%s-%s-%d", namespace, serviceName, port))
|
return provider.Normalize(fmt.Sprintf("%s-%s-%d", namespace, service.Name, port))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(serviceName, providerNamespaceSeparator) {
|
if !strings.Contains(service.Name, providerNamespaceSeparator) {
|
||||||
return provider.Normalize(fmt.Sprintf("%s-%s", namespace, serviceName))
|
return provider.Normalize(fmt.Sprintf("%s-%s", namespace, service.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
name, pName := splitSvcNameProvider(serviceName)
|
name, pName := splitSvcNameProvider(service.Name)
|
||||||
if pName == providerName {
|
if pName == providerName {
|
||||||
return provider.Normalize(fmt.Sprintf("%s-%s", namespace, name))
|
return provider.Normalize(fmt.Sprintf("%s-%s", namespace, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, if namespace == "default", we do not know whether it had been intentionally set as such,
|
if service.Namespace != "" {
|
||||||
// or if we're simply hitting the value set by default.
|
log.FromContext(ctx).Warnf("namespace %q is ignored in cross-provider context", service.Namespace)
|
||||||
// But as it is most likely very much the latter,
|
|
||||||
// and we do not want to systematically log spam users in that case,
|
|
||||||
// we skip logging whenever the namespace is "default".
|
|
||||||
if namespace != "default" {
|
|
||||||
log.FromContext(ctx).Warnf("namespace %q is ignored in cross-provider context", namespace)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider.Normalize(name) + providerNamespaceSeparator + pName
|
return provider.Normalize(name) + providerNamespaceSeparator + pName
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
kind: Ingress
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: ""
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: testing.example.com
|
|
@ -313,53 +313,57 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||||
conf.HTTP.Services["default-backend"] = service
|
conf.HTTP.Services["default-backend"] = service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rule := range ingress.Spec.Rules {
|
for _, rule := range ingress.Spec.Rules {
|
||||||
if err := checkStringQuoteValidity(rule.Host); err != nil {
|
if err := checkStringQuoteValidity(rule.Host); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Invalid syntax for host: %s", rule.Host)
|
log.FromContext(ctx).Errorf("Invalid syntax for host: %s", rule.Host)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range rule.HTTP.Paths {
|
if rule.HTTP != nil {
|
||||||
service, err := loadService(client, ingress.Namespace, p.Backend)
|
for _, p := range rule.HTTP.Paths {
|
||||||
if err != nil {
|
service, err := loadService(client, ingress.Namespace, p.Backend)
|
||||||
log.FromContext(ctx).
|
if err != nil {
|
||||||
WithField("serviceName", p.Backend.ServiceName).
|
log.FromContext(ctx).
|
||||||
WithField("servicePort", p.Backend.ServicePort.String()).
|
WithField("serviceName", p.Backend.ServiceName).
|
||||||
Errorf("Cannot create service: %v", err)
|
WithField("servicePort", p.Backend.ServicePort.String()).
|
||||||
continue
|
Errorf("Cannot create service: %v", err)
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if err = checkStringQuoteValidity(p.Path); err != nil {
|
if err = checkStringQuoteValidity(p.Path); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Invalid syntax for path: %s", p.Path)
|
log.FromContext(ctx).Errorf("Invalid syntax for path: %s", p.Path)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceName := provider.Normalize(ingress.Namespace + "-" + p.Backend.ServiceName + "-" + p.Backend.ServicePort.String())
|
serviceName := provider.Normalize(ingress.Namespace + "-" + p.Backend.ServiceName + "-" + p.Backend.ServicePort.String())
|
||||||
var rules []string
|
var rules []string
|
||||||
if len(rule.Host) > 0 {
|
if len(rule.Host) > 0 {
|
||||||
rules = []string{"Host(`" + rule.Host + "`)"}
|
rules = []string{"Host(`" + rule.Host + "`)"}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(p.Path) > 0 {
|
if len(p.Path) > 0 {
|
||||||
rules = append(rules, "PathPrefix(`"+p.Path+"`)")
|
rules = append(rules, "PathPrefix(`"+p.Path+"`)")
|
||||||
}
|
}
|
||||||
|
|
||||||
routerKey := strings.TrimPrefix(provider.Normalize(rule.Host+p.Path), "-")
|
routerKey := strings.TrimPrefix(provider.Normalize(rule.Host+p.Path), "-")
|
||||||
conf.HTTP.Routers[routerKey] = &dynamic.Router{
|
conf.HTTP.Routers[routerKey] = &dynamic.Router{
|
||||||
Rule: strings.Join(rules, " && "),
|
|
||||||
Service: serviceName,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ingress.Spec.TLS) > 0 {
|
|
||||||
// TLS enabled for this ingress, add TLS router
|
|
||||||
conf.HTTP.Routers[routerKey+"-tls"] = &dynamic.Router{
|
|
||||||
Rule: strings.Join(rules, " && "),
|
Rule: strings.Join(rules, " && "),
|
||||||
Service: serviceName,
|
Service: serviceName,
|
||||||
TLS: &dynamic.RouterTLSConfig{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(ingress.Spec.TLS) > 0 {
|
||||||
|
// TLS enabled for this ingress, add TLS router
|
||||||
|
conf.HTTP.Routers[routerKey+"-tls"] = &dynamic.Router{
|
||||||
|
Rule: strings.Join(rules, " && "),
|
||||||
|
Service: serviceName,
|
||||||
|
TLS: &dynamic.RouterTLSConfig{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conf.HTTP.Services[serviceName] = service
|
||||||
}
|
}
|
||||||
conf.HTTP.Services[serviceName] = service
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := p.updateIngressStatus(ingress, client)
|
err := p.updateIngressStatus(ingress, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).Errorf("Error while updating ingress status: %v", err)
|
log.FromContext(ctx).Errorf("Error while updating ingress status: %v", err)
|
||||||
|
|
|
@ -38,6 +38,17 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "Ingress one rule host only",
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Routers: map[string]*dynamic.Router{},
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Services: map[string]*dynamic.Service{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "Ingress with a basic rule on one path",
|
desc: "Ingress with a basic rule on one path",
|
||||||
expected: &dynamic.Configuration{
|
expected: &dynamic.Configuration{
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
"middlewares": {
|
"middlewares": {
|
||||||
"dashboard_redirect": {
|
"dashboard_redirect": {
|
||||||
"redirectRegex": {
|
"redirectRegex": {
|
||||||
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
|
"regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$",
|
||||||
"replacement": "${1}/dashboard/",
|
"replacement": "${1}/dashboard/",
|
||||||
"permanent": true
|
"permanent": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
"middlewares": {
|
"middlewares": {
|
||||||
"dashboard_redirect": {
|
"dashboard_redirect": {
|
||||||
"redirectRegex": {
|
"redirectRegex": {
|
||||||
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
|
"regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$",
|
||||||
"replacement": "${1}/dashboard/",
|
"replacement": "${1}/dashboard/",
|
||||||
"permanent": true
|
"permanent": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) {
|
||||||
|
|
||||||
cfg.HTTP.Middlewares["dashboard_redirect"] = &dynamic.Middleware{
|
cfg.HTTP.Middlewares["dashboard_redirect"] = &dynamic.Middleware{
|
||||||
RedirectRegex: &dynamic.RedirectRegex{
|
RedirectRegex: &dynamic.RedirectRegex{
|
||||||
Regex: `^(http:\/\/[^:]+(:\d+)?)/$`,
|
Regex: `^(http:\/\/[^:\/]+(:\d+)?)\/$`,
|
||||||
Replacement: "${1}/dashboard/",
|
Replacement: "${1}/dashboard/",
|
||||||
Permanent: true,
|
Permanent: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,9 +52,9 @@ func (h *httpForwarder) Accept() (net.Conn, error) {
|
||||||
type TCPEntryPoints map[string]*TCPEntryPoint
|
type TCPEntryPoints map[string]*TCPEntryPoint
|
||||||
|
|
||||||
// NewTCPEntryPoints creates a new TCPEntryPoints.
|
// NewTCPEntryPoints creates a new TCPEntryPoints.
|
||||||
func NewTCPEntryPoints(staticConfiguration static.Configuration) (TCPEntryPoints, error) {
|
func NewTCPEntryPoints(entryPointsConfig static.EntryPoints) (TCPEntryPoints, error) {
|
||||||
serverEntryPointsTCP := make(TCPEntryPoints)
|
serverEntryPointsTCP := make(TCPEntryPoints)
|
||||||
for entryPointName, config := range staticConfiguration.EntryPoints {
|
for entryPointName, config := range entryPointsConfig {
|
||||||
ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName))
|
ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName))
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
@ -171,6 +171,23 @@ func (e *TCPEntryPoint) StartTCP(ctx context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
safe.Go(func() {
|
safe.Go(func() {
|
||||||
|
// Enforce read/write deadlines at the connection level,
|
||||||
|
// because when we're peeking the first byte to determine whether we are doing TLS,
|
||||||
|
// the deadlines at the server level are not taken into account.
|
||||||
|
if e.transportConfiguration.RespondingTimeouts.ReadTimeout > 0 {
|
||||||
|
err := writeCloser.SetReadDeadline(time.Now().Add(time.Duration(e.transportConfiguration.RespondingTimeouts.ReadTimeout)))
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error while setting read deadline: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.transportConfiguration.RespondingTimeouts.WriteTimeout > 0 {
|
||||||
|
err = writeCloser.SetWriteDeadline(time.Now().Add(time.Duration(e.transportConfiguration.RespondingTimeouts.WriteTimeout)))
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error while setting write deadline: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
e.switcher.ServeTCP(newTrackedConnection(writeCloser, e.tracker))
|
e.switcher.ServeTCP(newTrackedConnection(writeCloser, e.tracker))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -191,48 +208,48 @@ func (e *TCPEntryPoint) Shutdown(ctx context.Context) {
|
||||||
logger.Debugf("Waiting %s seconds before killing connections.", graceTimeOut)
|
logger.Debugf("Waiting %s seconds before killing connections.", graceTimeOut)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
shutdownServer := func(server stoppableServer) {
|
||||||
|
defer wg.Done()
|
||||||
|
err := server.Shutdown(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
logger.Debugf("Server failed to shutdown within deadline because: %s", err)
|
||||||
|
if err = server.Close(); err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Error(err)
|
||||||
|
// We expect Close to fail again because Shutdown most likely failed when trying to close a listener.
|
||||||
|
// We still call it however, to make sure that all connections get closed as well.
|
||||||
|
server.Close()
|
||||||
|
}
|
||||||
|
|
||||||
if e.httpServer.Server != nil {
|
if e.httpServer.Server != nil {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go shutdownServer(e.httpServer.Server)
|
||||||
defer wg.Done()
|
|
||||||
if err := e.httpServer.Server.Shutdown(ctx); err != nil {
|
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
|
||||||
logger.Debugf("Wait server shutdown is overdue to: %s", err)
|
|
||||||
err = e.httpServer.Server.Close()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.httpsServer.Server != nil {
|
if e.httpsServer.Server != nil {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go shutdownServer(e.httpsServer.Server)
|
||||||
defer wg.Done()
|
|
||||||
if err := e.httpsServer.Server.Shutdown(ctx); err != nil {
|
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
|
||||||
logger.Debugf("Wait server shutdown is overdue to: %s", err)
|
|
||||||
err = e.httpsServer.Server.Close()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.tracker != nil {
|
if e.tracker != nil {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := e.tracker.Shutdown(ctx); err != nil {
|
err := e.tracker.Shutdown(ctx)
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
if err == nil {
|
||||||
logger.Debugf("Wait hijack connection is overdue to: %s", err)
|
return
|
||||||
e.tracker.Close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
logger.Debugf("Server failed to shutdown before deadline because: %s", err)
|
||||||
|
}
|
||||||
|
e.tracker.Close()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -459,8 +476,11 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
|
||||||
}
|
}
|
||||||
|
|
||||||
serverHTTP := &http.Server{
|
serverHTTP := &http.Server{
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
ErrorLog: httpServerLogger,
|
ErrorLog: httpServerLogger,
|
||||||
|
ReadTimeout: time.Duration(configuration.Transport.RespondingTimeouts.ReadTimeout),
|
||||||
|
WriteTimeout: time.Duration(configuration.Transport.RespondingTimeouts.WriteTimeout),
|
||||||
|
IdleTimeout: time.Duration(configuration.Transport.RespondingTimeouts.IdleTimeout),
|
||||||
}
|
}
|
||||||
|
|
||||||
listener := newHTTPForwarder(ln)
|
listener := newHTTPForwarder(ln)
|
||||||
|
|
|
@ -3,8 +3,11 @@ package server
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -15,128 +18,206 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestShutdownHTTP(t *testing.T) {
|
func TestShutdownHijacked(t *testing.T) {
|
||||||
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
|
||||||
Address: ":0",
|
|
||||||
Transport: &static.EntryPointsTransport{
|
|
||||||
LifeCycle: &static.LifeCycle{
|
|
||||||
RequestAcceptGraceTimeout: 0,
|
|
||||||
GraceTimeOut: types.Duration(5 * time.Second),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ForwardedHeaders: &static.ForwardedHeaders{},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
go entryPoint.StartTCP(context.Background())
|
|
||||||
|
|
||||||
router := &tcp.Router{}
|
|
||||||
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
}))
|
|
||||||
entryPoint.SwitchRouter(router)
|
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
go entryPoint.Shutdown(context.Background())
|
|
||||||
|
|
||||||
request, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8082", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = request.Write(conn)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
resp, err := http.ReadResponse(bufio.NewReader(conn), request)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShutdownHTTPHijacked(t *testing.T) {
|
|
||||||
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
|
||||||
Address: ":0",
|
|
||||||
Transport: &static.EntryPointsTransport{
|
|
||||||
LifeCycle: &static.LifeCycle{
|
|
||||||
RequestAcceptGraceTimeout: 0,
|
|
||||||
GraceTimeOut: types.Duration(5 * time.Second),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ForwardedHeaders: &static.ForwardedHeaders{},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
go entryPoint.StartTCP(context.Background())
|
|
||||||
|
|
||||||
router := &tcp.Router{}
|
router := &tcp.Router{}
|
||||||
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
conn, _, err := rw.(http.Hijacker).Hijack()
|
conn, _, err := rw.(http.Hijacker).Hijack()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
resp := http.Response{StatusCode: http.StatusOK}
|
resp := http.Response{StatusCode: http.StatusOK}
|
||||||
err = resp.Write(conn)
|
err = resp.Write(conn)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}))
|
}))
|
||||||
|
testShutdown(t, router)
|
||||||
entryPoint.SwitchRouter(router)
|
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
go entryPoint.Shutdown(context.Background())
|
|
||||||
|
|
||||||
request, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8082", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = request.Write(conn)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
resp, err := http.ReadResponse(bufio.NewReader(conn), request)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShutdownTCPConn(t *testing.T) {
|
func TestShutdownHTTP(t *testing.T) {
|
||||||
|
router := &tcp.Router{}
|
||||||
|
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}))
|
||||||
|
testShutdown(t, router)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShutdownTCP(t *testing.T) {
|
||||||
|
router := &tcp.Router{}
|
||||||
|
router.AddCatchAllNoTLS(tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
||||||
|
for {
|
||||||
|
_, err := http.ReadRequest(bufio.NewReader(conn))
|
||||||
|
|
||||||
|
if err == io.EOF || (err != nil && strings.HasSuffix(err.Error(), "use of closed network connection")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp := http.Response{StatusCode: http.StatusOK}
|
||||||
|
err = resp.Write(conn)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
testShutdown(t, router)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShutdown(t *testing.T, router *tcp.Router) {
|
||||||
|
epConfig := &static.EntryPointsTransport{}
|
||||||
|
epConfig.SetDefaults()
|
||||||
|
|
||||||
|
epConfig.LifeCycle.RequestAcceptGraceTimeout = 0
|
||||||
|
epConfig.LifeCycle.GraceTimeOut = types.Duration(5 * time.Second)
|
||||||
|
|
||||||
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
||||||
Address: ":0",
|
// We explicitly use an IPV4 address because on Alpine, with an IPV6 address
|
||||||
Transport: &static.EntryPointsTransport{
|
// there seems to be shenanigans related to properly cleaning up file descriptors
|
||||||
LifeCycle: &static.LifeCycle{
|
Address: "127.0.0.1:0",
|
||||||
RequestAcceptGraceTimeout: 0,
|
Transport: epConfig,
|
||||||
GraceTimeOut: types.Duration(5 * time.Second),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ForwardedHeaders: &static.ForwardedHeaders{},
|
ForwardedHeaders: &static.ForwardedHeaders{},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
go entryPoint.StartTCP(context.Background())
|
conn, err := startEntrypoint(entryPoint, router)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
router := &tcp.Router{}
|
epAddr := entryPoint.listener.Addr().String()
|
||||||
router.AddCatchAllNoTLS(tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
|
||||||
_, err := http.ReadRequest(bufio.NewReader(conn))
|
|
||||||
require.NoError(t, err)
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
resp := http.Response{StatusCode: http.StatusOK}
|
request, err := http.NewRequest(http.MethodHead, "http://127.0.0.1:8082", nil)
|
||||||
err = resp.Write(conn)
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
|
||||||
}))
|
|
||||||
|
|
||||||
entryPoint.SwitchRouter(router)
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
|
// We need to do a write on the conn before the shutdown to make it "exist".
|
||||||
|
// Because the connection indeed exists as far as TCP is concerned,
|
||||||
|
// but since we only pass it along to the HTTP server after at least one byte is peaked,
|
||||||
|
// the HTTP server (and hence its shutdown) does not know about the connection until that first byte peaking.
|
||||||
|
err = request.Write(conn)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
go entryPoint.Shutdown(context.Background())
|
go entryPoint.Shutdown(context.Background())
|
||||||
|
|
||||||
request, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8082", nil)
|
// Make sure that new connections are not permitted anymore.
|
||||||
require.NoError(t, err)
|
// Note that this should be true not only after Shutdown has returned,
|
||||||
|
// but technically also as early as the Shutdown has closed the listener,
|
||||||
|
// i.e. during the shutdown and before the gracetime is over.
|
||||||
|
var testOk bool
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
loopConn, err := net.Dial("tcp", epAddr)
|
||||||
|
if err == nil {
|
||||||
|
loopConn.Close()
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(err.Error(), "connection refused") && !strings.HasSuffix(err.Error(), "reset by peer") {
|
||||||
|
t.Fatalf(`unexpected error: got %v, wanted "connection refused" or "reset by peer"`, err)
|
||||||
|
}
|
||||||
|
testOk = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !testOk {
|
||||||
|
t.Fatal("entry point never closed")
|
||||||
|
}
|
||||||
|
|
||||||
err = request.Write(conn)
|
// And make sure that the connection we had opened before shutting things down is still operational
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
resp, err := http.ReadResponse(bufio.NewReader(conn), request)
|
resp, err := http.ReadResponse(bufio.NewReader(conn), request)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startEntrypoint(entryPoint *TCPEntryPoint, router *tcp.Router) (net.Conn, error) {
|
||||||
|
go entryPoint.StartTCP(context.Background())
|
||||||
|
|
||||||
|
entryPoint.SwitchRouter(router)
|
||||||
|
|
||||||
|
var conn net.Conn
|
||||||
|
var err error
|
||||||
|
var epStarted bool
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
conn, err = net.Dial("tcp", entryPoint.listener.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
epStarted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !epStarted {
|
||||||
|
return nil, errors.New("entry point never started")
|
||||||
|
}
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadTimeoutWithoutFirstByte(t *testing.T) {
|
||||||
|
epConfig := &static.EntryPointsTransport{}
|
||||||
|
epConfig.SetDefaults()
|
||||||
|
epConfig.RespondingTimeouts.ReadTimeout = types.Duration(time.Second * 2)
|
||||||
|
|
||||||
|
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
||||||
|
Address: ":0",
|
||||||
|
Transport: epConfig,
|
||||||
|
ForwardedHeaders: &static.ForwardedHeaders{},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
router := &tcp.Router{}
|
||||||
|
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
conn, err := startEntrypoint(entryPoint, router)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
errChan := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
b := make([]byte, 2048)
|
||||||
|
_, err := conn.Read(b)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
require.Equal(t, io.EOF, err)
|
||||||
|
case <-time.Tick(time.Second * 5):
|
||||||
|
t.Error("Timeout while read")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadTimeoutWithFirstByte(t *testing.T) {
|
||||||
|
epConfig := &static.EntryPointsTransport{}
|
||||||
|
epConfig.SetDefaults()
|
||||||
|
epConfig.RespondingTimeouts.ReadTimeout = types.Duration(time.Second * 2)
|
||||||
|
|
||||||
|
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
||||||
|
Address: ":0",
|
||||||
|
Transport: epConfig,
|
||||||
|
ForwardedHeaders: &static.ForwardedHeaders{},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
router := &tcp.Router{}
|
||||||
|
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
conn, err := startEntrypoint(entryPoint, router)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = conn.Write([]byte("GET /some HTTP/1.1\r\n"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
errChan := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
b := make([]byte, 2048)
|
||||||
|
_, err := conn.Read(b)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
require.Equal(t, io.EOF, err)
|
||||||
|
case <-time.Tick(time.Second * 5):
|
||||||
|
t.Error("Timeout while read")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,10 @@ func buildProxy(passHostHeader *bool, responseForwarding *dynamic.ResponseForwar
|
||||||
outReq.ProtoMajor = 1
|
outReq.ProtoMajor = 1
|
||||||
outReq.ProtoMinor = 1
|
outReq.ProtoMinor = 1
|
||||||
|
|
||||||
|
if _, ok := outReq.Header["User-Agent"]; !ok {
|
||||||
|
outReq.Header.Set("User-Agent", "")
|
||||||
|
}
|
||||||
|
|
||||||
// Do not pass client Host header unless optsetter PassHostHeader is set.
|
// Do not pass client Host header unless optsetter PassHostHeader is set.
|
||||||
if passHostHeader != nil && !*passHostHeader {
|
if passHostHeader != nil && !*passHostHeader {
|
||||||
outReq.Host = outReq.URL.Host
|
outReq.Host = outReq.URL.Host
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/log"
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
)
|
)
|
||||||
|
@ -34,7 +35,23 @@ func (r *Router) ServeTCP(conn WriteCloser) {
|
||||||
}
|
}
|
||||||
|
|
||||||
br := bufio.NewReader(conn)
|
br := bufio.NewReader(conn)
|
||||||
serverName, tls, peeked := clientHelloServerName(br)
|
serverName, tls, peeked, err := clientHelloServerName(br)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove read/write deadline and delegate this to underlying tcp server (for now only handled by HTTP Server)
|
||||||
|
err = conn.SetReadDeadline(time.Time{})
|
||||||
|
if err != nil {
|
||||||
|
log.WithoutContext().Errorf("Error while setting read deadline: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.SetWriteDeadline(time.Time{})
|
||||||
|
if err != nil {
|
||||||
|
log.WithoutContext().Errorf("Error while setting write deadline: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if !tls {
|
if !tls {
|
||||||
switch {
|
switch {
|
||||||
case r.catchAllNoTLS != nil:
|
case r.catchAllNoTLS != nil:
|
||||||
|
@ -176,33 +193,34 @@ func (c *Conn) Read(p []byte) (n int, err error) {
|
||||||
// clientHelloServerName returns the SNI server name inside the TLS ClientHello,
|
// clientHelloServerName returns the SNI server name inside the TLS ClientHello,
|
||||||
// without consuming any bytes from br.
|
// without consuming any bytes from br.
|
||||||
// On any error, the empty string is returned.
|
// On any error, the empty string is returned.
|
||||||
func clientHelloServerName(br *bufio.Reader) (string, bool, string) {
|
func clientHelloServerName(br *bufio.Reader) (string, bool, string, error) {
|
||||||
hdr, err := br.Peek(1)
|
hdr, err := br.Peek(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
opErr, ok := err.(*net.OpError)
|
||||||
log.Errorf("Error while Peeking first byte: %s", err)
|
if err != io.EOF && (!ok || !opErr.Timeout()) {
|
||||||
|
log.WithoutContext().Errorf("Error while Peeking first byte: %s", err)
|
||||||
}
|
}
|
||||||
return "", false, ""
|
return "", false, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordTypeHandshake = 0x16
|
const recordTypeHandshake = 0x16
|
||||||
if hdr[0] != recordTypeHandshake {
|
if hdr[0] != recordTypeHandshake {
|
||||||
// log.Errorf("Error not tls")
|
// log.Errorf("Error not tls")
|
||||||
return "", false, getPeeked(br) // Not TLS.
|
return "", false, getPeeked(br), nil // Not TLS.
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordHeaderLen = 5
|
const recordHeaderLen = 5
|
||||||
hdr, err = br.Peek(recordHeaderLen)
|
hdr, err = br.Peek(recordHeaderLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error while Peeking hello: %s", err)
|
log.Errorf("Error while Peeking hello: %s", err)
|
||||||
return "", false, getPeeked(br)
|
return "", false, getPeeked(br), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
|
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
|
||||||
helloBytes, err := br.Peek(recordHeaderLen + recLen)
|
helloBytes, err := br.Peek(recordHeaderLen + recLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error while Hello: %s", err)
|
log.Errorf("Error while Hello: %s", err)
|
||||||
return "", true, getPeeked(br)
|
return "", true, getPeeked(br), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sni := ""
|
sni := ""
|
||||||
|
@ -214,7 +232,7 @@ func clientHelloServerName(br *bufio.Reader) (string, bool, string) {
|
||||||
})
|
})
|
||||||
_ = server.Handshake()
|
_ = server.Handshake()
|
||||||
|
|
||||||
return sni, true, getPeeked(br)
|
return sni, true, getPeeked(br), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPeeked(br *bufio.Reader) string {
|
func getPeeked(br *bufio.Reader) string {
|
||||||
|
|
Loading…
Reference in a new issue