Merge branch 'v2.1' into master

This commit is contained in:
Fernandez Ludovic 2020-01-07 17:35:07 +01:00
commit da3d814c8b
56 changed files with 2162 additions and 558 deletions

View file

@ -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)

View file

@ -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
} }

View file

@ -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`

View file

@ -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
# ... # ...
``` ```

View file

@ -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"

View file

@ -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"

View file

@ -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.
# #

View file

@ -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

View file

@ -1,5 +1,5 @@
certificatesResolvers: certificatesResolvers:
sample: le:
# Enable ACME (Let's Encrypt): automatic SSL. # Enable ACME (Let's Encrypt): automatic SSL.
acme: acme:

View file

@ -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

View file

@ -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`

View file

@ -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:

View 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.

View file

@ -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`)"

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"
``` ```

View file

@ -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

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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)
}

View file

@ -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
}, },

View file

@ -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
}, },

View file

@ -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
}, },

View file

@ -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
}, },

View file

@ -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
}, },

View file

@ -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 {

View file

@ -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",

View file

View file

@ -0,0 +1,2 @@
[foo]
bar = "test"

View file

@ -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)
} }

View file

@ -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{

View file

@ -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
} }

View 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)
})
}
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
})
}
}

View file

@ -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 {

View file

@ -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) {

View file

@ -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

View file

@ -0,0 +1,9 @@
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
name: ""
namespace: testing
spec:
rules:
- host: testing.example.com

View file

@ -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)

View file

@ -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{

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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,
}, },

View file

@ -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)

View file

@ -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")
}
} }

View file

@ -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

View file

@ -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 {