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

View file

@ -3,79 +3,325 @@
The Kubernetes Ingress Controller, The Custom Resource Way. The Kubernetes Ingress Controller, The Custom Resource Way.
{: .subtitle } {: .subtitle }
## Resource Configuration ## Configuration Examples
If you're in a hurry, maybe you'd rather go through the [dynamic configuration](../../reference/dynamic-configuration/kubernetes-crd.md) reference. ??? example "Configuring KubernetesCRD and Deploying/Exposing Services"
### Traefik IngressRoute definition ```yaml tab="Resource Definition"
# All resources definition must be declared
```yaml --8<-- "content/reference/dynamic-configuration/kubernetes-crd-definition.yml"
--8<-- "content/routing/providers/crd_ingress_route.yml"
``` ```
That `IngressRoute` kind can then be used to define an `IngressRoute` object, such as in: ```yaml tab="RBAC"
--8<-- "content/reference/dynamic-configuration/kubernetes-crd-rbac.yml"
```
```yaml tab="Traefik"
apiVersion: v1
kind: ServiceAccount
metadata:
name: traefik-ingress-controller
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: traefik
labels:
app: traefik
spec:
replicas: 1
selector:
matchLabels:
app: traefik
template:
metadata:
labels:
app: traefik
spec:
serviceAccountName: traefik-ingress-controller
containers:
- name: traefik
image: traefik:v2.1
args:
- --log.level=DEBUG
- --api
- --api.insecure
- --entrypoints.web.address=:80
- --providers.kubernetescrd
ports:
- name: web
containerPort: 80
- name: admin
containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: traefik
spec:
type: LoadBalancer
selector:
app: traefik
ports:
- protocol: TCP
port: 80
name: web
targetPort: 80
- protocol: TCP
port: 8080
name: admin
targetPort: 8080
```
```yaml tab="IngressRoute"
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: myingressroute
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`foo`) && PathPrefix(`/bar`)
kind: Rule
services:
- name: whoami
port: 80
```
```yaml tab="Whoami"
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: whoami
namespace: default
labels:
app: containous
name: whoami
spec:
replicas: 2
selector:
matchLabels:
app: containous
task: whoami
template:
metadata:
labels:
app: containous
task: whoami
spec:
containers:
- name: containouswhoami
image: containous/whoami
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: whoami
namespace: default
spec:
ports:
- name: http
port: 80
selector:
app: containous
task: whoami
```
## Routing Configuration
### Custom Resource Definition (CRD)
* You can find an exhaustive list, generated from Traefik's source code, of the custom resources and their attributes in [the reference page](../../reference/dynamic-configuration/kubernetes-crd.md).
* Validate that [the prerequisites](../../providers/kubernetes-crd.md#configuration-requirements) are fulfilled before using the Traefik custom resources.
* Traefik CRDs are building blocks that you can assemble according to your needs.
You can find an excerpt of the available custom resources in the table below:
| Kind | Purpose | Concept Behind |
|------------------------------------------|---------------------------------------------------------------|----------------------------------------------------------------|
| [IngressRoute](#kind-ingressroute) | HTTP Routing | [HTTP router](../routers/index.md#configuring-http-routers) |
| [Middleware](#kind-middleware) | Tweaks the HTTP requests before they are sent to your service | [HTTP Middlewares](../../middlewares/overview.md) |
| [TraefikService](#kind-traefikservice) | Abstraction for HTTP loadbalancing/mirroring | [HTTP service](../services/index.md#configuring-http-services) |
| [IngressRouteTCP](#kind-ingressroutetcp) | TCP Routing | [TCP router](../routers/index.md#configuring-tcp-routers) |
| [TLSOptions](#kind-tlsoption) | Allows to configure some parameters of the TLS connection | [TLSOptions](../../https/tls.md#tls-options) |
### Kind: `IngressRoute`
`IngressRoute` is the CRD implementation of a [Traefik HTTP router](../routers/index.md#configuring-http-routers).
Register the `IngressRoute` kind in the Kubernetes cluster before creating `IngressRoute` objects.
!!! info "IngressRoute Attributes"
```yaml ```yaml
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute kind: IngressRoute
metadata: metadata:
name: ingressroutefoo name: foo
namespace: bar
spec:
entryPoints: # [1]
- foo
routes: # [2]
- kind: Rule
match: Host(`test.domain.com`) # [3]
priority: 10 # [4]
middlewares: # [5]
- name: middleware1 # [6]
namespace: default # [7]
services: # [8]
- kind: Service
name: foo
namespace: default
passHostHeader: true
port: 80
responseForwarding:
flushInterval: 1ms
scheme: https
sticky:
cookie:
httpOnly: true
name: cookie
secure: true
strategy: RoundRobin
weight: 10
tls: # [9]
secretName: supersecret # [10]
options: # [11]
name: opt # [12]
namespace: default # [13]
certResolver: foo # [14]
domains: # [15]
- main: foo.com # [16]
sans: # [17]
- a.foo.com
- b.foo.com
```
| Ref | Attribute | Purpose |
|------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [1] | `entryPoints` | List of [entry points](../routers/index.md#entrypoints) name |
| [2] | `routes` | List of route |
| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule) corresponding to an underlying router. |
| [4] | `routes[n].priority` | [Disambiguate](../routers/index.md#priority) rules of the same length, for route matching |
| [5] | `routes[n].middlewares` | List of reference to [Middleware](#kind-middleware) |
| [6] | `middlewares[n].name` | Defines the [Middleware](#kind-middleware) name |
| [7] | `middlewares[n].namespace` | Defines the [Middleware](#kind-middleware) namespace |
| [8] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) |
| [9] | `tls` | Defines [TLS](../routers/index.md#tls) certificate configuration |
| [10] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) |
| [11] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) |
| [12] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name |
| [13] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace |
| [14] | `tls.cetResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver) |
| [15] | `tls.domains` | List of [domains](../routers/index.md#domains) |
| [16] | `domains[n].main` | Defines the main domain name |
| [17] | `domains[n].sans` | List of SANs (alternative domains) |
??? example "Declaring an IngressRoute"
```yaml tab="IngressRoute"
# All resources definition must be declared
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: testName
namespace: default
spec: spec:
entryPoints: entryPoints:
- web - web
routes: routes:
# Match is the rule corresponding to an underlying router. - kind: Rule
# Later on, match could be the simple form of a path prefix, e.g. just "/bar", match: Host(`test.domain.com`)
# but for now we only support a traefik style matching rule. middlewares:
- match: Host(`foo.com`) && PathPrefix(`/bar`) - name: middleware1
# kind could eventually be one of "Rule", "Path", "Host", "Method", "Header", namespace: default
# "Parameter", etc, to support simpler forms of rule matching, but for now we priority: 10
# only support "Rule".
kind: Rule
# (optional) Priority disambiguates rules of the same length, for route matching.
priority: 12
services: services:
- name: whoami - kind: Service
port: 80 name: foo
# (default 1) A weight used by the weighted round-robin strategy (WRR). namespace: default
weight: 1
# (default true) PassHostHeader controls whether to leave the request's Host
# Header as it was before it reached the proxy, or whether to let the proxy set it
# to the destination (backend) host.
passHostHeader: true passHostHeader: true
port: 80
responseForwarding: responseForwarding:
# (default 100ms) Interval between flushes of the buffered response body to the client. flushInterval: 1ms
flushInterval: 100ms scheme: https
sticky:
cookie:
httpOnly: true
name: cookie
secure: true
strategy: RoundRobin
weight: 10
tls:
certResolver: foo
domains:
- main: foo.com
sans:
- a.foo.com
- b.foo.com
options:
name: opt
namespace: default
secretName: supersecret
```
--- ```yaml tab="Middlewares"
# All resources definition must be declared
# Prefixing with /foo
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP kind: Middleware
metadata: metadata:
name: ingressroutetcpfoo.crd name: middleware1
namespace: default
spec:
addPrefix:
prefix: /foo
```
```yaml tab="TLSOption"
apiVersion: traefik.containo.us/v1alpha1
kind: TLSOption
metadata:
name: opt
namespace: default
spec: spec:
entryPoints: minVersion: VersionTLS12
- footcp
routes:
# Match is the rule corresponding to an underlying router.
- match: HostSNI(`*`)
services:
- name: whoamitcp
port: 8080
``` ```
### Middleware ```yaml tab="Secret"
apiVersion: v1
kind: Secret
metadata:
name: supersecret
Additionally, to allow for the use of middlewares in an `IngressRoute`, we defined the CRD below for the `Middleware` kind. data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
```yaml tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
--8<-- "content/routing/providers/crd_middlewares.yml"
``` ```
Once the `Middleware` kind has been registered with the Kubernetes cluster, it can then be used in `IngressRoute` definitions, such as: ### Kind: `Middleware`
```yaml `Middleware` is the CRD implementation of a [Traefik middleware](../../middlewares/overview.md).
Register the `Middleware` kind in the Kubernetes cluster before creating `Middleware` objects or referencing middlewares in the [`IngressRoute`](#kind-ingressroute) objects.
??? "Declaring and Referencing a Middleware"
```yaml tab="Middleware"
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: Middleware kind: Middleware
metadata: metadata:
@ -86,8 +332,9 @@ spec:
stripPrefix: stripPrefix:
prefixes: prefixes:
- /stripit - /stripit
```
--- ```yaml tab="IngressRoute"
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute kind: IngressRoute
metadata: metadata:
@ -111,28 +358,116 @@ spec:
As Kubernetes also has its own notion of namespace, one should not confuse the kubernetes namespace of a resource As Kubernetes also has its own notion of namespace, one should not confuse the kubernetes namespace of a resource
(in the reference to the middleware) with the [provider namespace](../../middlewares/overview.md#provider-namespace), (in the reference to the middleware) with the [provider namespace](../../middlewares/overview.md#provider-namespace),
when the definition of the middleware is from another provider. when the definition of the middleware comes from another provider.
In this context, specifying a namespace when referring to the resource does not make any sense, and will be ignored. In this context, specifying a namespace when referring to the resource does not make any sense, and will be ignored.
More information about available middlewares in the dedicated [middlewares section](../../middlewares/overview.md). More information about available middlewares in the dedicated [middlewares section](../../middlewares/overview.md).
### Services ### Kind: `TraefikService`
If one needs a setup more sophisticated than a load-balancer of servers (which is a Kubernetes Service kind behind the scenes), `TraefikService` is the CRD implementation of a ["Traefik Service"](../services/index.md).
one can define and use additional service objects specific to Traefik, based on the `TraefikService` kind defined in the CRD below.
```yaml Register the `TraefikService` kind in the Kubernetes cluster before creating `TraefikService` objects,
--8<-- "content/routing/providers/crd_traefikservice.yml" referencing services in the [`IngressRoute`](#kind-ingressroute)/[`IngressRouteTCP`](#kind-ingressroutetcp) objects or recursively in others `TraefikService` objects.
!!! info "Disambiguate Traefik and Kubernetes Services "
As the field `name` can reference different types of objects, use the field `kind` to avoid any ambiguity.
The field `kind` allows the following values:
* `Service` (default value): to reference a [Kubernetes Service](https://kubernetes.io/docs/concepts/services-networking/service/)
* `TraefikService`: to reference another [Traefik Service](../services/index.md)
`TraefikService` object allows to use any (valid) combinations of:
* servers [load balancing](#server-load-balancing).
* services [Weighted Round Robin](#weighted-round-robin) load balancing.
* services [mirroring](#mirroring).
#### Server Load Balancing
More information in the dedicated server [load balancing](../services/index.md#load-balancing) section.
??? "Declaring and Using Server Load Balancing"
```yaml tab="IngressRoute"
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: ingressroutebar
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`bar.com`) && PathPrefix(`/foo`)
kind: Rule
services:
- name: svc1
namespace: default
- name: svc2
namespace: default
``` ```
Once the `TraefikService` kind has been registered with the Kubernetes cluster, it can then be used in `IngressRoute` definitions ```yaml tab="K8s Service"
(as well as recursively in other Traefik Services), such as below. apiVersion: v1
Note how the `name` field in the IngressRoute definition now refers to a TraefikService instead of a (Kubernetes) Service. kind: Service
The reason this is allowed, and why a `name` can refer either to a TraefikService or a Service, metadata:
is because the `kind` field is used to break the ambiguity. The allowed values for this field are `TraefikService`, or `Service` name: svc1
(which is the default value). namespace: default
```yaml spec:
ports:
- name: http
port: 80
selector:
app: containous
task: app1
---
apiVersion: v1
kind: Service
metadata:
name: svc2
namespace: default
spec:
ports:
- name: http
port: 80
selector:
app: containous
task: app2
```
#### Weighted Round Robin
More information in the dedicated [Weighted Round Robin](../services/index.md#weighted-round-robin-service) service load balancing section.
??? "Declaring and Using Weighted Round Robin"
```yaml tab="IngressRoute"
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: ingressroutebar
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`bar.com`) && PathPrefix(`/foo`)
kind: Rule
services:
- name: wrr1
namespace: default
kind: TraefikService
```
```yaml tab="Weighted Round Robin"
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService kind: TraefikService
metadata: metadata:
@ -142,38 +477,91 @@ metadata:
spec: spec:
weighted: weighted:
services: services:
- name: s2 - name: svc1
kind: Service
port: 80 port: 80
weight: 1 weight: 1
- name: s3 - name: wrr2
kind: TraefikService
weight: 1
- name: mirror1
kind: TraefikService
weight: 1 weight: 1
port: 80
--- ---
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService kind: TraefikService
metadata: metadata:
name: mirror1 name: wrr2
namespace: default namespace: default
spec: spec:
mirroring: weighted:
name: wrr1 services:
kind: TraefikService - name: svc2
mirrors:
- name: s1
percent: 20
port: 80 port: 80
weight: 1
- name: svc3
port: 80
weight: 1
```
```yaml tab="K8s Service"
apiVersion: v1
kind: Service
metadata:
name: svc1
namespace: default
spec:
ports:
- name: http
port: 80
selector:
app: containous
task: app1
--- ---
apiVersion: v1
kind: Service
metadata:
name: svc2
namespace: default
spec:
ports:
- name: http
port: 80
selector:
app: containous
task: app2
---
apiVersion: v1
kind: Service
metadata:
name: svc3
namespace: default
spec:
ports:
- name: http
port: 80
selector:
app: containous
task: app3
```
#### Mirroring
More information in the dedicated [mirroring](../services/index.md#mirroring-service) service section.
??? "Declaring and Using Mirroring"
```yaml tab="IngressRoute"
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute kind: IngressRoute
metadata: metadata:
name: ingressroutebar name: ingressroutebar
namespace: default namespace: default
spec: spec:
entryPoints: entryPoints:
- web - web
@ -186,26 +574,252 @@ spec:
kind: TraefikService kind: TraefikService
``` ```
```yaml tab="Mirroring k8s Service"
# Mirroring from a k8s Service
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: mirror1
namespace: default
spec:
mirroring:
name: svc1
port: 80
mirrors:
- name: svc2
port: 80
percent: 20
- name: svc3
kind: TraefikService
percent: 20
```
```yaml tab="Mirroring Traefik Service"
# Mirroring from a Traefik Service
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: mirror1
namespace: default
spec:
mirroring:
name: wrr1
kind: TraefikService
mirrors:
- name: svc2
port: 80
percent: 20
- name: svc3
kind: TraefikService
percent: 20
```
```yaml tab="K8s Service"
apiVersion: v1
kind: Service
metadata:
name: svc1
namespace: default
spec:
ports:
- name: http
port: 80
selector:
app: containous
task: app1
---
apiVersion: v1
kind: Service
metadata:
name: svc2
namespace: default
spec:
ports:
- name: http
port: 80
selector:
app: containous
task: app2
```
!!! important "References and namespaces" !!! important "References and namespaces"
If the optional `namespace` attribute is not set, the configuration will be applied with the namespace of the current resource. If the optional `namespace` attribute is not set, the configuration will be applied with the namespace of the current resource.
Additionally, when the definition of the `TraefikService` is from another provider, Additionally, when the definition of the `TraefikService` is from another provider,
the cross-provider syntax (service@provider) should be used to refer to the `TraefikService`, just as in the middleware case. the cross-provider syntax (`service@provider`) should be used to refer to the `TraefikService`, just as in the middleware case.
Specifying a namespace attribute in this case would not make any sense, and will be ignored (except if the provider is `kubernetescrd`). Specifying a namespace attribute in this case would not make any sense, and will be ignored (except if the provider is `kubernetescrd`).
### TLS Option ### Kind `IngressRouteTCP`
Additionally, to allow for the use of TLS options in an IngressRoute, we defined the CRD below for the TLSOption kind. `IngressRouteTCP` is the CRD implementation of a [Traefik TCP router](../routers/index.md#configuring-tcp-routers).
More information about TLS Options is available in the dedicated [TLS Configuration Options](../../../https/tls/#tls-options).
Register the `IngressRouteTCP` kind in the Kubernetes cluster before creating `IngressRouteTCP` objects.
!!! info "IngressRouteTCP Attributes"
```yaml ```yaml
--8<-- "content/routing/providers/crd_tls_option.yml" apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
name: ingressroutetcpfoo
spec:
entryPoints: # [1]
- footcp
routes: # [2]
- match: HostSNI(`*`) # [3]
services: # [4]
- name: foo # [5]
port: 8080 # [6]
weight: 10 # [7]
TerminationDelay: 400 # [8]
tls: # [9]
secretName: supersecret # [10]
options: # [11]
name: opt # [12]
namespace: default # [13]
certResolver: foo # [14]
domains: # [15]
- main: foo.com # [16]
sans: # [17]
- a.foo.com
- b.foo.com
passthrough: false # [18]
``` ```
Once the TLSOption kind has been registered with the Kubernetes cluster or defined in the File Provider, it can then be used in IngressRoute definitions, such as: | Ref | Attribute | Purpose |
|------|--------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [1] | `entryPoints` | List of [entrypoints](../routers/index.md#entrypoints_1) name |
| [2] | `routes` | List of route |
| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule_1) corresponding to an underlying router. |
| [4] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) |
| [5] | `services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) |
| [6] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) |
| [7] | `services[n].weight` | Defines the weight to apply to the server load balancing |
| [8] | `services[n].TerminationDelay` | corresponds to the deadline that the proxy sets, after one of its connected peers indicates it has closed the writing capability of its connection, to close the reading capability as well, hence fully terminating the connection.<br/>It is a duration in milliseconds, defaulting to 100. A negative value means an infinite deadline (i.e. the reading capability is never closed). |
| [9] | `tls` | Defines [TLS](../routers/index.md#tls_1) certificate configuration |
| [10] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) |
| [11] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) |
| [12] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name |
| [13] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace |
| [14] | `tls.cetResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver_1) |
| [15] | `tls.domains` | List of [domains](../routers/index.md#domains_1) |
| [16] | `domains[n].main` | Defines the main domain name |
| [17] | `domains[n].sans` | List of SANs (alternative domains) |
| [18] | `tls.passthrough` | If `true`, delegates the TLS termination to the backend |
```yaml ??? example "Declaring an IngressRouteTCP"
```yaml tab="IngressRouteTCP"
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
name: ingressroutetcpfoo
spec:
entryPoints:
- footcp
routes:
# Match is the rule corresponding to an underlying router.
- match: HostSNI(`*`)
services:
- name: foo
port: 8080
TerminationDelay: 400
weight: 10
- name: bar
port: 8081
TerminationDelay: 500
weight: 10
tls:
certResolver: foo
domains:
- main: foo.com
sans:
- a.foo.com
- b.foo.com
options:
name: opt
namespace: default
secretName: supersecret
passthrough: false
```
```yaml tab="TLSOption"
apiVersion: traefik.containo.us/v1alpha1
kind: TLSOption
metadata:
name: opt
namespace: default
spec:
minVersion: VersionTLS12
```
```yaml tab="Secret"
apiVersion: v1
kind: Secret
metadata:
name: supersecret
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
```
### Kind: `TLSOption`
`TLSOption` is the CRD implementation of a [Traefik "TLS Option"](../../https/tls.md#tls-options).
Register the `TLSOption` kind in the Kubernetes cluster before creating `TLSOption` objects
or referencing TLS options in the [`IngressRoute`](#kind-ingressroute) / [`IngressRouteTCP`](#kind-ingressroutetcp) objects.
!!! info "TLSOption Attributes"
```yaml tab="TLSOption"
apiVersion: traefik.containo.us/v1alpha1
kind: TLSOption
metadata:
name: mytlsoption
namespace: default
spec:
minVersion: VersionTLS12 # [1]
maxVersion: VersionTLS13 # [1]
curvePreferences: # [3]
- CurveP521
- CurveP384
cipherSuites: # [4]
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_RSA_WITH_AES_256_GCM_SHA384
clientAuth: # [5]
secretNames: # [6]
- secretCA1
- secretCA2
clientAuthType: VerifyClientCertIfGiven # [7]
sniStrict: true # [8]
```
| Ref | Attribute | Purpose |
|-----|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [1] | `minVersion` | Defines the [minimum TLS version](../../https/tls.md#minimum-tls-version) that is acceptable |
| [2] | `maxVersion` | Defines the [maximum TLS version](../../https/tls.md#maximum-tls-version) that is acceptable |
| [3] | `cipherSuites` | list of supported [cipher suites](../../https/tls.md#cipher-suites) for TLS versions up to TLS 1.2 |
| [4] | `curvePreferences` | List of the [elliptic curves references](../../https/tls.md#curve-preferences) that will be used in an ECDHE handshake, in preference order |
| [5] | `clientAuth` | determines the server's policy for TLS [Client Authentication](../../https/tls.md#client-authentication-mtls) |
| [6] | `clientAuth.secretNames` | list of names of the referenced Kubernetes [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) (in TLSOption namespace) |
| [7] | `clientAuth.clientAuthType` | defines the client authentication type to apply. The available values are: `NoClientCert`, `RequestClientCert`, `VerifyClientCertIfGiven` and `RequireAndVerifyClientCert` |
| [8] | `sniStrict` | if `true`, Traefik won't allow connections from clients connections that do not specify a server_name extension |
??? example "Declaring and referencing a TLSOption"
```yaml tab="TLSOption"
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: TLSOption kind: TLSOption
metadata: metadata:
@ -214,8 +828,18 @@ metadata:
spec: spec:
minVersion: VersionTLS12 minVersion: VersionTLS12
sniStrict: true
cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_RSA_WITH_AES_256_GCM_SHA384
clientAuth:
secretNames:
- secretCA1
- secretCA2
clientAuthType: VerifyClientCertIfGiven
```
--- ```yaml tab="IngressRoute"
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute kind: IngressRoute
metadata: metadata:
@ -236,6 +860,27 @@ spec:
namespace: default namespace: default
``` ```
```yaml tab="Secrets"
apiVersion: v1
kind: Secret
metadata:
name: secretCA1
namespace: default
data:
tls.ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
---
apiVersion: v1
kind: Secret
metadata:
name: secretCA2
namespace: default
data:
tls.ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
```
!!! important "References and namespaces" !!! important "References and namespaces"
If the optional `namespace` attribute is not set, the configuration will be applied with the namespace of the IngressRoute. If the optional `namespace` attribute is not set, the configuration will be applied with the namespace of the IngressRoute.
@ -245,39 +890,6 @@ spec:
just as in the [middleware case](../../middlewares/overview.md#provider-namespace). just as in the [middleware case](../../middlewares/overview.md#provider-namespace).
Specifying a namespace attribute in this case would not make any sense, and will be ignored. Specifying a namespace attribute in this case would not make any sense, and will be ignored.
### TLS
To allow for TLS, we made use of the `Secret` kind, as it was already defined, and it can be directly used in an `IngressRoute`:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: supersecret
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: ingressroutetls
spec:
entryPoints:
- websecure
routes:
- match: Host(`foo.com`) && PathPrefix(`/bar`)
kind: Rule
services:
- name: whoami
port: 443
tls:
secretName: supersecret
```
## Further ## Further
Also see the [full example](../../user-guides/crd-acme/index.md) with Let's Encrypt. Also see the [full example](../../user-guides/crd-acme/index.md) with Let's Encrypt.

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
reg := &api.AgentServiceRegistration{
ID: "whoami1",
Name: "whoami",
Tags: []string{
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.router1.rule=Path(`/whoami`)", "traefik.http.routers.router1.rule=Path(`/whoami`)",
"traefik.http.routers.router1.service=service1", "traefik.http.routers.router1.service=service1",
"traefik.http.services.service1.loadBalancer.server.url=http://" + s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "traefik.http.services.service1.loadBalancer.server.url=http://" + containerIP,
},
Port: 80,
Address: containerIP,
} }
err := s.registerService(reg, false)
err := s.registerService("whoami1", "whoami", s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "80", labels, 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{
ID: "whoamitcp",
Name: "whoamitcp",
Tags: []string{
"traefik.tcp.Routers.Super.Rule=HostSNI(`my.super.host`)", "traefik.tcp.Routers.Super.Rule=HostSNI(`my.super.host`)",
"traefik.tcp.Routers.Super.tls=true", "traefik.tcp.Routers.Super.tls=true",
"traefik.tcp.Services.Super.Loadbalancer.server.port=8080", "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{
ID: "whoami1",
Name: "whoami",
Tags: []string{
"traefik.http.Routers.Super.Rule=Host(`my.super.host`)", "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{
ID: "whoami2",
Name: "whoami",
Tags: []string{
"traefik.http.Routers.SuperHost.Rule=Host(`my-super.host`)", "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{
ID: "whoami1",
Name: "whoami",
Tags: []string{
"traefik.random.value=my.super.host", "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,12 +313,14 @@ 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
} }
if rule.HTTP != nil {
for _, p := range rule.HTTP.Paths { for _, p := range rule.HTTP.Paths {
service, err := loadService(client, ingress.Namespace, p.Backend) service, err := loadService(client, ingress.Namespace, p.Backend)
if err != nil { if err != nil {
@ -360,6 +362,8 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
} }
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
if e.httpServer.Server != nil {
wg.Add(1) shutdownServer := func(server stoppableServer) {
go func() {
defer wg.Done() defer wg.Done()
if err := e.httpServer.Server.Shutdown(ctx); err != nil { err := server.Shutdown(ctx)
if err == nil {
return
}
if ctx.Err() == context.DeadlineExceeded { if ctx.Err() == context.DeadlineExceeded {
logger.Debugf("Wait server shutdown is overdue to: %s", err) logger.Debugf("Server failed to shutdown within deadline because: %s", err)
err = e.httpServer.Server.Close() if err = server.Close(); err != nil {
if err != nil {
logger.Error(err) 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 {
wg.Add(1)
go shutdownServer(e.httpServer.Server)
} }
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 err == nil {
return
}
if ctx.Err() == context.DeadlineExceeded { if ctx.Err() == context.DeadlineExceeded {
logger.Debugf("Wait hijack connection is overdue to: %s", err) logger.Debugf("Server failed to shutdown before deadline because: %s", err)
}
e.tracker.Close() e.tracker.Close()
}
}
}() }()
} }
@ -461,6 +478,9 @@ 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)
router := &tcp.Router{}
router.AddCatchAllNoTLS(tcp.HandlerFunc(func(conn tcp.WriteCloser) {
_, err := http.ReadRequest(bufio.NewReader(conn))
require.NoError(t, err) require.NoError(t, err)
time.Sleep(1 * time.Second)
resp := http.Response{StatusCode: http.StatusOK} epAddr := entryPoint.listener.Addr().String()
err = resp.Write(conn)
request, err := http.NewRequest(http.MethodHead, "http://127.0.0.1:8082", nil)
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 {