diff --git a/CHANGELOG.md b/CHANGELOG.md index d197bdafb..cce3ec6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +## [v2.1.2](https://github.com/containous/traefik/tree/v2.1.2) (2020-01-07) +[All Commits](https://github.com/containous/traefik/compare/v2.1.1...v2.1.2) + +**Bug fixes:** +- **[authentication,middleware,tracing]** fix(tracing): makes sure tracing headers are being propagated when using forwardAuth ([#6072](https://github.com/containous/traefik/pull/6072) by [jcchavezs](https://github.com/jcchavezs)) +- **[cli]** fix: invalid label/flag parsing. ([#6028](https://github.com/containous/traefik/pull/6028) by [ldez](https://github.com/ldez)) +- **[consulcatalog]** Query consul catalog for service health separately ([#6046](https://github.com/containous/traefik/pull/6046) by [SantoDE](https://github.com/SantoDE)) +- **[k8s,k8s/crd]** Restore ExternalName https support for Kubernetes CRD ([#6037](https://github.com/containous/traefik/pull/6037) by [kpeiruza](https://github.com/kpeiruza)) +- **[k8s,k8s/crd]** Log the ignored namespace only when needed ([#6087](https://github.com/containous/traefik/pull/6087) by [jbdoumenjou](https://github.com/jbdoumenjou)) +- **[k8s,k8s/ingress]** k8s Ingress: fix crash on rules with nil http ([#6121](https://github.com/containous/traefik/pull/6121) by [grimmy](https://github.com/grimmy)) +- **[logs]** Improves error message when a configuration file is empty. ([#6135](https://github.com/containous/traefik/pull/6135) by [ldez](https://github.com/ldez)) +- **[server]** Handle respondingTimeout and better shutdown tests. ([#6115](https://github.com/containous/traefik/pull/6115) by [juliens](https://github.com/juliens)) +- **[server]** Don't set user-agent to Go-http-client/1.1 ([#6030](https://github.com/containous/traefik/pull/6030) by [sh7dm](https://github.com/sh7dm)) +- **[tracing]** fix: Malformed x-b3-traceid Header ([#6079](https://github.com/containous/traefik/pull/6079) by [ldez](https://github.com/ldez)) +- **[webui]** fix: dashboard redirect loop ([#6078](https://github.com/containous/traefik/pull/6078) by [ldez](https://github.com/ldez)) + +**Documentation:** +- **[acme]** Use consistent name in ACME documentation ([#6019](https://github.com/containous/traefik/pull/6019) by [ldez](https://github.com/ldez)) +- **[api,k8s/crd]** Add a documentation example for dashboard and api for kubernetes CRD ([#6022](https://github.com/containous/traefik/pull/6022) by [dduportal](https://github.com/dduportal)) +- **[cli]** Fix examples for the use of websecure via CLI ([#6116](https://github.com/containous/traefik/pull/6116) by [tiagoboeing](https://github.com/tiagoboeing)) +- **[k8s,k8s/crd]** Improve documentation about Kubernetes IngressRoute ([#6058](https://github.com/containous/traefik/pull/6058) by [jbdoumenjou](https://github.com/jbdoumenjou)) +- **[middleware]** Improve sourceRange explanation for ipWhiteList ([#6070](https://github.com/containous/traefik/pull/6070) by [der-domi](https://github.com/der-domi)) + +## [v2.1.1](https://github.com/containous/traefik/tree/v2.1.1) (2019-12-12) +[All Commits](https://github.com/containous/traefik/compare/v2.1.0...v2.1.1) + +**Bug fixes:** +- **[logs,middleware,metrics]** CloseNotifier: return pointer instead of value ([#6010](https://github.com/containous/traefik/pull/6010) by [mpl](https://github.com/mpl)) + +**Documentation:** +- Add Migration Guide for Traefik v2.1 ([#6017](https://github.com/containous/traefik/pull/6017) by [SantoDE](https://github.com/SantoDE)) + ## [v2.1.0](https://github.com/containous/traefik/tree/v2.1.0) (2019-12-10) [All Commits](https://github.com/containous/traefik/compare/v2.0.0-rc1...v2.1.0) diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index b773a0917..d457f1232 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -172,7 +172,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err acmeProviders := initACMEProvider(staticConfiguration, &providerAggregator, tlsManager) - serverEntryPointsTCP, err := server.NewTCPEntryPoints(*staticConfiguration) + serverEntryPointsTCP, err := server.NewTCPEntryPoints(staticConfiguration.EntryPoints) if err != nil { return nil, err } diff --git a/docs/content/contributing/building-testing.md b/docs/content/contributing/building-testing.md index c69fcdec8..8433e7d11 100644 --- a/docs/content/contributing/building-testing.md +++ b/docs/content/contributing/building-testing.md @@ -64,7 +64,7 @@ Requirements: - `go` v1.13+ - 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" @@ -100,30 +100,32 @@ Requirements: #### 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 cd ~/go/src/github.com/containous/traefik # Get go-bindata. (Important: the ellipses are required.) 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, such as the web dashboard and the provider's templates) +# required to merge non-code components into the final binary, +# such as the web dashboard/UI go generate +``` +```bash # Standard go build go build ./cmd/traefik ``` 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 ### Method 1: `Docker` and `make` diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index ef476df82..bb696d47a 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -59,10 +59,10 @@ Please check the [configuration examples below](#configuration-examples) for mor [entryPoints.web-secure] address = ":443" - [certificatesResolvers.sample.acme] + [certificatesResolvers.le.acme] email = "your-email@your-domain.org" storage = "acme.json" - [certificatesResolvers.sample.acme.httpChallenge] + [certificatesResolvers.le.acme.httpChallenge] # used during the challenge entryPoint = "web" ``` @@ -89,10 +89,10 @@ Please check the [configuration examples below](#configuration-examples) for mor --entryPoints.web.address=:80 --entryPoints.websecure.address=:443 # ... - --certificatesResolvers.sample.acme.email=your-email@your-domain.org - --certificatesResolvers.sample.acme.storage=acme.json + --certificatesResolvers.le.acme.email=your-email@your-domain.org + --certificatesResolvers.le.acme.storage=acme.json # 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." @@ -164,9 +164,9 @@ when using the `TLS-ALPN-01` challenge, Traefik must be reachable by Let's Encry ??? example "Configuring the `tlsChallenge`" ```toml tab="File (TOML)" - [certificatesResolvers.sample.acme] + [certificatesResolvers.le.acme] # ... - [certificatesResolvers.sample.acme.tlsChallenge] + [certificatesResolvers.le.acme.tlsChallenge] ``` ```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" # ... - --certificatesResolvers.sample.acme.tlsChallenge=true + --certificatesResolvers.le.acme.tlsChallenge=true ``` ### `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. 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`" @@ -199,9 +199,9 @@ when using the `HTTP-01` challenge, `certificatesResolvers.sample.acme.httpChall [entryPoints.web-secure] address = ":443" - [certificatesResolvers.sample.acme] + [certificatesResolvers.le.acme] # ... - [certificatesResolvers.sample.acme.httpChallenge] + [certificatesResolvers.le.acme.httpChallenge] entryPoint = "web" ``` @@ -225,7 +225,7 @@ when using the `HTTP-01` challenge, `certificatesResolvers.sample.acme.httpChall --entryPoints.web.address=:80 --entryPoints.websecure.address=:443 # ... - --certificatesResolvers.sample.acme.httpChallenge.entryPoint=web + --certificatesResolvers.le.acme.httpChallenge.entryPoint=web ``` !!! 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" ```toml tab="File (TOML)" - [certificatesResolvers.sample.acme] + [certificatesResolvers.le.acme] # ... - [certificatesResolvers.sample.acme.dnsChallenge] + [certificatesResolvers.le.acme.dnsChallenge] provider = "digitalocean" delayBeforeCheck = 0 # ... @@ -259,8 +259,8 @@ Use the `DNS-01` challenge to generate and renew ACME certificates by provisioni ```bash tab="CLI" # ... - --certificatesResolvers.sample.acme.dnsChallenge.provider=digitalocean - --certificatesResolvers.sample.acme.dnsChallenge.delayBeforeCheck=0 + --certificatesResolvers.le.acme.dnsChallenge.provider=digitalocean + --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. ```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"] ``` @@ -378,7 +378,7 @@ certificatesResolvers: ```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 @@ -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" ```toml tab="File (TOML)" - [certificatesResolvers.sample.acme] + [certificatesResolvers.le.acme] # ... 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" # ... - --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. ```toml tab="File (TOML)" -[certificatesResolvers.sample.acme] +[certificatesResolvers.le.acme] # ... storage = "acme.json" # ... @@ -436,7 +436,7 @@ certificatesResolvers: ```bash tab="CLI" # ... ---certificatesResolvers.sample.acme.storage=acme.json +--certificatesResolvers.le.acme.storage=acme.json # ... ``` diff --git a/docs/content/https/include-acme-multiple-domains-from-rule-example.md b/docs/content/https/include-acme-multiple-domains-from-rule-example.md index f82cb8e0f..b33f368fa 100644 --- a/docs/content/https/include-acme-multiple-domains-from-rule-example.md +++ b/docs/content/https/include-acme-multiple-domains-from-rule-example.md @@ -12,9 +12,9 @@ labels: deploy: labels: - 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.certresolver=le + - traefik.http.services.blog-svc.loadbalancer.server.port=8080" ``` ```yaml tab="Kubernetes" diff --git a/docs/content/https/include-acme-single-domain-example.md b/docs/content/https/include-acme-single-domain-example.md index f8e087b31..b153222ff 100644 --- a/docs/content/https/include-acme-single-domain-example.md +++ b/docs/content/https/include-acme-single-domain-example.md @@ -12,9 +12,9 @@ labels: deploy: labels: - 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.certresolver=le + - traefik.http.services.blog-svc.loadbalancer.server.port=8080" ``` ```yaml tab="Kubernetes" diff --git a/docs/content/https/ref-acme.toml b/docs/content/https/ref-acme.toml index 7567470f9..5b509fee6 100644 --- a/docs/content/https/ref-acme.toml +++ b/docs/content/https/ref-acme.toml @@ -35,13 +35,13 @@ # # Optional (but recommended) # - [certificatesResolvers.sample.acme.tlsChallenge] + [certificatesResolvers.le.acme.tlsChallenge] # Use a HTTP-01 ACME challenge. # # Optional # - # [certificatesResolvers.sample.acme.httpChallenge] + # [certificatesResolvers.le.acme.httpChallenge] # EntryPoint to use for the HTTP-01 challenges. # @@ -54,7 +54,7 @@ # # Optional # - # [certificatesResolvers.sample.acme.dnsChallenge] + # [certificatesResolvers.le.acme.dnsChallenge] # DNS provider used. # diff --git a/docs/content/https/ref-acme.txt b/docs/content/https/ref-acme.txt index 89431729b..be321d336 100644 --- a/docs/content/https/ref-acme.txt +++ b/docs/content/https/ref-acme.txt @@ -4,13 +4,13 @@ # # Required # ---certificatesResolvers.sample.acme.email=test@traefik.io +--certificatesResolvers.le.acme.email=test@traefik.io # File or key used for certificates storage. # # Required # ---certificatesResolvers.sample.acme.storage=acme.json +--certificatesResolvers.le.acme.storage=acme.json # CA server to use. # Uncomment the line to use Let's Encrypt's staging server, @@ -19,7 +19,7 @@ # Optional # 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. # @@ -28,38 +28,38 @@ # # Available values : "EC256", "EC384", "RSA2048", "RSA4096", "RSA8192" # ---certificatesResolvers.sample.acme.keyType=RSA4096 +--certificatesResolvers.le.acme.keyType=RSA4096 # Use a TLS-ALPN-01 ACME challenge. # # Optional (but recommended) # ---certificatesResolvers.sample.acme.tlsChallenge=true +--certificatesResolvers.le.acme.tlsChallenge=true # Use a HTTP-01 ACME challenge. # # Optional # ---certificatesResolvers.sample.acme.httpChallenge=true +--certificatesResolvers.le.acme.httpChallenge=true # EntryPoint to use for the HTTP-01 challenges. # # Required # ---certificatesResolvers.sample.acme.httpChallenge.entryPoint=web +--certificatesResolvers.le.acme.httpChallenge.entryPoint=web # Use a DNS-01 ACME challenge rather than HTTP-01 challenge. # Note: mandatory for wildcard certificate generation. # # Optional # ---certificatesResolvers.sample.acme.dnsChallenge=true +--certificatesResolvers.le.acme.dnsChallenge=true # DNS provider used. # # 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. # If delayBeforeCheck is greater than zero, this check is delayed for the configured duration in seconds. @@ -68,14 +68,14 @@ # Optional # Default: 0 # ---certificatesResolvers.sample.acme.dnsChallenge.delayBeforeCheck=0 +--certificatesResolvers.le.acme.dnsChallenge.delayBeforeCheck=0 # Use following DNS servers to resolve the FQDN authority. # # Optional # 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. # @@ -85,4 +85,4 @@ # Optional # Default: false # ---certificatesResolvers.sample.acme.dnsChallenge.disablePropagationCheck=true +--certificatesResolvers.le.acme.dnsChallenge.disablePropagationCheck=true diff --git a/docs/content/https/ref-acme.yaml b/docs/content/https/ref-acme.yaml index b827e6f06..1dc34ece4 100644 --- a/docs/content/https/ref-acme.yaml +++ b/docs/content/https/ref-acme.yaml @@ -1,5 +1,5 @@ certificatesResolvers: - sample: + le: # Enable ACME (Let's Encrypt): automatic SSL. acme: diff --git a/docs/content/https/tls.md b/docs/content/https/tls.md index d291800da..b06073349 100644 --- a/docs/content/https/tls.md +++ b/docs/content/https/tls.md @@ -40,7 +40,7 @@ tls: 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). - 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 diff --git a/docs/content/middlewares/ipwhitelist.md b/docs/content/middlewares/ipwhitelist.md index 79706d59d..cf7b64b2f 100644 --- a/docs/content/middlewares/ipwhitelist.md +++ b/docs/content/middlewares/ipwhitelist.md @@ -66,7 +66,7 @@ http: ### `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` diff --git a/docs/content/migration/v1-to-v2.md b/docs/content/migration/v1-to-v2.md index ff529efef..8747e99fc 100644 --- a/docs/content/migration/v1-to-v2.md +++ b/docs/content/migration/v1-to-v2.md @@ -104,7 +104,7 @@ Then any router can refer to an instance of the wanted middleware. ```yaml tab="K8s IngressRoute" # 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 kind: Middleware 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" # 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 kind: TLSOption metadata: diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md new file mode 100644 index 000000000..634b562e1 --- /dev/null +++ b/docs/content/migration/v2.md @@ -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. diff --git a/docs/content/operations/include-api-examples.md b/docs/content/operations/include-api-examples.md index b16da30f6..18fa46b65 100644 --- a/docs/content/operations/include-api-examples.md +++ b/docs/content/operations/include-api-examples.md @@ -19,6 +19,28 @@ deploy: - "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" # Dynamic Configuration - "traefik.http.routers.api.rule=Host(`traefik.domain.com`)" diff --git a/docs/content/providers/docker.md b/docs/content/providers/docker.md index 0fbbf17d2..d76635ea2 100644 --- a/docs/content/providers/docker.md +++ b/docs/content/providers/docker.md @@ -86,7 +86,7 @@ and [Docker Swarm Mode](https://docs.docker.com/engine/swarm/). ## Routing Configuration 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. diff --git a/docs/content/providers/kubernetes-crd.md b/docs/content/providers/kubernetes-crd.md index 8305b2705..e0130899a 100644 --- a/docs/content/providers/kubernetes-crd.md +++ b/docs/content/providers/kubernetes-crd.md @@ -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, 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 -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 diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition.yml new file mode 100644 index 000000000..f696ac4cf --- /dev/null +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition.yml @@ -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 diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-ingressroutetcp-definition.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-ingressroutetcp-definition.yml new file mode 100644 index 000000000..36b202ae3 --- /dev/null +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-ingressroutetcp-definition.yml @@ -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 diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml new file mode 100644 index 000000000..9b56464d7 --- /dev/null +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml @@ -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 diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml new file mode 100644 index 000000000..5f6054d13 --- /dev/null +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml @@ -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 diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd.md b/docs/content/reference/dynamic-configuration/kubernetes-crd.md index c130e84e1..477b082f9 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd.md +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd.md @@ -3,6 +3,20 @@ Dynamic configuration with Kubernetes Custom Resource {: .subtitle } +## Definitions + ```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" ``` diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 12283dcd7..41e824f96 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -41,7 +41,7 @@ They define the port which will receive the requests (whether HTTP or TCP). [entryPoints.web] address = ":80" - [entryPoints.web-secure] + [entryPoints.websecure] address = ":443" ``` @@ -51,18 +51,18 @@ They define the port which will receive the requests (whether HTTP or TCP). web: address: ":80" - web-secure: + websecure: address: ":443" ``` ```bash tab="CLI" ## Static configuration --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`. - - `web` listens on port `80`, and `web-secure` on port `443`. + - Two entrypoints are defined: one called `web`, and the other called `websecure`. + - `web` listens on port `80`, and `websecure` on port `443`. ## Configuration diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index fa4eafde4..926c4bb67 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -3,239 +3,884 @@ The Kubernetes Ingress Controller, The Custom Resource Way. {: .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 + --8<-- "content/reference/dynamic-configuration/kubernetes-crd-definition.yml" + ``` + + ```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 ---8<-- "content/routing/providers/crd_ingress_route.yml" -``` + ```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 + ``` -That `IngressRoute` kind can then be used to define an `IngressRoute` object, such as in: +## Routing Configuration -```yaml -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: ingressroutefoo +### Custom Resource Definition (CRD) -spec: - entryPoints: - - web - routes: - # Match is the rule corresponding to an underlying router. - # Later on, match could be the simple form of a path prefix, e.g. just "/bar", - # but for now we only support a traefik style matching rule. - - match: Host(`foo.com`) && PathPrefix(`/bar`) - # kind could eventually be one of "Rule", "Path", "Host", "Method", "Header", - # "Parameter", etc, to support simpler forms of rule matching, but for now we - # only support "Rule". - kind: Rule - # (optional) Priority disambiguates rules of the same length, for route matching. - priority: 12 - services: - - name: whoami - port: 80 - # (default 1) A weight used by the weighted round-robin strategy (WRR). - 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 - responseForwarding: - # (default 100ms) Interval between flushes of the buffered response body to the client. - flushInterval: 100ms +* 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: ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRouteTCP -metadata: - name: ingressroutetcpfoo.crd +| 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) | -spec: - entryPoints: - - footcp - routes: - # Match is the rule corresponding to an underlying router. - - match: HostSNI(`*`) - services: - - name: whoamitcp - port: 8080 -``` +### Kind: `IngressRoute` -### Middleware +`IngressRoute` is the CRD implementation of a [Traefik HTTP router](../routers/index.md#configuring-http-routers). -Additionally, to allow for the use of middlewares in an `IngressRoute`, we defined the CRD below for the `Middleware` kind. +Register the `IngressRoute` kind in the Kubernetes cluster before creating `IngressRoute` objects. -```yaml ---8<-- "content/routing/providers/crd_middlewares.yml" -``` +!!! info "IngressRoute Attributes" -Once the `Middleware` kind has been registered with the Kubernetes cluster, it can then be used in `IngressRoute` definitions, such as: + ```yaml + apiVersion: traefik.containo.us/v1alpha1 + kind: IngressRoute + metadata: + 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 + ``` -```yaml -apiVersion: traefik.containo.us/v1alpha1 -kind: Middleware -metadata: - name: stripprefix - namespace: foo +| 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) | -spec: - stripPrefix: - prefixes: - - /stripit +??? example "Declaring an IngressRoute" ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: ingressroutebar + ```yaml tab="IngressRoute" + # All resources definition must be declared + apiVersion: traefik.containo.us/v1alpha1 + kind: IngressRoute + metadata: + name: testName + namespace: default + spec: + entryPoints: + - web + routes: + - kind: Rule + match: Host(`test.domain.com`) + middlewares: + - name: middleware1 + namespace: default + priority: 10 + services: + - 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: + certResolver: foo + domains: + - main: foo.com + sans: + - a.foo.com + - b.foo.com + options: + name: opt + namespace: default + secretName: supersecret + ``` -spec: - entryPoints: - - web - routes: - - match: Host(`bar.com`) && PathPrefix(`/stripit`) - kind: Rule - services: - - name: whoami - port: 80 - middlewares: - - name: stripprefix + ```yaml tab="Middlewares" + # All resources definition must be declared + # Prefixing with /foo + apiVersion: traefik.containo.us/v1alpha1 + kind: Middleware + metadata: + name: middleware1 + namespace: default + spec: + addPrefix: + prefix: /foo + ``` + + ```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: `Middleware` + +`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 + kind: Middleware + metadata: + name: stripprefix namespace: foo -``` + + spec: + stripPrefix: + prefixes: + - /stripit + ``` + + ```yaml tab="IngressRoute" + apiVersion: traefik.containo.us/v1alpha1 + kind: IngressRoute + metadata: + name: ingressroutebar + + spec: + entryPoints: + - web + routes: + - match: Host(`bar.com`) && PathPrefix(`/stripit`) + kind: Rule + services: + - name: whoami + port: 80 + middlewares: + - name: stripprefix + namespace: foo + ``` !!! important "Cross-provider namespace" 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), - 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. 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), -one can define and use additional service objects specific to Traefik, based on the `TraefikService` kind defined in the CRD below. +`TraefikService` is the CRD implementation of a ["Traefik Service"](../services/index.md). -```yaml ---8<-- "content/routing/providers/crd_traefikservice.yml" -``` +Register the `TraefikService` kind in the Kubernetes cluster before creating `TraefikService` objects, +referencing services in the [`IngressRoute`](#kind-ingressroute)/[`IngressRouteTCP`](#kind-ingressroutetcp) objects or recursively in others `TraefikService` objects. -Once the `TraefikService` kind has been registered with the Kubernetes cluster, it can then be used in `IngressRoute` definitions -(as well as recursively in other Traefik Services), such as below. -Note how the `name` field in the IngressRoute definition now refers to a TraefikService instead of a (Kubernetes) Service. -The reason this is allowed, and why a `name` can refer either to a TraefikService or a Service, -is because the `kind` field is used to break the ambiguity. The allowed values for this field are `TraefikService`, or `Service` -(which is the default value). +!!! info "Disambiguate Traefik and Kubernetes Services " -```yaml -apiVersion: traefik.containo.us/v1alpha1 -kind: TraefikService -metadata: - name: wrr1 - namespace: default + 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) -spec: - weighted: - services: - - name: s2 - kind: Service - port: 80 - weight: 1 - - name: s3 - weight: 1 - port: 80 +`TraefikService` object allows to use any (valid) combinations of: ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: TraefikService -metadata: - name: mirror1 - namespace: default - -spec: - mirroring: - name: wrr1 - kind: TraefikService - mirrors: - - name: s1 - percent: 20 - port: 80 - ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: ingressroutebar - namespace: default +* servers [load balancing](#server-load-balancing). +* services [Weighted Round Robin](#weighted-round-robin) load balancing. +* services [mirroring](#mirroring). -spec: - entryPoints: - - web - routes: - - match: Host(`bar.com`) && PathPrefix(`/foo`) - kind: Rule - services: - - name: mirror1 +#### 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 - kind: TraefikService -``` + + spec: + entryPoints: + - web + routes: + - match: Host(`bar.com`) && PathPrefix(`/foo`) + kind: Rule + services: + - name: svc1 + namespace: default + - name: svc2 + namespace: default + ``` + + ```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 + ``` + +#### 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 + kind: TraefikService + metadata: + name: wrr1 + namespace: default + + spec: + weighted: + services: + - name: svc1 + port: 80 + weight: 1 + - name: wrr2 + kind: TraefikService + weight: 1 + - name: mirror1 + kind: TraefikService + weight: 1 + + --- + apiVersion: traefik.containo.us/v1alpha1 + kind: TraefikService + metadata: + name: wrr2 + namespace: default + + spec: + weighted: + services: + - name: svc2 + 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 + kind: IngressRoute + metadata: + name: ingressroutebar + namespace: default + + spec: + entryPoints: + - web + routes: + - match: Host(`bar.com`) && PathPrefix(`/foo`) + kind: Rule + services: + - name: mirror1 + namespace: default + 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" 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, - 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`). -### 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. -More information about TLS Options is available in the dedicated [TLS Configuration Options](../../../https/tls/#tls-options). +`IngressRouteTCP` is the CRD implementation of a [Traefik TCP router](../routers/index.md#configuring-tcp-routers). -```yaml ---8<-- "content/routing/providers/crd_tls_option.yml" -``` +Register the `IngressRouteTCP` kind in the Kubernetes cluster before creating `IngressRouteTCP` objects. -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: +!!! info "IngressRouteTCP Attributes" -```yaml -apiVersion: traefik.containo.us/v1alpha1 -kind: TLSOption -metadata: - name: mytlsoption - namespace: default + ```yaml + 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] + ``` -spec: - minVersion: VersionTLS12 +| 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.
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 | ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: ingressroutebar +??? example "Declaring an IngressRouteTCP" -spec: - entryPoints: - - web - routes: - - match: Host(`bar.com`) && PathPrefix(`/stripit`) - kind: Rule - services: - - name: whoami - port: 80 - tls: - options: + ```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 + kind: TLSOption + metadata: + name: mytlsoption + namespace: default + + spec: + 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 + kind: IngressRoute + metadata: + name: ingressroutebar + + spec: + entryPoints: + - web + routes: + - match: Host(`bar.com`) && PathPrefix(`/stripit`) + kind: Rule + services: + - name: whoami + port: 80 + tls: + options: + name: mytlsoption + 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" 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). 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 Also see the [full example](../../user-guides/crd-acme/index.md) with Let's Encrypt. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index f4aade025..cebbc5418 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -160,6 +160,7 @@ nav: - 'HTTP Challenge': 'user-guides/docker-compose/acme-http/index.md' - 'DNS Challenge': 'user-guides/docker-compose/acme-dns/index.md' - 'Migration': + - 'Traefik v2 minor migrations': 'migration/v2.md' - 'Traefik v1 to v2': 'migration/v1-to-v2.md' - 'Contributing': - 'Thank You!': 'contributing/thank-you.md' diff --git a/go.mod b/go.mod index d632e68aa..ce9214ce9 100644 --- a/go.mod +++ b/go.mod @@ -81,7 +81,7 @@ require ( github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154 github.com/tinylib/msgp v1.0.2 // 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/unrolled/render v1.0.1 github.com/unrolled/secure v1.0.6 diff --git a/go.sum b/go.sum index 46e8088d3..b13aaaf77 100644 --- a/go.sum +++ b/go.sum @@ -599,8 +599,8 @@ github.com/transip/gotransip v5.8.2+incompatible/go.mod h1:uacMoJVmrfOcscM4Bi5NV github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo= 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.20.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-client-go v2.21.1+incompatible h1:oozboeZmWz+tyh3VZttJWlF3K73mHgbokieceqKccLo= +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/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/unrolled/render v1.0.1 h1:VDDnQQVfBMsOsp3VaCJszSO0nkBIVEYoPWeRThk9spY= diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index be1579e8a..6e100b033 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http" "os" - "strconv" "time" "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 { - iPort, err := strconv.Atoi(port) - if err != nil { - return err - } +func (s *ConsulCatalogSuite) registerService(reg *api.AgentServiceRegistration, onAgent bool) error { client := s.consulClient if onAgent { client = s.consulAgentClient } - return client.Agent().ServiceRegister(&api.AgentServiceRegistration{ - ID: id, - Name: name, - Address: address, - Port: iPort, - Tags: tags, - }) + return client.Agent().ServiceRegister(reg) } 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) { - 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) - 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) - 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) tempObjects := struct { @@ -128,14 +140,21 @@ func (s *ConsulCatalogSuite) TestWithNotExposedByDefaultAndDefaultsSettings(c *c } func (s *ConsulCatalogSuite) TestByLabels(c *check.C) { - labels := []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, - } + containerIP := s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress - err := s.registerService("whoami1", "whoami", s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "80", labels, false) + reg := &api.AgentServiceRegistration{ + ID: "whoami1", + Name: "whoami", + Tags: []string{ + "traefik.enable=true", + "traefik.http.routers.router1.rule=Path(`/whoami`)", + "traefik.http.routers.router1.service=service1", + "traefik.http.services.service1.loadBalancer.server.url=http://" + containerIP, + }, + Port: 80, + Address: containerIP, + } + err := s.registerService(reg, false) c.Assert(err, checker.IsNil) tempObjects := struct { @@ -172,7 +191,14 @@ func (s *ConsulCatalogSuite) TestSimpleConfiguration(c *check.C) { file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects) 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) 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) 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) 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) 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) // Start traefik @@ -269,14 +308,20 @@ func (s *ConsulCatalogSuite) TestConsulServiceWithTCPLabels(c *check.C) { file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects) defer os.Remove(file) - // Start a container with some labels - labels := []string{ - "traefik.tcp.Routers.Super.Rule=HostSNI(`my.super.host`)", - "traefik.tcp.Routers.Super.tls=true", - "traefik.tcp.Services.Super.Loadbalancer.server.port=8080", + // Start a container with some tags + reg := &api.AgentServiceRegistration{ + ID: "whoamitcp", + Name: "whoamitcp", + Tags: []string{ + "traefik.tcp.Routers.Super.Rule=HostSNI(`my.super.host`)", + "traefik.tcp.Routers.Super.tls=true", + "traefik.tcp.Services.Super.Loadbalancer.server.port=8080", + }, + Port: 8080, + Address: s.composeProject.Container(c, "whoamitcp").NetworkSettings.IPAddress, } - err := s.registerService("whoamitcp", "whoamitcp", s.composeProject.Container(c, "whoamitcp").NetworkSettings.IPAddress, "8080", labels, false) + err := s.registerService(reg, false) c.Assert(err, checker.IsNil) // Start traefik @@ -310,18 +355,31 @@ func (s *ConsulCatalogSuite) TestConsulServiceWithLabels(c *check.C) { file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects) defer os.Remove(file) - // Start a container with some labels - labels := []string{ - "traefik.http.Routers.Super.Rule=Host(`my.super.host`)", + // Start a container with some tags + reg1 := &api.AgentServiceRegistration{ + ID: "whoami1", + Name: "whoami", + Tags: []string{ + "traefik.http.Routers.Super.Rule=Host(`my.super.host`)", + }, + Port: 80, + Address: s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, } - err := s.registerService("whoami1", "whoami", s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "80", labels, false) + + err := s.registerService(reg1, false) c.Assert(err, checker.IsNil) // Start another container by replacing a '.' by a '-' - labels = []string{ - "traefik.http.Routers.SuperHost.Rule=Host(`my-super.host`)", + reg2 := &api.AgentServiceRegistration{ + ID: "whoami2", + Name: "whoami", + Tags: []string{ + "traefik.http.Routers.SuperHost.Rule=Host(`my-super.host`)", + }, + Port: 80, + Address: s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress, } - err = s.registerService("whoami2", "whoami", s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress, "80", labels, false) + err = s.registerService(reg2, false) c.Assert(err, checker.IsNil) // 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) defer os.Remove(file) - // Start a container with some labels - labels := []string{ + // Start a container with some tags + tags := []string{ "traefik.enable=true", "traefik.http.Routers.Super.service=whoami", "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) - 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) // Start traefik @@ -417,11 +490,18 @@ func (s *ConsulCatalogSuite) TestConsulServiceWithOneMissingLabels(c *check.C) { file := s.adaptFile(c, "fixtures/consul_catalog/simple.toml", tempObjects) defer os.Remove(file) - // Start a container with some labels - labels := []string{ - "traefik.random.value=my.super.host", + // Start a container with some tags + reg := &api.AgentServiceRegistration{ + ID: "whoami1", + Name: "whoami", + Tags: []string{ + "traefik.random.value=my.super.host", + }, + Port: 80, + Address: s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, } - err := s.registerService("whoami1", "whoami", s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress, "80", labels, false) + + err := s.registerService(reg, false) c.Assert(err, checker.IsNil) // Start traefik @@ -441,3 +521,82 @@ func (s *ConsulCatalogSuite) TestConsulServiceWithOneMissingLabels(c *check.C) { err = try.Request(req, 1500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound)) 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) +} diff --git a/integration/testdata/rawdata-consul.json b/integration/testdata/rawdata-consul.json index 2f42deca9..675226673 100644 --- a/integration/testdata/rawdata-consul.json +++ b/integration/testdata/rawdata-consul.json @@ -86,7 +86,7 @@ }, "dashboard_redirect@internal": { "redirectRegex": { - "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", + "regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$", "replacement": "${1}/dashboard/", "permanent": true }, diff --git a/integration/testdata/rawdata-etcd.json b/integration/testdata/rawdata-etcd.json index dac6c6ae4..bee6543af 100644 --- a/integration/testdata/rawdata-etcd.json +++ b/integration/testdata/rawdata-etcd.json @@ -86,7 +86,7 @@ }, "dashboard_redirect@internal": { "redirectRegex": { - "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", + "regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$", "replacement": "${1}/dashboard/", "permanent": true }, diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index b70c3b552..45518f455 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -60,7 +60,7 @@ "middlewares": { "dashboard_redirect@internal": { "redirectRegex": { - "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", + "regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$", "replacement": "${1}/dashboard/", "permanent": true }, diff --git a/integration/testdata/rawdata-redis.json b/integration/testdata/rawdata-redis.json index bb4eb10a0..7f42b22b7 100644 --- a/integration/testdata/rawdata-redis.json +++ b/integration/testdata/rawdata-redis.json @@ -86,7 +86,7 @@ }, "dashboard_redirect@internal": { "redirectRegex": { - "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", + "regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$", "replacement": "${1}/dashboard/", "permanent": true }, diff --git a/integration/testdata/rawdata-zk.json b/integration/testdata/rawdata-zk.json index 616b8e7c4..a8f0d9a76 100644 --- a/integration/testdata/rawdata-zk.json +++ b/integration/testdata/rawdata-zk.json @@ -86,7 +86,7 @@ }, "dashboard_redirect@internal": { "redirectRegex": { - "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", + "regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$", "replacement": "${1}/dashboard/", "permanent": true }, diff --git a/pkg/config/file/file_node.go b/pkg/config/file/file_node.go index a18a8e8c4..3570c64d4 100644 --- a/pkg/config/file/file_node.go +++ b/pkg/config/file/file_node.go @@ -13,8 +13,7 @@ import ( ) // 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 -// not among filters. +// If filters is not empty, it skips any configuration element whose name is not among filters. func decodeFileToNode(filePath string, filters ...string) (*parser.Node, error) { content, err := ioutil.ReadFile(filePath) 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 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 { diff --git a/pkg/config/file/file_node_test.go b/pkg/config/file/file_node_test.go index 4b5b4e975..e301047a1 100644 --- a/pkg/config/file/file_node_test.go +++ b/pkg/config/file/file_node_test.go @@ -5,6 +5,7 @@ import ( "github.com/containous/traefik/v2/pkg/config/parser" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) 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) { nodeToml, err := decodeFileToNode("./fixtures/sample.toml", "Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) nodeYaml, err := decodeFileToNode("./fixtures/sample.yml") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) assert.Equal(t, nodeToml, nodeYaml) } @@ -60,9 +87,7 @@ func Test_decodeFileToNode_compare(t *testing.T) { func Test_decodeFileToNode_Toml(t *testing.T) { node, err := decodeFileToNode("./fixtures/sample.toml", "Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) expected := &parser.Node{ Name: "traefik", @@ -294,9 +319,7 @@ func Test_decodeFileToNode_Toml(t *testing.T) { func Test_decodeFileToNode_Yaml(t *testing.T) { node, err := decodeFileToNode("./fixtures/sample.yml") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) expected := &parser.Node{ Name: "traefik", diff --git a/pkg/config/file/fixtures/empty.toml b/pkg/config/file/fixtures/empty.toml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/config/file/fixtures/no_conf.toml b/pkg/config/file/fixtures/no_conf.toml new file mode 100644 index 000000000..fd45fbc06 --- /dev/null +++ b/pkg/config/file/fixtures/no_conf.toml @@ -0,0 +1,2 @@ +[foo] + bar = "test" diff --git a/pkg/config/parser/labels_decode.go b/pkg/config/parser/labels_decode.go index 733662849..4ecc5be46 100644 --- a/pkg/config/parser/labels_decode.go +++ b/pkg/config/parser/labels_decode.go @@ -21,6 +21,10 @@ func DecodeToNode(labels map[string]string, rootName string, filters ...string) var parts []string for _, v := range split { + if v == "" { + return nil, fmt.Errorf("invalid element: %s", key) + } + if v[0] == '[' { return nil, fmt.Errorf("invalid leading character '[' in field name (bracket is a slice delimiter): %s", v) } diff --git a/pkg/config/parser/labels_decode_test.go b/pkg/config/parser/labels_decode_test.go index cd854acf7..b742a615d 100644 --- a/pkg/config/parser/labels_decode_test.go +++ b/pkg/config/parser/labels_decode_test.go @@ -26,6 +26,15 @@ func TestDecodeToNode(t *testing.T) { in: map[string]string{}, 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", in: map[string]string{ diff --git a/pkg/middlewares/accesslog/capture_response_writer.go b/pkg/middlewares/accesslog/capture_response_writer.go index da4991a72..4202b0ee9 100644 --- a/pkg/middlewares/accesslog/capture_response_writer.go +++ b/pkg/middlewares/accesslog/capture_response_writer.go @@ -10,7 +10,7 @@ import ( ) var ( - _ middlewares.Stateful = &captureResponseWriter{} + _ middlewares.Stateful = &captureResponseWriterWithCloseNotify{} ) type capturer interface { @@ -24,7 +24,7 @@ func newCaptureResponseWriter(rw http.ResponseWriter) capturer { if _, ok := rw.(http.CloseNotifier); !ok { return capt } - return captureResponseWriterWithCloseNotify{capt} + return &captureResponseWriterWithCloseNotify{capt} } // 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) } -func (crw *captureResponseWriter) CloseNotify() <-chan bool { - if c, ok := crw.rw.(http.CloseNotifier); ok { - return c.CloseNotify() - } - return nil -} - func (crw *captureResponseWriter) Status() int { return crw.status } diff --git a/pkg/middlewares/accesslog/capture_response_writer_test.go b/pkg/middlewares/accesslog/capture_response_writer_test.go new file mode 100644 index 000000000..3606fc033 --- /dev/null +++ b/pkg/middlewares/accesslog/capture_response_writer_test.go @@ -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) + }) + } +} diff --git a/pkg/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go index f1b4421bf..4bd5bbaf6 100644 --- a/pkg/middlewares/auth/forward.go +++ b/pkg/middlewares/auth/forward.go @@ -88,9 +88,11 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 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) if forwardErr != nil { diff --git a/pkg/middlewares/auth/forward_test.go b/pkg/middlewares/auth/forward_test.go index 20dfa6608..707e4f4ed 100644 --- a/pkg/middlewares/auth/forward_test.go +++ b/pkg/middlewares/auth/forward_test.go @@ -3,13 +3,18 @@ package auth import ( "context" "fmt" + "io" "io/ioutil" "net/http" "net/http/httptest" "testing" "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/tracing" + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/mocktracer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "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 +} diff --git a/pkg/middlewares/metrics/metrics_test.go b/pkg/middlewares/metrics/metrics_test.go index 9df351649..596ac6abe 100644 --- a/pkg/middlewares/metrics/metrics_test.go +++ b/pkg/middlewares/metrics/metrics_test.go @@ -7,6 +7,7 @@ import ( "testing" "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. @@ -56,3 +57,44 @@ func newCollectingRetryMetrics() *collectingRetryMetrics { func (m *collectingRetryMetrics) ServiceRetriesCounter() metrics.Counter { 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) + }) + } +} diff --git a/pkg/middlewares/metrics/recorder.go b/pkg/middlewares/metrics/recorder.go index 4206558c7..b39a79954 100644 --- a/pkg/middlewares/metrics/recorder.go +++ b/pkg/middlewares/metrics/recorder.go @@ -20,7 +20,7 @@ func newResponseRecorder(rw http.ResponseWriter) recorder { if _, ok := rw.(http.CloseNotifier); !ok { return rec } - return responseRecorderWithCloseNotify{rec} + return &responseRecorderWithCloseNotify{rec} } // 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() } -// 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. func (r *responseRecorder) Flush() { if f, ok := r.ResponseWriter.(http.Flusher); ok { diff --git a/pkg/provider/consulcatalog/consul_catalog.go b/pkg/provider/consulcatalog/consul_catalog.go index 4c52eac16..079f3f9f2 100644 --- a/pkg/provider/consulcatalog/consul_catalog.go +++ b/pkg/provider/consulcatalog/consul_catalog.go @@ -152,12 +152,12 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error var data []itemData for name := range consulServiceNames { - consulServices, err := p.fetchService(ctx, name) + consulServices, healthServices, err := p.fetchService(ctx, name) if err != nil { return nil, err } - for _, consulService := range consulServices { + for i, consulService := range consulServices { address := consulService.ServiceAddress if address == "" { address = consulService.Address @@ -171,7 +171,7 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error Port: strconv.Itoa(consulService.ServicePort), Labels: tagsToNeutralLabels(consulService.ServiceTags, p.Prefix), Tags: consulService.ServiceTags, - Status: consulService.Checks.AggregatedStatus(), + Status: healthServices[i].Checks.AggregatedStatus(), } extraConf, err := p.getConfiguration(item) @@ -187,15 +187,21 @@ func (p *Provider) getConsulServicesData(ctx context.Context) ([]itemData, error 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 if !p.ExposedByDefault { tagFilter = p.Prefix + ".enable=true" } opts := &api.QueryOptions{AllowStale: p.Stale, RequireConsistent: p.RequireConsistent, UseCache: p.Cache} + 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) { diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 0adca3e75..6f6129850 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -307,8 +307,13 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa var servers []dynamic.Server if service.Spec.Type == corev1.ServiceTypeExternalName { + protocol := "http" + if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") { + protocol = "https" + } + 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 } @@ -375,11 +380,11 @@ func (c configBuilder) nameAndService(ctx context.Context, namespaceService stri return "", nil, err } - fullName := fullServiceName(svcCtx, namespace, service.Name, service.Port) + fullName := fullServiceName(svcCtx, namespace, service, service.Port) return fullName, serversLB, nil case service.Kind == "TraefikService": - return fullServiceName(svcCtx, namespace, service.Name, 0), nil, nil + return fullServiceName(svcCtx, namespace, service, 0), nil, nil default: return "", nil, fmt.Errorf("unsupported service kind %s", service.Kind) } @@ -394,27 +399,22 @@ func splitSvcNameProvider(name string) (string, string) { 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 { - 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) { - return provider.Normalize(fmt.Sprintf("%s-%s", namespace, serviceName)) + if !strings.Contains(service.Name, providerNamespaceSeparator) { + return provider.Normalize(fmt.Sprintf("%s-%s", namespace, service.Name)) } - name, pName := splitSvcNameProvider(serviceName) + name, pName := splitSvcNameProvider(service.Name) if pName == providerName { 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, - // or if we're simply hitting the value set by default. - // 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) + if service.Namespace != "" { + log.FromContext(ctx).Warnf("namespace %q is ignored in cross-provider context", service.Namespace) } return provider.Normalize(name) + providerNamespaceSeparator + pName diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-one-rule-host-only_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-one-rule-host-only_ingress.yml new file mode 100644 index 000000000..5c979351c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-one-rule-host-only_ingress.yml @@ -0,0 +1,9 @@ +kind: Ingress +apiVersion: extensions/v1beta1 +metadata: + name: "" + namespace: testing + +spec: + rules: + - host: testing.example.com diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index f81bf6369..84268cca0 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -313,53 +313,57 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl conf.HTTP.Services["default-backend"] = service } } + for _, rule := range ingress.Spec.Rules { if err := checkStringQuoteValidity(rule.Host); err != nil { log.FromContext(ctx).Errorf("Invalid syntax for host: %s", rule.Host) continue } - for _, p := range rule.HTTP.Paths { - service, err := loadService(client, ingress.Namespace, p.Backend) - if err != nil { - log.FromContext(ctx). - WithField("serviceName", p.Backend.ServiceName). - WithField("servicePort", p.Backend.ServicePort.String()). - Errorf("Cannot create service: %v", err) - continue - } + if rule.HTTP != nil { + for _, p := range rule.HTTP.Paths { + service, err := loadService(client, ingress.Namespace, p.Backend) + if err != nil { + log.FromContext(ctx). + WithField("serviceName", p.Backend.ServiceName). + WithField("servicePort", p.Backend.ServicePort.String()). + Errorf("Cannot create service: %v", err) + continue + } - if err = checkStringQuoteValidity(p.Path); err != nil { - log.FromContext(ctx).Errorf("Invalid syntax for path: %s", p.Path) - continue - } + if err = checkStringQuoteValidity(p.Path); err != nil { + log.FromContext(ctx).Errorf("Invalid syntax for path: %s", p.Path) + continue + } - serviceName := provider.Normalize(ingress.Namespace + "-" + p.Backend.ServiceName + "-" + p.Backend.ServicePort.String()) - var rules []string - if len(rule.Host) > 0 { - rules = []string{"Host(`" + rule.Host + "`)"} - } + serviceName := provider.Normalize(ingress.Namespace + "-" + p.Backend.ServiceName + "-" + p.Backend.ServicePort.String()) + var rules []string + if len(rule.Host) > 0 { + rules = []string{"Host(`" + rule.Host + "`)"} + } - if len(p.Path) > 0 { - rules = append(rules, "PathPrefix(`"+p.Path+"`)") - } + if len(p.Path) > 0 { + rules = append(rules, "PathPrefix(`"+p.Path+"`)") + } - routerKey := strings.TrimPrefix(provider.Normalize(rule.Host+p.Path), "-") - conf.HTTP.Routers[routerKey] = &dynamic.Router{ - Rule: strings.Join(rules, " && "), - Service: serviceName, - } - - if len(ingress.Spec.TLS) > 0 { - // TLS enabled for this ingress, add TLS router - conf.HTTP.Routers[routerKey+"-tls"] = &dynamic.Router{ + routerKey := strings.TrimPrefix(provider.Normalize(rule.Host+p.Path), "-") + conf.HTTP.Routers[routerKey] = &dynamic.Router{ Rule: strings.Join(rules, " && "), Service: serviceName, - TLS: &dynamic.RouterTLSConfig{}, } + + if len(ingress.Spec.TLS) > 0 { + // TLS enabled for this ingress, add TLS router + conf.HTTP.Routers[routerKey+"-tls"] = &dynamic.Router{ + Rule: strings.Join(rules, " && "), + Service: serviceName, + TLS: &dynamic.RouterTLSConfig{}, + } + } + conf.HTTP.Services[serviceName] = service } - conf.HTTP.Services[serviceName] = service } + err := p.updateIngressStatus(ingress, client) if err != nil { log.FromContext(ctx).Errorf("Error while updating ingress status: %v", err) diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index a75c4356d..66fca43db 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -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", expected: &dynamic.Configuration{ diff --git a/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json b/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json index 60f72a7f7..e4a1e2ee4 100644 --- a/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json +++ b/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json @@ -25,7 +25,7 @@ "middlewares": { "dashboard_redirect": { "redirectRegex": { - "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", + "regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$", "replacement": "${1}/dashboard/", "permanent": true } diff --git a/pkg/provider/traefik/fixtures/full_configuration.json b/pkg/provider/traefik/fixtures/full_configuration.json index e8c3a9ec6..f4852fcb5 100644 --- a/pkg/provider/traefik/fixtures/full_configuration.json +++ b/pkg/provider/traefik/fixtures/full_configuration.json @@ -57,7 +57,7 @@ "middlewares": { "dashboard_redirect": { "redirectRegex": { - "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", + "regex": "^(http:\\/\\/[^:\\/]+(:\\d+)?)\\/$", "replacement": "${1}/dashboard/", "permanent": true } diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index 1501b29a0..091d2b13f 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -86,7 +86,7 @@ func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) { cfg.HTTP.Middlewares["dashboard_redirect"] = &dynamic.Middleware{ RedirectRegex: &dynamic.RedirectRegex{ - Regex: `^(http:\/\/[^:]+(:\d+)?)/$`, + Regex: `^(http:\/\/[^:\/]+(:\d+)?)\/$`, Replacement: "${1}/dashboard/", Permanent: true, }, diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index e67b5b200..db4dcb5bd 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -52,9 +52,9 @@ func (h *httpForwarder) Accept() (net.Conn, error) { type TCPEntryPoints map[string]*TCPEntryPoint // NewTCPEntryPoints creates a new TCPEntryPoints. -func NewTCPEntryPoints(staticConfiguration static.Configuration) (TCPEntryPoints, error) { +func NewTCPEntryPoints(entryPointsConfig static.EntryPoints) (TCPEntryPoints, error) { serverEntryPointsTCP := make(TCPEntryPoints) - for entryPointName, config := range staticConfiguration.EntryPoints { + for entryPointName, config := range entryPointsConfig { ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName)) var err error @@ -171,6 +171,23 @@ func (e *TCPEntryPoint) StartTCP(ctx context.Context) { } 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)) }) } @@ -191,48 +208,48 @@ func (e *TCPEntryPoint) Shutdown(ctx context.Context) { logger.Debugf("Waiting %s seconds before killing connections.", graceTimeOut) var wg sync.WaitGroup + + shutdownServer := func(server stoppableServer) { + defer wg.Done() + err := server.Shutdown(ctx) + if err == nil { + return + } + if ctx.Err() == context.DeadlineExceeded { + logger.Debugf("Server failed to shutdown within deadline because: %s", err) + if err = server.Close(); err != nil { + logger.Error(err) + } + return + } + logger.Error(err) + // We expect Close to fail again because Shutdown most likely failed when trying to close a listener. + // We still call it however, to make sure that all connections get closed as well. + server.Close() + } + if e.httpServer.Server != nil { wg.Add(1) - go func() { - defer wg.Done() - if err := e.httpServer.Server.Shutdown(ctx); err != nil { - if ctx.Err() == context.DeadlineExceeded { - logger.Debugf("Wait server shutdown is overdue to: %s", err) - err = e.httpServer.Server.Close() - if err != nil { - logger.Error(err) - } - } - } - }() + go shutdownServer(e.httpServer.Server) } if e.httpsServer.Server != nil { wg.Add(1) - go func() { - 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) - } - } - } - }() + go shutdownServer(e.httpsServer.Server) } if e.tracker != nil { wg.Add(1) go func() { defer wg.Done() - if err := e.tracker.Shutdown(ctx); err != nil { - if ctx.Err() == context.DeadlineExceeded { - logger.Debugf("Wait hijack connection is overdue to: %s", err) - e.tracker.Close() - } + err := e.tracker.Shutdown(ctx) + if err == nil { + return } + if ctx.Err() == context.DeadlineExceeded { + logger.Debugf("Server failed to shutdown before deadline because: %s", err) + } + e.tracker.Close() }() } @@ -459,8 +476,11 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati } serverHTTP := &http.Server{ - Handler: handler, - ErrorLog: httpServerLogger, + Handler: handler, + 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) diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index 425a06390..6a0f75c08 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -3,8 +3,11 @@ package server import ( "bufio" "context" + "errors" + "io" "net" "net/http" + "strings" "testing" "time" @@ -15,128 +18,206 @@ import ( "github.com/stretchr/testify/require" ) -func TestShutdownHTTP(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()) - +func TestShutdownHijacked(t *testing.T) { router := &tcp.Router{} router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { conn, _, err := rw.(http.Hijacker).Hijack() require.NoError(t, err) - time.Sleep(1 * time.Second) resp := http.Response{StatusCode: http.StatusOK} err = resp.Write(conn) require.NoError(t, err) })) - - 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) + testShutdown(t, router) } -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{ - Address: ":0", - Transport: &static.EntryPointsTransport{ - LifeCycle: &static.LifeCycle{ - RequestAcceptGraceTimeout: 0, - GraceTimeOut: types.Duration(5 * time.Second), - }, - }, + // We explicitly use an IPV4 address because on Alpine, with an IPV6 address + // there seems to be shenanigans related to properly cleaning up file descriptors + Address: "127.0.0.1:0", + Transport: epConfig, ForwardedHeaders: &static.ForwardedHeaders{}, }) require.NoError(t, err) - go entryPoint.StartTCP(context.Background()) + conn, err := startEntrypoint(entryPoint, router) + require.NoError(t, err) - router := &tcp.Router{} - router.AddCatchAllNoTLS(tcp.HandlerFunc(func(conn tcp.WriteCloser) { - _, err := http.ReadRequest(bufio.NewReader(conn)) - require.NoError(t, err) - time.Sleep(1 * time.Second) + epAddr := entryPoint.listener.Addr().String() - resp := http.Response{StatusCode: http.StatusOK} - err = resp.Write(conn) - require.NoError(t, err) - })) + request, err := http.NewRequest(http.MethodHead, "http://127.0.0.1:8082", nil) + 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) go entryPoint.Shutdown(context.Background()) - request, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8082", nil) - require.NoError(t, err) + // Make sure that new connections are not permitted anymore. + // 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) - require.NoError(t, err) + // And make sure that the connection we had opened before shutting things down is still operational resp, err := http.ReadResponse(bufio.NewReader(conn), request) 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") + } } diff --git a/pkg/server/service/proxy.go b/pkg/server/service/proxy.go index 6da65d090..d6fdfcb8e 100644 --- a/pkg/server/service/proxy.go +++ b/pkg/server/service/proxy.go @@ -52,6 +52,10 @@ func buildProxy(passHostHeader *bool, responseForwarding *dynamic.ResponseForwar outReq.ProtoMajor = 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. if passHostHeader != nil && !*passHostHeader { outReq.Host = outReq.URL.Host diff --git a/pkg/tcp/router.go b/pkg/tcp/router.go index f2b3d8e88..89ad868ea 100644 --- a/pkg/tcp/router.go +++ b/pkg/tcp/router.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "strings" + "time" "github.com/containous/traefik/v2/pkg/log" ) @@ -34,7 +35,23 @@ func (r *Router) ServeTCP(conn WriteCloser) { } 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 { switch { 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, // without consuming any bytes from br. // 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) if err != nil { - if err != io.EOF { - log.Errorf("Error while Peeking first byte: %s", err) + opErr, ok := err.(*net.OpError) + 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 if hdr[0] != recordTypeHandshake { // log.Errorf("Error not tls") - return "", false, getPeeked(br) // Not TLS. + return "", false, getPeeked(br), nil // Not TLS. } const recordHeaderLen = 5 hdr, err = br.Peek(recordHeaderLen) if err != nil { 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] helloBytes, err := br.Peek(recordHeaderLen + recLen) if err != nil { log.Errorf("Error while Hello: %s", err) - return "", true, getPeeked(br) + return "", true, getPeeked(br), nil } sni := "" @@ -214,7 +232,7 @@ func clientHelloServerName(br *bufio.Reader) (string, bool, string) { }) _ = server.Handshake() - return sni, true, getPeeked(br) + return sni, true, getPeeked(br), nil } func getPeeked(br *bufio.Reader) string {