From 61e59d74e0c4165efb7891a40c07652cba8d4dfe Mon Sep 17 00:00:00 2001 From: mpl Date: Thu, 12 Dec 2019 15:12:05 +0100 Subject: [PATCH 01/22] CloseNotifier: return pointer instead of value --- .../accesslog/capture_response_writer.go | 11 +--- .../accesslog/capture_response_writer_test.go | 50 +++++++++++++++++++ pkg/middlewares/metrics/metrics_test.go | 42 ++++++++++++++++ pkg/middlewares/metrics/recorder.go | 9 +--- 4 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 pkg/middlewares/accesslog/capture_response_writer_test.go 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/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 { From 7eb866ffee05b0cf90d2b05d4197da00cf328dd3 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 12 Dec 2019 16:32:06 +0100 Subject: [PATCH 02/22] Improve documentation about Traefik build. --- docs/content/contributing/building-testing.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/content/contributing/building-testing.md b/docs/content/contributing/building-testing.md index d55e5971d..33e9bc885 100644 --- a/docs/content/contributing/building-testing.md +++ b/docs/content/contributing/building-testing.md @@ -105,13 +105,18 @@ 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 ``` From b5ae141fb6c56a794da936a046ce9c4dd001875c Mon Sep 17 00:00:00 2001 From: Manuel Zapf Date: Thu, 12 Dec 2019 17:06:05 +0100 Subject: [PATCH 03/22] Add Migration Guide for Traefik v2.1 --- docs/content/migration/v2.md | 99 ++++++++++++++++++++++++++++++++++++ docs/mkdocs.yml | 1 + 2 files changed, 100 insertions(+) create mode 100644 docs/content/migration/v2.md 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/mkdocs.yml b/docs/mkdocs.yml index 24d242542..0e23fbdba 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -154,6 +154,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' From a98c9f99d146af90c6b52481b02534c4633b4950 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 12 Dec 2019 19:44:04 +0100 Subject: [PATCH 04/22] Prepare release v2.1.1 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d197bdafb..282a9e93e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [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) From 903c63ac13f86cc85cd7b62e54e7bec5f232f628 Mon Sep 17 00:00:00 2001 From: Damien Duportal Date: Fri, 13 Dec 2019 10:36:04 +0100 Subject: [PATCH 05/22] add a documentation example for dashboard and api for kubernetes CRD --- .../operations/include-api-examples.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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`)" From 7dcee38b21998592228a3b7b6bfe4eb7a81ca69c Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 13 Dec 2019 15:46:06 +0100 Subject: [PATCH 06/22] Use consistent name in ACME documentation --- docs/content/https/acme.md | 46 +++++++++---------- ...acme-multiple-domains-from-rule-example.md | 2 +- .../include-acme-single-domain-example.md | 2 +- docs/content/https/ref-acme.toml | 6 +-- docs/content/https/ref-acme.txt | 24 +++++----- docs/content/https/ref-acme.yaml | 2 +- 6 files changed, 41 insertions(+), 41 deletions(-) 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: From b501c6d5bf1e0e39a296159406ff31376aef3c95 Mon Sep 17 00:00:00 2001 From: Kenneth Peiruza Date: Mon, 16 Dec 2019 21:48:03 +0100 Subject: [PATCH 07/22] Added ExternalName https support for Kubernetes CRD, as done in v2.0 --- pkg/provider/kubernetes/crd/kubernetes_http.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 0adca3e75..ed318e276 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 } From 4d0aee67be777e09a7929b2cee7d9d19fd4f7281 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 17 Dec 2019 14:30:06 +0100 Subject: [PATCH 08/22] doc: remove section about templates --- docs/content/contributing/building-testing.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/content/contributing/building-testing.md b/docs/content/contributing/building-testing.md index 33e9bc885..1f04745dc 100644 --- a/docs/content/contributing/building-testing.md +++ b/docs/content/contributing/building-testing.md @@ -62,7 +62,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" @@ -98,7 +98,8 @@ 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 @@ -123,10 +124,6 @@ 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` From 893023639626457445c4df30fcd00c82aaefc970 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 17 Dec 2019 16:10:06 +0100 Subject: [PATCH 09/22] fix: invalid label/flag parsing. --- pkg/config/parser/labels_decode.go | 4 ++++ pkg/config/parser/labels_decode_test.go | 9 +++++++++ 2 files changed, 13 insertions(+) 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{ From 4f669bdd66c2670c6f40155d0e113ff87b1b78a3 Mon Sep 17 00:00:00 2001 From: Dmitry Sharshakov Date: Wed, 18 Dec 2019 13:22:06 +0300 Subject: [PATCH 10/22] Don't set user-agent to Go-http-client/1.1 --- pkg/server/service/proxy.go | 4 ++++ 1 file changed, 4 insertions(+) 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 From 431abe79f32c8437d5c9319ba2b0fb04d0e42b2b Mon Sep 17 00:00:00 2001 From: Manuel Zapf Date: Thu, 19 Dec 2019 11:00:07 +0100 Subject: [PATCH 11/22] Query consul for service health separately --- integration/consul_catalog_test.go | 253 +++++++++++++++---- pkg/provider/consulcatalog/consul_catalog.go | 16 +- 2 files changed, 217 insertions(+), 52 deletions(-) diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 70905356b..06f0287a1 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/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) { From bc0b97d5d85c855151124f485b1fac552519b147 Mon Sep 17 00:00:00 2001 From: der-domi Date: Thu, 19 Dec 2019 21:38:03 +0100 Subject: [PATCH 12/22] Update ipwhitelist.md --- docs/content/middlewares/ipwhitelist.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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` From c127d34d32c8ebb3dd138ab494350efcdbc78950 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Sun, 22 Dec 2019 08:24:03 +0100 Subject: [PATCH 13/22] fix: Malformed x-b3-traceid Header --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b6849d6c4..927b5a8e3 100644 --- a/go.mod +++ b/go.mod @@ -80,7 +80,7 @@ require ( github.com/stvp/go-udp-testing v0.0.0-20171104055251-c4434f09ec13 github.com/tinylib/msgp v1.0.2 // indirect github.com/transip/gotransip v5.8.2+incompatible // indirect - github.com/uber/jaeger-client-go v2.19.0+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.5 diff --git a/go.sum b/go.sum index ef1db0ce6..acdf27be3 100644 --- a/go.sum +++ b/go.sum @@ -554,8 +554,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.19.0+incompatible h1:pbwbYfHUoaase0oPQOdZ1GcaUjImYGimUXSQ/+8+Z8Q= -github.com/uber/jaeger-client-go v2.19.0+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= From b380522df88c721fb4424cb1f59a38f1c6136051 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 24 Dec 2019 17:36:04 +0100 Subject: [PATCH 14/22] fix: dashboard redirect loop --- integration/testdata/rawdata-ingress.json | 2 +- pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json | 2 +- pkg/provider/traefik/fixtures/full_configuration.json | 2 +- pkg/provider/traefik/internal.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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/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, }, From 0837ec9b70cfdf5011f8331a9a09e7f4cbcf0bbe Mon Sep 17 00:00:00 2001 From: Tiago Boeing Date: Tue, 31 Dec 2019 21:56:04 -0300 Subject: [PATCH 15/22] Fix command for use websecure via CLI --- docs/content/routing/entrypoints.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 From 807dc46ad0b160cb9e5b4f312752dca1d1bde8e8 Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Mon, 6 Jan 2020 16:56:05 +0100 Subject: [PATCH 16/22] Handle respondingtimeout and better shutdown tests. Co-authored-by: Mathieu Lonjaret --- cmd/traefik/traefik.go | 2 +- pkg/server/server_entrypoint_tcp.go | 86 ++++--- pkg/server/server_entrypoint_tcp_test.go | 273 +++++++++++++++-------- pkg/tcp/router.go | 36 ++- 4 files changed, 258 insertions(+), 139 deletions(-) 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/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/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 { From 7283d7eb2f19e90e6628a8b3ad11b0a09e9579c6 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Doumenjou Date: Tue, 7 Jan 2020 10:46:04 +0100 Subject: [PATCH 17/22] Log the ignored namespace only when needed --- .../kubernetes/crd/kubernetes_http.go | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index ed318e276..6f6129850 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -380,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) } @@ -399,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 From d3977ce40e690ea4f12d3ad7e6e72af56418734b Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Doumenjou Date: Tue, 7 Jan 2020 11:26:05 +0100 Subject: [PATCH 18/22] Improve documentation about Kubernetes IngressRoute --- docs/content/https/tls.md | 2 +- docs/content/migration/v1-to-v2.md | 4 +- docs/content/providers/docker.md | 2 +- docs/content/providers/kubernetes-crd.md | 36 +- .../kubernetes-crd-definition.yml | 73 ++ ...ernetes-crd-ingressroutetcp-definition.yml | 13 + .../kubernetes-crd-rbac.yml | 57 + .../kubernetes-crd-resource.yml | 157 +++ .../dynamic-configuration/kubernetes-crd.md | 16 +- .../routing/providers/kubernetes-crd.md | 1038 +++++++++++++---- 10 files changed, 1179 insertions(+), 219 deletions(-) create mode 100644 docs/content/reference/dynamic-configuration/kubernetes-crd-definition.yml create mode 100644 docs/content/reference/dynamic-configuration/kubernetes-crd-ingressroutetcp-definition.yml create mode 100644 docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml create mode 100644 docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml 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/migration/v1-to-v2.md b/docs/content/migration/v1-to-v2.md index a2968ce00..e324b9849 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/providers/docker.md b/docs/content/providers/docker.md index 5310a78e4..6ac8cecb7 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/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. From c02f22200573c7a3a39a0c255e2b1bd6775407aa Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 7 Jan 2020 15:24:05 +0100 Subject: [PATCH 19/22] Improves error message when a configuration file is empty. --- pkg/config/file/file_node.go | 18 ++++++++-- pkg/config/file/file_node_test.go | 47 ++++++++++++++++++++------- pkg/config/file/fixtures/empty.toml | 0 pkg/config/file/fixtures/no_conf.toml | 2 ++ 4 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 pkg/config/file/fixtures/empty.toml create mode 100644 pkg/config/file/fixtures/no_conf.toml 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" From 49356cadd43afdcfd039fd537f21fd4bde75d25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Ch=C3=A1vez?= Date: Tue, 7 Jan 2020 15:48:07 +0100 Subject: [PATCH 20/22] fix(tracing): makes sure tracing headers are being propagated when using forwardAuth --- pkg/middlewares/auth/forward.go | 6 ++-- pkg/middlewares/auth/forward_test.go | 46 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) 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 +} From bd676922c30b94d835214b486aae61e7fc427338 Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Tue, 7 Jan 2020 09:26:08 -0600 Subject: [PATCH 21/22] k8s Ingress: fix crash on rules with nil http --- .../Ingress-one-rule-host-only_ingress.yml | 9 +++ pkg/provider/kubernetes/ingress/kubernetes.go | 68 ++++++++++--------- .../kubernetes/ingress/kubernetes_test.go | 11 +++ 3 files changed, 56 insertions(+), 32 deletions(-) create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Ingress-one-rule-host-only_ingress.yml 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{ From 4461ecfed16c00698a5992986f2a3828a8682e19 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 7 Jan 2020 16:56:05 +0100 Subject: [PATCH 22/22] Prepare release v2.1.2 --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 282a9e93e..cce3ec6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## [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)