diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dcc6c8c4..4d0f5c2c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change Log +## [v1.6.6](https://github.com/containous/traefik/tree/v1.6.6) (2018-08-20) +[All Commits](https://github.com/containous/traefik/compare/v1.6.5...v1.6.6) + +**Bug fixes:** +- **[acme]** Avoid duplicated ACME resolution ([#3751](https://github.com/containous/traefik/pull/3751) by [nmengin](https://github.com/nmengin)) +- **[api]** Remove TLS in API ([#3788](https://github.com/containous/traefik/pull/3788) by [Juliens](https://github.com/Juliens)) +- **[cluster]** Remove unusable `--cluster` flag ([#3616](https://github.com/containous/traefik/pull/3616) by [dtomcej](https://github.com/dtomcej)) +- **[ecs]** Fix bad condition in ECS provider ([#3609](https://github.com/containous/traefik/pull/3609) by [mmatur](https://github.com/mmatur)) +- Set keepalive on TCP socket so idleTimeout works ([#3740](https://github.com/containous/traefik/pull/3740) by [ajardan](https://github.com/ajardan)) + +**Documentation:** +- A tiny rewording on the documentation API's page ([#3794](https://github.com/containous/traefik/pull/3794) by [dduportal](https://github.com/dduportal)) +- Adding warnings and solution about the configuration exposure ([#3790](https://github.com/containous/traefik/pull/3790) by [dduportal](https://github.com/dduportal)) +- Fix path to the debug pprof API ([#3608](https://github.com/containous/traefik/pull/3608) by [multani](https://github.com/multani)) + +**Misc:** +- **[oxy,websocket]** Update oxy dependency ([#3777](https://github.com/containous/traefik/pull/3777) by [Juliens](https://github.com/Juliens)) + ## [v1.7.0-rc3](https://github.com/containous/traefik/tree/v1.7.0-rc3) (2018-08-01) [All Commits](https://github.com/containous/traefik/compare/v1.7.0-rc2...v1.7.0-rc3) diff --git a/Gopkg.lock b/Gopkg.lock index 3beaec275..3a6374681 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -695,7 +695,7 @@ branch = "master" name = "github.com/gorilla/websocket" packages = ["."] - revision = "eb925808374e5ca90c83401a40d711dc08c0c0f6" + revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d" [[projects]] name = "github.com/gravitational/trace" @@ -776,6 +776,7 @@ revision = "9b66602d496a139e4722bdde32f0f1ac1c12d4a8" [[projects]] + branch = "master" name = "github.com/jjcollinge/servicefabric" packages = ["."] revision = "8eebe170fa1ba25d3dfb928b3f86a7313b13b9fe" @@ -1262,7 +1263,7 @@ "roundrobin", "utils" ] - revision = "fb889e801a26e7e18ef36322ac72a07157f8cc1f" + revision = "f6bbeac6d5c4c06f88ba07ed42983ff36a5b407e" [[projects]] name = "github.com/vulcand/predicate" diff --git a/README.md b/README.md index ff090d6fb..021d8b4d5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![](https://images.microbadger.com/badges/image/traefik.svg)](https://microbadger.com/images/traefik) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/containous/traefik/blob/master/LICENSE.md) [![Join the chat at https://slack.traefik.io](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://slack.traefik.io) -[![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy) +[![Twitter](https://img.shields.io/twitter/follow/traefik.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefik) Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. @@ -164,12 +164,10 @@ Each version is supported until the next one is released (e.g. 1.1.x will be sup We use [Semantic Versioning](http://semver.org/) -## Plumbing +## Mailing lists -- [Oxy](https://github.com/vulcand/oxy): an awesome proxy library made by Mailgun folks -- [Gorilla mux](https://github.com/gorilla/mux): famous request router -- [Negroni](https://github.com/urfave/negroni): web middlewares made simple -- [Lego](https://github.com/xenolf/lego): the best [Let's Encrypt](https://letsencrypt.org) library in go +- General announcements, new releases: mail at news+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/news) +- Security announcements: mail at security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security). ## Credits diff --git a/acme/acme.go b/acme/acme.go index 492439d83..d9c90137f 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -12,6 +12,7 @@ import ( "net/url" "reflect" "strings" + "sync" "time" "github.com/BurntSushi/ty/fun" @@ -64,6 +65,8 @@ type ACME struct { jobs *channels.InfiniteChannel TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"` dynamicCerts *safe.Safe + resolvingDomains map[string]struct{} + resolvingDomainsMutex sync.RWMutex } func (a *ACME) init() error { @@ -76,6 +79,10 @@ func (a *ACME) init() error { } a.jobs = channels.NewInfiniteChannel() + + // Init the currently resolved domain map + a.resolvingDomains = make(map[string]struct{}) + return nil } @@ -202,6 +209,9 @@ func (a *ACME) leadershipListener(elected bool) error { } needRegister = true + } else if len(account.KeyType) == 0 { + // Set the KeyType if not already defined in the account + account.KeyType = acmeprovider.GetKeyType(a.KeyType) } a.client, err = a.buildACMEClient(account) @@ -534,6 +544,10 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { if len(uncheckedDomains) == 0 { return } + + a.addResolvingDomains(uncheckedDomains) + defer a.removeResolvingDomains(uncheckedDomains) + certificate, err := a.getDomainsCertificates(uncheckedDomains) if err != nil { log.Errorf("Error getting ACME certificates %+v : %v", uncheckedDomains, err) @@ -565,6 +579,24 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { } } +func (a *ACME) addResolvingDomains(resolvingDomains []string) { + a.resolvingDomainsMutex.Lock() + defer a.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + a.resolvingDomains[domain] = struct{}{} + } +} + +func (a *ACME) removeResolvingDomains(resolvingDomains []string) { + a.resolvingDomainsMutex.Lock() + defer a.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + delete(a.resolvingDomains, domain) + } +} + // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates func (a *ACME) getProvidedCertificate(domains string) *tls.Certificate { @@ -600,6 +632,9 @@ func searchProvidedCertificateForDomains(domain string, certs map[string]*tls.Ce // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string { + a.resolvingDomainsMutex.RLock() + defer a.resolvingDomainsMutex.RUnlock() + log.Debugf("Looking for provided certificate to validate %s...", domains) allCerts := make(map[string]*tls.Certificate) @@ -622,6 +657,13 @@ func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string } } + // Get currently resolved domains + for domain := range a.resolvingDomains { + if _, ok := allCerts[domain]; !ok { + allCerts[domain] = &tls.Certificate{} + } + } + // Get Configuration Domains for i := 0; i < len(a.Domains); i++ { allCerts[a.Domains[i].Main] = &tls.Certificate{} diff --git a/acme/acme_test.go b/acme/acme_test.go index 9e3d2ace4..aadfa17b6 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -331,9 +331,12 @@ func TestAcme_getUncheckedCertificates(t *testing.T) { mm["*.containo.us"] = &tls.Certificate{} mm["traefik.acme.io"] = &tls.Certificate{} - a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}} + dm := make(map[string]struct{}) + dm["*.traefik.wtf"] = struct{}{} - domains := []string{"traefik.containo.us", "trae.containo.us"} + a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}, resolvingDomains: dm} + + domains := []string{"traefik.containo.us", "trae.containo.us", "foo.traefik.wtf"} uncheckedDomains := a.getUncheckedDomains(domains, nil) assert.Empty(t, uncheckedDomains) domains = []string{"traefik.acme.io", "trae.acme.io"} @@ -351,6 +354,9 @@ func TestAcme_getUncheckedCertificates(t *testing.T) { account := Account{DomainsCertificate: domainsCertificates} uncheckedDomains = a.getUncheckedDomains(domains, &account) assert.Empty(t, uncheckedDomains) + domains = []string{"traefik.containo.us", "trae.containo.us", "traefik.wtf"} + uncheckedDomains = a.getUncheckedDomains(domains, nil) + assert.Len(t, uncheckedDomains, 1) } func TestAcme_getProvidedCertificate(t *testing.T) { diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index 8c4a79c69..392951292 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -144,8 +144,8 @@ var _templatesConsul_catalogTmpl = []byte(`[backends] [frontends."frontend-{{ $service.ServiceName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -389,8 +389,8 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -549,13 +549,13 @@ var _templatesEcsTmpl = []byte(`[backends] {{range $serviceName, $instances := .Services }} {{ $firstInstance := index $instances 0 }} - {{ $circuitBreaker := getCircuitBreaker $firstInstance.TraefikLabels }} + {{ $circuitBreaker := getCircuitBreaker $firstInstance.SegmentLabels }} {{if $circuitBreaker }} [backends."backend-{{ $serviceName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $firstInstance.TraefikLabels }} + {{ $loadBalancer := getLoadBalancer $firstInstance.SegmentLabels }} {{if $loadBalancer }} [backends."backend-{{ $serviceName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -565,14 +565,14 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $maxConn := getMaxConn $firstInstance.TraefikLabels }} + {{ $maxConn := getMaxConn $firstInstance.SegmentLabels }} {{if $maxConn }} [backends."backend-{{ $serviceName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $firstInstance.TraefikLabels }} + {{ $healthCheck := getHealthCheck $firstInstance.SegmentLabels }} {{if $healthCheck }} [backends."backend-{{ $serviceName }}".healthCheck] scheme = "{{ $healthCheck.Scheme }}" @@ -588,7 +588,7 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $buffering := getBuffering $firstInstance.TraefikLabels }} + {{ $buffering := getBuffering $firstInstance.SegmentLabels }} {{if $buffering }} [backends."backend-{{ $serviceName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -610,38 +610,40 @@ var _templatesEcsTmpl = []byte(`[backends] {{range $serviceName, $instances := .Services }} {{range $instance := filterFrontends $instances }} - [frontends."frontend-{{ $serviceName }}"] - backend = "backend-{{ $serviceName }}" - priority = {{ getPriority $instance.TraefikLabels }} - passHostHeader = {{ getPassHostHeader $instance.TraefikLabels }} - passTLSCert = {{ getPassTLSCert $instance.TraefikLabels }} + {{ $frontendName := getFrontendName $instance }} - entryPoints = [{{range getEntryPoints $instance.TraefikLabels }} + [frontends."frontend-{{ $frontendName }}"] + backend = "backend-{{ $serviceName }}" + priority = {{ getPriority $instance.SegmentLabels }} + passHostHeader = {{ getPassHostHeader $instance.SegmentLabels }} + passTLSCert = {{ getPassTLSCert $instance.SegmentLabels }} + + entryPoints = [{{range getEntryPoints $instance.SegmentLabels }} "{{.}}", {{end}}] - {{ $auth := getAuth $instance.TraefikLabels }} + {{ $auth := getAuth $instance.SegmentLabels }} {{if $auth }} - [frontends."frontend-{{ $serviceName }}".auth] + [frontends."frontend-{{ $frontendName }}".auth] headerField = "{{ $auth.HeaderField }}" {{if $auth.Forward }} - [frontends."frontend-{{ $serviceName }}".auth.forward] + [frontends."frontend-{{ $frontendName }}".auth.forward] address = "{{ $auth.Forward.Address }}" trustForwardHeader = {{ $auth.Forward.TrustForwardHeader }} {{if $auth.Forward.TLS }} - [frontends."frontend-{{ $serviceName }}".auth.forward.tls] + [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} {{if $auth.Basic }} - [frontends."frontend-{{ $serviceName }}".auth.basic] + [frontends."frontend-{{ $frontendName }}".auth.basic] removeHeader = {{ $auth.Basic.RemoveHeader }} {{if $auth.Basic.Users }} users = [{{range $auth.Basic.Users }} @@ -652,7 +654,7 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{if $auth.Digest }} - [frontends."frontend-{{ $serviceName }}".auth.digest] + [frontends."frontend-{{ $frontendName }}".auth.digest] removeHeader = {{ $auth.Digest.RemoveHeader }} {{if $auth.Digest.Users }} users = [{{range $auth.Digest.Users }} @@ -663,14 +665,14 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $whitelist := getWhiteList $instance.TraefikLabels }} + {{ $whitelist := getWhiteList $instance.SegmentLabels }} {{if $whitelist }} - [frontends."frontend-{{ $serviceName }}".whiteList] + [frontends."frontend-{{ $frontendName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} "{{.}}", {{end}}] {{if $whitelist.IPStrategy }} - [frontends."frontend-{{ $serviceName }}".whiteList.IPStrategy] + [frontends."frontend-{{ $frontendName }}".whiteList.IPStrategy] depth = {{ $whitelist.IPStrategy.Depth }} excludedIPs = [{{range $whitelist.IPStrategy.ExcludedIPs }} "{{.}}", @@ -678,20 +680,20 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $redirect := getRedirect $instance.TraefikLabels }} + {{ $redirect := getRedirect $instance.SegmentLabels }} {{if $redirect }} - [frontends."frontend-{{ $serviceName }}".redirect] + [frontends."frontend-{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" regex = "{{ $redirect.Regex }}" replacement = "{{ $redirect.Replacement }}" permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $instance.TraefikLabels }} + {{ $errorPages := getErrorPages $instance.SegmentLabels }} {{if $errorPages }} - [frontends."frontend-{{ $serviceName }}".errors] + [frontends."frontend-{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} - [frontends."frontend-{{ $serviceName }}".errors."{{ $pageName }}"] + [frontends."frontend-{{ $frontendName }}".errors."{{ $pageName }}"] status = [{{range $page.Status }} "{{.}}", {{end}}] @@ -700,22 +702,22 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $rateLimit := getRateLimit $instance.TraefikLabels }} + {{ $rateLimit := getRateLimit $instance.SegmentLabels }} {{if $rateLimit }} - [frontends."frontend-{{ $serviceName }}".rateLimit] + [frontends."frontend-{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" - [frontends."frontend-{{ $serviceName }}".rateLimit.rateSet] + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet] {{ range $limitName, $limit := $rateLimit.RateSet }} - [frontends."frontend-{{ $serviceName }}".rateLimit.rateSet."{{ $limitName }}"] + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet."{{ $limitName }}"] period = "{{ $limit.Period }}" average = {{ $limit.Average }} burst = {{ $limit.Burst }} {{end}} {{end}} - {{ $headers := getHeaders $instance.TraefikLabels }} + {{ $headers := getHeaders $instance.SegmentLabels }} {{if $headers }} - [frontends."frontend-{{ $serviceName }}".headers] + [frontends."frontend-{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }} SSLHost = "{{ $headers.SSLHost }}" @@ -747,28 +749,28 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{if $headers.CustomRequestHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.customRequestHeaders] + [frontends."frontend-{{ $frontendName }}".headers.customRequestHeaders] {{range $k, $v := $headers.CustomRequestHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{if $headers.CustomResponseHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.customResponseHeaders] + [frontends."frontend-{{ $frontendName }}".headers.customResponseHeaders] {{range $k, $v := $headers.CustomResponseHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{if $headers.SSLProxyHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.SSLProxyHeaders] + [frontends."frontend-{{ $frontendName }}".headers.SSLProxyHeaders] {{range $k, $v := $headers.SSLProxyHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{end}} - [frontends."frontend-{{ $serviceName }}".routes."route-frontend-{{ $serviceName }}"] + [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] rule = "{{ getFrontendRule $instance }}" {{end}} @@ -912,8 +914,8 @@ var _templatesKubernetesTmpl = []byte(`[backends] trustForwardHeader = {{ $frontend.Auth.Forward.TrustForwardHeader }} {{if $frontend.Auth.Forward.TLS }} [frontends."{{ $frontendName }}".auth.forward.tls] - cert = "{{ $frontend.Auth.Forward.TLS.Cert }}" - key = "{{ $frontend.Auth.Forward.TLS.Key }}" + cert = """{{ $frontend.Auth.Forward.TLS.Cert }}""" + key = """{{ $frontend.Auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $frontend.Auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -1137,8 +1139,8 @@ var _templatesKvTmpl = []byte(`[backends] [frontends."{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -1399,8 +1401,8 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} [frontends."{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -1646,8 +1648,8 @@ var _templatesMesosTmpl = []byte(`[backends] [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -1915,8 +1917,8 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/cluster/datastore.go b/cluster/datastore.go index 7bcf0a4c2..84eb4a73b 100644 --- a/cluster/datastore.go +++ b/cluster/datastore.go @@ -78,7 +78,7 @@ func (d *Datastore) watchChanges() error { stopCh := make(chan struct{}) kvCh, err := d.kv.Watch(d.lockKey, stopCh, nil) if err != nil { - return err + return fmt.Errorf("error while watching key %s: %v", d.lockKey, err) } safe.Go(func() { ctx, cancel := context.WithCancel(d.ctx) diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index 2f282fa73..1dd6aa920 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -102,29 +102,23 @@ entryPoint = "https" # # KeyType = "RSA4096" -# Domains list. -# Only domains defined here can generate wildcard certificates. -# -# [[acme.domains]] -# main = "local1.com" -# sans = ["test1.local1.com", "test2.local1.com"] -# [[acme.domains]] -# main = "local2.com" -# [[acme.domains]] -# main = "*.local3.com" -# sans = ["local3.com", "test1.test1.local3.com"] - -# Use a HTTP-01 ACME challenge. +# Use a TLS-ALPN-01 ACME challenge. # # Optional (but recommended) # -[acme.httpChallenge] +[acme.tlsChallenge] + +# Use a HTTP-01 ACME challenge. +# +# Optional +# +# [acme.httpChallenge] # EntryPoint to use for the HTTP-01 challenges. # # Required # - entryPoint = "http" + # entryPoint = "http" # Use a DNS-01 ACME challenge rather than HTTP-01 challenge. # Note: mandatory for wildcard certificate generation. @@ -147,6 +141,18 @@ entryPoint = "https" # Default: 0 # # delayBeforeCheck = 0 + +# Domains list. +# Only domains defined here can generate wildcard certificates. +# +# [[acme.domains]] +# main = "local1.com" +# sans = ["test1.local1.com", "test2.local1.com"] +# [[acme.domains]] +# main = "local2.com" +# [[acme.domains]] +# main = "*.local3.com" +# sans = ["local3.com", "test1.test1.local3.com"] ``` ### `caServer` @@ -164,7 +170,7 @@ caServer = "https://acme-staging-v02.api.letsencrypt.org/directory" ### ACME Challenge -#### TLS Challenge +#### `tlsChallenge` Use the `TLS-ALPN-01` challenge to generate and renew ACME certificates by provisioning a TLS certificate. @@ -245,44 +251,43 @@ Useful if internal networks block external DNS queries. Here is a list of supported `provider`s, that can automate the DNS verification, along with the required environment variables and their [wildcard & root domain support](/configuration/acme/#wildcard-domains) for each. Do not hesitate to complete it. -| Provider Name | Provider Code | Environment Variables | Wildcard & Root Domain Support | -|--------------------------------------------------------|----------------|-----------------------------------------------------------------------------------------------------------------------------|--------------------------------| -| [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | Not tested yet | -| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP` | Not tested yet | -| [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` | Not tested yet | -| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CLOUDFLARE_EMAIL`, `CLOUDFLARE_API_KEY` - The `Global API Key` needs to be used, not the `Origin CA Key` | YES | -| [CloudXNS](https://www.cloudxns.net) | `cloudxns` | `CLOUDXNS_API_KEY`, `CLOUDXNS_SECRET_KEY` | Not tested yet | -| [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` | YES | -| [DNSimple](https://dnsimple.com) | `dnsimple` | `DNSIMPLE_OAUTH_TOKEN`, `DNSIMPLE_BASE_URL` | Not tested yet | -| [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | `DNSMADEEASY_API_KEY`, `DNSMADEEASY_API_SECRET`, `DNSMADEEASY_SANDBOX` | Not tested yet | -| [DNSPod](http://www.dnspod.net/) | `dnspod` | `DNSPOD_API_KEY` | Not tested yet | -| [Duck DNS](https://www.duckdns.org/) | `duckdns` | `DUCKDNS_TOKEN` | Not tested yet | -| [Dyn](https://dyn.com) | `dyn` | `DYN_CUSTOMER_NAME`, `DYN_USER_NAME`, `DYN_PASSWORD` | Not tested yet | -| External Program | `exec` | `EXEC_PATH` | Not tested yet | -| [Exoscale](https://www.exoscale.ch) | `exoscale` | `EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`, `EXOSCALE_ENDPOINT` | YES | -| [Fast DNS](https://www.akamai.com/) | `fastdns` | `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`, `AKAMAI_ACCESS_TOKEN` | Not tested yet | -| [Gandi](https://www.gandi.net) | `gandi` | `GANDI_API_KEY` | Not tested yet | -| [Gandi V5](http://doc.livedns.gandi.net) | `gandiv5` | `GANDIV5_API_KEY` | YES | -| [Glesys](https://glesys.com/) | `glesys` | `GLESYS_API_USER`, `GLESYS_API_KEY`, `GLESYS_DOMAIN` | Not tested yet | -| [GoDaddy](https://godaddy.com/domains) | `godaddy` | `GODADDY_API_KEY`, `GODADDY_API_SECRET` | Not tested yet | -| [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | `GCE_PROJECT`, `GCE_SERVICE_ACCOUNT_FILE` | YES | -| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` | Not tested yet | -| [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` | Not tested yet | -| manual | - | none, but you need to run Træfik interactively, turn on `acmeLogging` to see instructions and press Enter. | YES | -| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | YES | -| [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | Not tested yet | -| [NIFCloud](https://cloud.nifty.com/service/dns.htm) | `nifcloud` | `NIFCLOUD_ACCESS_KEY_ID`, `NIFCLOUD_SECRET_ACCESS_KEY` | Not tested yet | -| [Ns1](https://ns1.com/) | `ns1` | `NS1_API_KEY` | Not tested yet | -| [Open Telekom Cloud](https://cloud.telekom.de) | `otc` | `OTC_DOMAIN_NAME`, `OTC_USER_NAME`, `OTC_PASSWORD`, `OTC_PROJECT_NAME`, `OTC_IDENTITY_ENDPOINT` | Not tested yet | -| [OVH](https://www.ovh.com) | `ovh` | `OVH_ENDPOINT`, `OVH_APPLICATION_KEY`, `OVH_APPLICATION_SECRET`, `OVH_CONSUMER_KEY` | YES | -| [PowerDNS](https://www.powerdns.com) | `pdns` | `PDNS_API_KEY`, `PDNS_API_URL` | Not tested yet | -| [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | `RACKSPACE_USER`, `RACKSPACE_API_KEY` | Not tested yet | -| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` | Not tested yet | -| [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_HOSTED_ZONE_ID` or a configured user/instance IAM profile. | YES | -| [Sakura Cloud](https://cloud.sakura.ad.jp/) | `sakuracloud` | `SAKURACLOUD_ACCESS_TOKEN`, `SAKURACLOUD_ACCESS_TOKEN_SECRET` | Not tested yet | -| [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet | -| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet | - +| Provider Name | Provider Code | Environment Variables | Wildcard & Root Domain Support | +|--------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------|--------------------------------| +| [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | Not tested yet | +| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP` | Not tested yet | +| [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` | Not tested yet | +| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CLOUDFLARE_EMAIL`, `CLOUDFLARE_API_KEY` - The `Global API Key` needs to be used, not the `Origin CA Key` | YES | +| [CloudXNS](https://www.cloudxns.net) | `cloudxns` | `CLOUDXNS_API_KEY`, `CLOUDXNS_SECRET_KEY` | Not tested yet | +| [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` | YES | +| [DNSimple](https://dnsimple.com) | `dnsimple` | `DNSIMPLE_OAUTH_TOKEN`, `DNSIMPLE_BASE_URL` | Not tested yet | +| [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | `DNSMADEEASY_API_KEY`, `DNSMADEEASY_API_SECRET`, `DNSMADEEASY_SANDBOX` | Not tested yet | +| [DNSPod](http://www.dnspod.net/) | `dnspod` | `DNSPOD_API_KEY` | Not tested yet | +| [Duck DNS](https://www.duckdns.org/) | `duckdns` | `DUCKDNS_TOKEN` | Not tested yet | +| [Dyn](https://dyn.com) | `dyn` | `DYN_CUSTOMER_NAME`, `DYN_USER_NAME`, `DYN_PASSWORD` | Not tested yet | +| External Program | `exec` | `EXEC_PATH` | Not tested yet | +| [Exoscale](https://www.exoscale.ch) | `exoscale` | `EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`, `EXOSCALE_ENDPOINT` | YES | +| [Fast DNS](https://www.akamai.com/) | `fastdns` | `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`, `AKAMAI_ACCESS_TOKEN` | Not tested yet | +| [Gandi](https://www.gandi.net) | `gandi` | `GANDI_API_KEY` | Not tested yet | +| [Gandi V5](http://doc.livedns.gandi.net) | `gandiv5` | `GANDIV5_API_KEY` | YES | +| [Glesys](https://glesys.com/) | `glesys` | `GLESYS_API_USER`, `GLESYS_API_KEY`, `GLESYS_DOMAIN` | Not tested yet | +| [GoDaddy](https://godaddy.com/domains) | `godaddy` | `GODADDY_API_KEY`, `GODADDY_API_SECRET` | Not tested yet | +| [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | `GCE_PROJECT`, `GCE_SERVICE_ACCOUNT_FILE` | YES | +| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` | Not tested yet | +| [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` | Not tested yet | +| manual | - | none, but you need to run Træfik interactively, turn on `acmeLogging` to see instructions and press Enter. | YES | +| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | YES | +| [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | Not tested yet | +| [NIFCloud](https://cloud.nifty.com/service/dns.htm) | `nifcloud` | `NIFCLOUD_ACCESS_KEY_ID`, `NIFCLOUD_SECRET_ACCESS_KEY` | Not tested yet | +| [Ns1](https://ns1.com/) | `ns1` | `NS1_API_KEY` | Not tested yet | +| [Open Telekom Cloud](https://cloud.telekom.de) | `otc` | `OTC_DOMAIN_NAME`, `OTC_USER_NAME`, `OTC_PASSWORD`, `OTC_PROJECT_NAME`, `OTC_IDENTITY_ENDPOINT` | Not tested yet | +| [OVH](https://www.ovh.com) | `ovh` | `OVH_ENDPOINT`, `OVH_APPLICATION_KEY`, `OVH_APPLICATION_SECRET`, `OVH_CONSUMER_KEY` | YES | +| [PowerDNS](https://www.powerdns.com) | `pdns` | `PDNS_API_KEY`, `PDNS_API_URL` | Not tested yet | +| [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | `RACKSPACE_USER`, `RACKSPACE_API_KEY` | Not tested yet | +| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` | Not tested yet | +| [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `[AWS_REGION]`, `[AWS_HOSTED_ZONE_ID]` or a configured user/instance IAM profile. | YES | +| [Sakura Cloud](https://cloud.sakura.ad.jp/) | `sakuracloud` | `SAKURACLOUD_ACCESS_TOKEN`, `SAKURACLOUD_ACCESS_TOKEN_SECRET` | Not tested yet | +| [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet | +| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet | ### `domains` diff --git a/docs/configuration/api.md b/docs/configuration/api.md index eda514c98..215e2ce7c 100644 --- a/docs/configuration/api.md +++ b/docs/configuration/api.md @@ -4,6 +4,9 @@ ```toml # API definition +# Warning: Enabling API will expose Træfik's configuration. +# It is not recommended in production, +# unless secured by authentication and authorizations [api] # Name of the related entry point # @@ -12,7 +15,7 @@ # entryPoint = "traefik" - # Enabled Dashboard + # Enable Dashboard # # Optional # Default: true @@ -38,6 +41,22 @@ For more customization, see [entry points](/configuration/entrypoints/) document ![Web UI Health](/img/traefik-health.png) +## Security + +Enabling the API will expose all configuration elements, +including sensitive data. + +It is not recommended in production, +unless secured by authentication and authorizations. + +A good sane default (but not exhaustive) set of recommendations +would be to apply the following protection mechanism: + +* _At application level:_ enabling HTTP [Basic Authentication](#authentication) +* _At transport level:_ NOT exposing publicly the API's port, +keeping it restricted over internal networks +(restricted networks as in https://en.wikipedia.org/wiki/Principle_of_least_privilege). + ## API | Path | Method | Description | diff --git a/docs/configuration/backends/consulcatalog.md b/docs/configuration/backends/consulcatalog.md index dabab3387..91c8a9123 100644 --- a/docs/configuration/backends/consulcatalog.md +++ b/docs/configuration/backends/consulcatalog.md @@ -152,6 +152,17 @@ Additional settings can be defined using Consul Catalog tags. | `.frontend.whiteList.ipStrategy.depth=5` | See [whitelist](/configuration/entrypoints/#white-listing) | | `.frontend.whiteList.ipStrategy.excludedIPs=127.0.0.1` | See [whitelist](/configuration/entrypoints/#white-listing) | +### Multiple frontends for a single service + +If you need to support multiple frontends for a service, for example when having multiple `rules` that can't be combined, specify them as follows: + +``` +.frontends.A.rule=Host:A:PathPrefix:/A +.frontends.B.rule=Host:B:PathPrefix:/ +``` + +`A` and `B` here are just arbitrary names, they can be anything. You can use any setting that applies to `.frontend` from the table above. + ### Custom Headers !!! note diff --git a/docs/configuration/backends/docker.md b/docs/configuration/backends/docker.md index 69abc0209..37c61c610 100644 --- a/docs/configuration/backends/docker.md +++ b/docs/configuration/backends/docker.md @@ -19,7 +19,7 @@ Træfik can be configured to use Docker as a provider. # endpoint = "unix:///var/run/docker.sock" -# Default domain used. +# Default base domain used for the frontend rules. # Can be overridden by setting the "traefik.domain" label on a container. # # Required @@ -110,7 +110,7 @@ To enable constraints see [provider-specific constraints section](/configuration # endpoint = "tcp://127.0.0.1:2375" -# Default domain used. +# Default base domain used for the frontend rules. # Can be overridden by setting the "traefik.domain" label on a services. # # Optional @@ -210,7 +210,7 @@ Labels can be used on containers to override default behavior. | Label | Description | |-------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `traefik.docker.network` | Overrides the default docker network to use for connections to the container. [1] | -| `traefik.domain` | Sets the default domain for the frontend rules. | +| `traefik.domain` | Sets the default base domain for the frontend rules. For more information, check the [Container Labels section's of the user guide "Let's Encrypt & Docker"](/user-guide/docker-and-lets-encrypt/#container-labels) | | `traefik.enable=false` | Disables this container in Træfik. | | `traefik.port=80` | Registers this port. Useful when the container exposes multiples ports. | | `traefik.protocol=https` | Overrides the default `http` protocol | @@ -237,10 +237,10 @@ Labels can be used on containers to override default behavior. | `traefik.frontend.auth.basic=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` [2] (DEPRECATED). | | `traefik.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.basic.users=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` [2]. | -| `traefik.frontend.auth.basic.usersfile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | +| `traefik.frontend.auth.basic.usersFile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.digest.users=EXPR` | Sets the digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. | -| `traefik.frontend.auth.digest.usersfile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | +| `traefik.frontend.auth.digest.usersFile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.forward.address=https://example.com` | Sets the URL of the authentication server. | | `traefik.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. | | `traefik.frontend.auth.forward.tls.caOptional=true` | Checks the certificates if present but do not force to be signed by a specified Certificate Authority (CA). | @@ -330,10 +330,10 @@ Segment labels override the default behavior. | `traefik..frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | | `traefik..frontend.auth.basic.removeHeader=true` | Same as `traefik.frontend.auth.basic.removeHeader` | | `traefik..frontend.auth.basic.users=EXPR` | Same as `traefik.frontend.auth.basic.users` | -| `traefik..frontend.auth.basic.usersfile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersfile` | +| `traefik..frontend.auth.basic.usersFile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersFile` | | `traefik..frontend.auth.digest.removeHeader=true` | Same as `traefik.frontend.auth.digest.removeHeader` | | `traefik..frontend.auth.digest.users=EXPR` | Same as `traefik.frontend.auth.digest.users` | -| `traefik..frontend.auth.digest.usersfile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersfile` | +| `traefik..frontend.auth.digest.usersFile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersFile` | | `traefik..frontend.auth.forward.address=https://example.com` | Same as `traefik.frontend.auth.forward.address` | | `traefik..frontend.auth.forward.tls.ca=/path/ca.pem` | Same as `traefik.frontend.auth.forward.tls.ca` | | `traefik..frontend.auth.forward.tls.caOptional=true` | Same as `traefik.frontend.auth.forward.tls.caOptional` | diff --git a/docs/configuration/backends/ecs.md b/docs/configuration/backends/ecs.md index 03e715440..c2bd67b4c 100644 --- a/docs/configuration/backends/ecs.md +++ b/docs/configuration/backends/ecs.md @@ -190,7 +190,7 @@ Labels can be used on task containers to override default behavior: | `traefik.frontend.whiteList.sourceRange=RANGE` | Sets a list of IP-Ranges which are allowed to access.
An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | | `traefik.frontend.whiteList.ipStrategy=true` | Uses the default IPStrategy.
Can be used when there is an existing `clientIPStrategy` but you want the remote address for whitelisting. | | `traefik.frontend.whiteList.ipStrategy.depth=5` | See [whitelist](/configuration/entrypoints/#white-listing) | -| `traefik.frontend.whiteList.ipStrategy.excludedIPs=127.0.0.1 ` | See [whitelist](/configuration/entrypoints/#white-listing) | +| `traefik.frontend.whiteList.ipStrategy.excludedIPs=127.0.0.1` | See [whitelist](/configuration/entrypoints/#white-listing) | ### Custom Headers @@ -223,3 +223,88 @@ Labels can be used on task containers to override default behavior: | `traefik.frontend.headers.STSSeconds=315360000` | Sets the max-age of the STS header. | | `traefik.frontend.headers.STSIncludeSubdomains=true` | Adds the `IncludeSubdomains` section of the STS header. | | `traefik.frontend.headers.STSPreload=true` | Adds the preload flag to the STS header. | + +### Containers with Multiple Ports (segment labels) + +Segment labels are used to define routes to an application exposing multiple ports. +A segment is a group of labels that apply to a port exposed by an application. +You can define as many segments as ports exposed in an application. + +Segment labels override the default behavior. + +| Label | Description | +|------------------------------------------------------------------------------|----------------------------------------------------------------| +| `traefik..backend=BACKEND` | Same as `traefik.backend` | +| `traefik..domain=DOMAIN` | Same as `traefik.domain` | +| `traefik..port=PORT` | Same as `traefik.port` | +| `traefik..protocol=http` | Same as `traefik.protocol` | +| `traefik..weight=10` | Same as `traefik.weight` | +| `traefik..frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | +| `traefik..frontend.auth.basic.removeHeader=true` | Same as `traefik.frontend.auth.basic.removeHeader` | +| `traefik..frontend.auth.basic.users=EXPR` | Same as `traefik.frontend.auth.basic.users` | +| `traefik..frontend.auth.basic.usersFile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersFile` | +| `traefik..frontend.auth.digest.removeHeader=true` | Same as `traefik.frontend.auth.digest.removeHeader` | +| `traefik..frontend.auth.digest.users=EXPR` | Same as `traefik.frontend.auth.digest.users` | +| `traefik..frontend.auth.digest.usersFile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersFile` | +| `traefik..frontend.auth.forward.address=https://example.com` | Same as `traefik.frontend.auth.forward.address` | +| `traefik..frontend.auth.forward.tls.ca=/path/ca.pem` | Same as `traefik.frontend.auth.forward.tls.ca` | +| `traefik..frontend.auth.forward.tls.caOptional=true` | Same as `traefik.frontend.auth.forward.tls.caOptional` | +| `traefik..frontend.auth.forward.tls.cert=/path/server.pem` | Same as `traefik.frontend.auth.forward.tls.cert` | +| `traefik..frontend.auth.forward.tls.insecureSkipVerify=true` | Same as `traefik.frontend.auth.forward.tls.insecureSkipVerify` | +| `traefik..frontend.auth.forward.tls.key=/path/server.key` | Same as `traefik.frontend.auth.forward.tls.key` | +| `traefik..frontend.auth.forward.trustForwardHeader=true` | Same as `traefik.frontend.auth.forward.trustForwardHeader` | +| `traefik..frontend.auth.headerField=X-WebAuth-User` | Same as `traefik.frontend.auth.headerField` | +| `traefik..frontend.auth.removeHeader=true` | Same as `traefik.frontend.auth.removeHeader` | +| `traefik..frontend.entryPoints=https` | Same as `traefik.frontend.entryPoints` | +| `traefik..frontend.errors..backend=NAME` | Same as `traefik.frontend.errors..backend` | +| `traefik..frontend.errors..query=PATH` | Same as `traefik.frontend.errors..query` | +| `traefik..frontend.errors..status=RANGE` | Same as `traefik.frontend.errors..status` | +| `traefik..frontend.passHostHeader=true` | Same as `traefik.frontend.passHostHeader` | +| `traefik..frontend.passTLSCert=true` | Same as `traefik.frontend.passTLSCert` | +| `traefik..frontend.priority=10` | Same as `traefik.frontend.priority` | +| `traefik..frontend.rateLimit.extractorFunc=EXP` | Same as `traefik.frontend.rateLimit.extractorFunc` | +| `traefik..frontend.rateLimit.rateSet..period=6` | Same as `traefik.frontend.rateLimit.rateSet..period` | +| `traefik..frontend.rateLimit.rateSet..average=6` | Same as `traefik.frontend.rateLimit.rateSet..average` | +| `traefik..frontend.rateLimit.rateSet..burst=6` | Same as `traefik.frontend.rateLimit.rateSet..burst` | +| `traefik..frontend.redirect.entryPoint=https` | Same as `traefik.frontend.redirect.entryPoint` | +| `traefik..frontend.redirect.regex=^http://localhost/(.*)` | Same as `traefik.frontend.redirect.regex` | +| `traefik..frontend.redirect.replacement=http://mydomain/$1` | Same as `traefik.frontend.redirect.replacement` | +| `traefik..frontend.redirect.permanent=true` | Same as `traefik.frontend.redirect.permanent` | +| `traefik..frontend.rule=EXP` | Same as `traefik.frontend.rule` | +| `traefik..frontend.whiteList.sourceRange=RANGE` | Same as `traefik.frontend.whiteList.sourceRange` | +| `traefik..frontend.whiteList.useXForwardedFor=true` | Same as `traefik.frontend.whiteList.useXForwardedFor` | +| `traefik..frontend.whiteList.ipStrategy=true` | Same as `traefik.frontend.whiteList.ipStrategy` | +| `traefik..frontend.whiteList.ipStrategy.depth=5` | Same as `traefik.frontend.whiteList.ipStrategy.depth` | +| `traefik..frontend.whiteList.ipStrategy.excludedIPs=127.0.0.1` | Same as `traefik.frontend.whiteList.ipStrategy.excludedIPs` | + +#### Custom Headers + +| Label | Description | +|----------------------------------------------------------------------|----------------------------------------------------------| +| `traefik..frontend.headers.customRequestHeaders=EXPR ` | Same as `traefik.frontend.headers.customRequestHeaders` | +| `traefik..frontend.headers.customResponseHeaders=EXPR` | Same as `traefik.frontend.headers.customResponseHeaders` | + +#### Security Headers + +| Label | Description | +|-------------------------------------------------------------------------|--------------------------------------------------------------| +| `traefik..frontend.headers.allowedHosts=EXPR` | Same as `traefik.frontend.headers.allowedHosts` | +| `traefik..frontend.headers.browserXSSFilter=true` | Same as `traefik.frontend.headers.browserXSSFilter` | +| `traefik..frontend.headers.contentSecurityPolicy=VALUE` | Same as `traefik.frontend.headers.contentSecurityPolicy` | +| `traefik..frontend.headers.contentTypeNosniff=true` | Same as `traefik.frontend.headers.contentTypeNosniff` | +| `traefik..frontend.headers.customBrowserXSSValue=VALUE` | Same as `traefik.frontend.headers.customBrowserXSSValue` | +| `traefik..frontend.headers.customFrameOptionsValue=VALUE` | Same as `traefik.frontend.headers.customFrameOptionsValue` | +| `traefik..frontend.headers.forceSTSHeader=false` | Same as `traefik.frontend.headers.forceSTSHeader` | +| `traefik..frontend.headers.frameDeny=false` | Same as `traefik.frontend.headers.frameDeny` | +| `traefik..frontend.headers.hostsProxyHeaders=EXPR` | Same as `traefik.frontend.headers.hostsProxyHeaders` | +| `traefik..frontend.headers.isDevelopment=false` | Same as `traefik.frontend.headers.isDevelopment` | +| `traefik..frontend.headers.publicKey=VALUE` | Same as `traefik.frontend.headers.publicKey` | +| `traefik..frontend.headers.referrerPolicy=VALUE` | Same as `traefik.frontend.headers.referrerPolicy` | +| `traefik..frontend.headers.SSLRedirect=true` | Same as `traefik.frontend.headers.SSLRedirect` | +| `traefik..frontend.headers.SSLTemporaryRedirect=true` | Same as `traefik.frontend.headers.SSLTemporaryRedirect` | +| `traefik..frontend.headers.SSLHost=HOST` | Same as `traefik.frontend.headers.SSLHost` | +| `traefik..frontend.headers.SSLForceHost=true` | Same as `traefik.frontend.headers.SSLForceHost` | +| `traefik..frontend.headers.SSLProxyHeaders=EXPR` | Same as `traefik.frontend.headers.SSLProxyHeaders=EXPR` | +| `traefik..frontend.headers.STSSeconds=315360000` | Same as `traefik.frontend.headers.STSSeconds=315360000` | +| `traefik..frontend.headers.STSIncludeSubdomains=true` | Same as `traefik.frontend.headers.STSIncludeSubdomains=true` | +| `traefik..frontend.headers.STSPreload=true` | Same as `traefik.frontend.headers.STSPreload=true` | diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 0dda22870..b1ca37196 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -127,7 +127,13 @@ This will give more flexibility in cloud/dynamic environments. Traefik automatically requests endpoint information based on the service provided in the ingress spec. Although traefik will connect directly to the endpoints (pods), it still checks the service port to see if TLS communication is required. -If the service port defined in the ingress spec is 443, then the backend communication protocol is assumed to be TLS, and will connect via TLS automatically. + +There are 2 ways to configure Traefik to use https to communicate with backend pods: + +1. If the service port defined in the ingress spec is 443 (note that you can still use `targetPort` to use a different port on your pod). +2. If the service port defined in the ingress spec has a name that starts with `https` (such as `https-api`, `https-web` or just `https`). + +If either of those configuration options exist, then the backend communication protocol is assumed to be TLS, and will connect via TLS automatically. !!! note Please note that by enabling TLS communication between traefik and your pods, you will have to have trusted certificates that have the proper trust chain and IP subject name. diff --git a/docs/configuration/backends/rancher.md b/docs/configuration/backends/rancher.md index 792fe3164..9a86083e8 100644 --- a/docs/configuration/backends/rancher.md +++ b/docs/configuration/backends/rancher.md @@ -166,10 +166,10 @@ Labels can be used on task containers to override default behavior: | `traefik.frontend.auth.basic=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` (DEPRECATED). | | `traefik.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.basic.users=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` . | -| `traefik.frontend.auth.basic.usersfile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | +| `traefik.frontend.auth.basic.usersFile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.digest.users=EXPR` | Sets the digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. | -| `traefik.frontend.auth.digest.usersfile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | +| `traefik.frontend.auth.digest.usersFile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.forward.address=https://example.com` | Sets the URL of the authentication server. | | `traefik.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. | | `traefik.frontend.auth.forward.tls.caOptional=true` | Checks the certificates if present but do not force to be signed by a specified Certificate Authority (CA). | @@ -249,10 +249,10 @@ Segment labels override the default behavior. | `traefik..frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | | `traefik..frontend.auth.basic.removeHeader=true` | Same as `traefik.frontend.auth.basic.removeHeader` | | `traefik..frontend.auth.basic.users=EXPR` | Same as `traefik.frontend.auth.basic.users` | -| `traefik..frontend.auth.basic.usersfile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersfile` | +| `traefik..frontend.auth.basic.usersFile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersFile` | | `traefik..frontend.auth.digest.removeHeader=true` | Same as `traefik.frontend.auth.digest.removeHeader` | | `traefik..frontend.auth.digest.users=EXPR` | Same as `traefik.frontend.auth.digest.users` | -| `traefik..frontend.auth.digest.usersfile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersfile` | +| `traefik..frontend.auth.digest.usersFile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersFile` | | `traefik..frontend.auth.forward.address=https://example.com` | Same as `traefik.frontend.auth.forward.address` | | `traefik..frontend.auth.forward.tls.ca=/path/ca.pem` | Same as `traefik.frontend.auth.forward.tls.ca` | | `traefik..frontend.auth.forward.tls.caOptional=true` | Same as `traefik.frontend.auth.forward.tls.caOptional` | diff --git a/docs/index.md b/docs/index.md index 2afd6b199..50c180572 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/containous/traefik)](https://goreportcard.com/report/github.com/containous/traefik) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/containous/traefik/blob/master/LICENSE.md) [![Join the chat at https://slack.traefik.io](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://slack.traefik.io) -[![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy) +[![Twitter](https://img.shields.io/twitter/follow/traefik.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefik) Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. @@ -86,6 +86,10 @@ services: - /var/run/docker.sock:/var/run/docker.sock # So that Traefik can listen to the Docker events ``` +!!! warning + Enabling the Web UI with the `--api` flag might exposes configuration elements. You can read more about this on the [API/Dashboard's Security section](/configuration/api#security). + + **That's it. Now you can launch Træfik!** Start your `reverse-proxy` with the following command: @@ -199,3 +203,19 @@ Using the tiny Docker image: ```shell docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik ``` + +## Security + +### Security Advisories + +We strongly advise you to join our mailing list to be aware of the latest announcements from our security team. You can subscribe sending a mail to security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security). + +### CVE + +Reported vulnerabilities can be found on +[cve.mitre.org](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=traefik). + +### Report a Vulnerability + +We want to keep Træfik safe for everyone. +If you've discovered a security vulnerability in Træfik, we appreciate your help in disclosing it to us in a responsible manner, using [this form](https://security.traefik.io). \ No newline at end of file diff --git a/docs/user-guide/docker-and-lets-encrypt.md b/docs/user-guide/docker-and-lets-encrypt.md index af7ebe93a..9c3c95b49 100644 --- a/docs/user-guide/docker-and-lets-encrypt.md +++ b/docs/user-guide/docker-and-lets-encrypt.md @@ -1,4 +1,4 @@ -# Docker & Traefik +# Let's Encrypt & Docker In this use case, we want to use Træfik as a _layer-7_ load balancer with SSL termination for a set of micro-services used to run a web application. @@ -8,7 +8,7 @@ In addition, we want to use Let's Encrypt to automatically generate and renew SS ## Setting Up -In order for this to work, you'll need a server with a public IP address, with Docker installed on it. +In order for this to work, you'll need a server with a public IP address, with Docker and docker-compose installed on it. In this example, we're using the fictitious domain _my-awesome-app.org_. diff --git a/examples/acme/manage_acme_docker_environment.sh b/examples/acme/manage_acme_docker_environment.sh index e007665b5..6200d041d 100755 --- a/examples/acme/manage_acme_docker_environment.sh +++ b/examples/acme/manage_acme_docker_environment.sh @@ -50,7 +50,7 @@ start_boulder() { # Script usage show_usage() { echo - echo "USAGE : manage_acme_docker_environment.sh [--start|--stop|--restart]" + echo "USAGE : manage_acme_docker_environment.sh [--dev|--start|--stop|--restart]" echo } diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 4e776efe7..e3156d2ff 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -676,3 +676,49 @@ func (s *ConsulCatalogSuite) TestMaintenanceMode(c *check.C) { err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) c.Assert(err, checker.IsNil) } + +func (s *ConsulCatalogSuite) TestMultipleFrontendRule(c *check.C) { + cmd, display := s.traefikCmd( + withConfigFile("fixtures/consul_catalog/simple.toml"), + "--consulCatalog", + "--consulCatalog.endpoint="+s.consulIP+":8500", + "--consulCatalog.domain=consul.localhost") + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // Wait for Traefik to turn ready. + err = try.GetRequest("http://127.0.0.1:8000/", 2*time.Second, try.StatusCodeIs(http.StatusNotFound)) + c.Assert(err, checker.IsNil) + + whoami := s.composeProject.Container(c, "whoami1") + + err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, + []string{ + "traefik.frontends.service1.rule=Host:whoami1.consul.localhost", + "traefik.frontends.service2.rule=Host:whoami2.consul.localhost", + }) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + + err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "whoami1.consul.localhost" + + err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "whoami2.consul.localhost" + + err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) +} diff --git a/integration/fixtures/https/https_redirect.toml b/integration/fixtures/https/https_redirect.toml index 498d14f89..fc0cdf67c 100644 --- a/integration/fixtures/https/https_redirect.toml +++ b/integration/fixtures/https/https_redirect.toml @@ -22,15 +22,49 @@ defaultEntryPoints = ["http", "https"] weight = 1 [frontends] + [frontends.frontend1] backend = "backend1" [frontends.frontend1.routes.test_1] rule = "Host: example.com; PathPrefixStrip: /api" - [frontends.frontend2] + [frontends.frontend2] backend = "backend1" [frontends.frontend2.routes.test_1] - rule = "Host: test.com; AddPrefix: /foo" + rule = "Host: example2.com; PathPrefixStrip: /api/" + [frontends.frontend3] backend = "backend1" [frontends.frontend3.routes.test_1] + rule = "Host: test.com; AddPrefix: /foo" + [frontends.frontend4] + backend = "backend1" + [frontends.frontend4.routes.test_1] + rule = "Host: test2.com; AddPrefix: /foo/" + + [frontends.frontend5] + backend = "backend1" + [frontends.frontend5.routes.test_1] rule = "Host: foo.com; PathPrefixStripRegex: /{id:[a-z]+}" + [frontends.frontend6] + backend = "backend1" + [frontends.frontend6.routes.test_1] + rule = "Host: foo2.com; PathPrefixStripRegex: /{id:[a-z]+}/" + + [frontends.frontend7] + backend = "backend1" + [frontends.frontend7.routes.test_1] + rule = "Host: bar.com; ReplacePathRegex: /api /" + [frontends.frontend8] + backend = "backend1" + [frontends.frontend8.routes.test_1] + rule = "Host: bar2.com; ReplacePathRegex: /api/ /" + + [frontends.frontend9] + backend = "backend1" + [frontends.frontend9.routes.test_1] + rule = "Host: pow.com; ReplacePath: /api" + [frontends.frontend10] + backend = "backend1" + [frontends.frontend10.routes.test_1] + rule = "Host: pow2.com; ReplacePath: /api/" + diff --git a/integration/https_test.go b/integration/https_test.go index a85fab003..bf900b6b6 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -3,6 +3,7 @@ package integration import ( "bytes" "crypto/tls" + "fmt" "net" "net/http" "net/http/httptest" @@ -743,7 +744,7 @@ func (s *HTTPSSuite) TestEntrypointHttpsRedirectAndPathModification(c *check.C) defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers", 500*time.Millisecond, try.BodyContains("Host: example.com")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1000*time.Millisecond, try.BodyContains("Host: example.com")) c.Assert(err, checker.IsNil) client := &http.Client{ @@ -753,115 +754,82 @@ func (s *HTTPSSuite) TestEntrypointHttpsRedirectAndPathModification(c *check.C) } testCases := []struct { - desc string - host string - sourceURL string - expectedURL string + desc string + hosts []string + path string }{ { - desc: "Stripped URL redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api", - expectedURL: "https://example.com:8443/api", + desc: "Stripped URL redirect", + hosts: []string{"example.com", "foo.com", "bar.com"}, + path: "/api", }, { - desc: "Stripped URL with trailing slash redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api/", - expectedURL: "https://example.com:8443/api/", + desc: "Stripped URL with trailing slash redirect", + hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"}, + path: "/api/", }, { - desc: "Stripped URL with double trailing slash redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api//", - expectedURL: "https://example.com:8443/api//", + desc: "Stripped URL with double trailing slash redirect", + hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"}, + path: "/api//", }, { - desc: "Stripped URL with path redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api/bacon", - expectedURL: "https://example.com:8443/api/bacon", + desc: "Stripped URL with path redirect", + hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"}, + path: "/api/bacon", }, { - desc: "Stripped URL with path and trailing slash redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api/bacon/", - expectedURL: "https://example.com:8443/api/bacon/", + desc: "Stripped URL with path and trailing slash redirect", + hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"}, + path: "/api/bacon/", }, { - desc: "Stripped URL with path and double trailing slash redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api/bacon//", - expectedURL: "https://example.com:8443/api/bacon//", + desc: "Stripped URL with path and double trailing slash redirect", + hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"}, + path: "/api/bacon//", }, { - desc: "Root Path with redirect", - host: "test.com", - sourceURL: "http://127.0.0.1:8888/", - expectedURL: "https://test.com:8443/", + desc: "Root Path with redirect", + hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"}, + path: "/", }, { - desc: "Root Path with double trailing slash redirect", - host: "test.com", - sourceURL: "http://127.0.0.1:8888//", - expectedURL: "https://test.com:8443//", + desc: "Root Path with double trailing slash redirect", + hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"}, + path: "//", }, { - desc: "AddPrefix with redirect", - host: "test.com", - sourceURL: "http://127.0.0.1:8888/wtf", - expectedURL: "https://test.com:8443/wtf", + desc: "Path modify with redirect", + hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"}, + path: "/wtf", }, { - desc: "AddPrefix with trailing slash redirect", - host: "test.com", - sourceURL: "http://127.0.0.1:8888/wtf/", - expectedURL: "https://test.com:8443/wtf/", + desc: "Path modify with trailing slash redirect", + hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"}, + path: "/wtf/", }, { - desc: "AddPrefix with matching path segment redirect", - host: "test.com", - sourceURL: "http://127.0.0.1:8888/wtf/foo", - expectedURL: "https://test.com:8443/wtf/foo", - }, - { - desc: "Stripped URL Regex redirect", - host: "foo.com", - sourceURL: "http://127.0.0.1:8888/api", - expectedURL: "https://foo.com:8443/api", - }, - { - desc: "Stripped URL Regex with trailing slash redirect", - host: "foo.com", - sourceURL: "http://127.0.0.1:8888/api/", - expectedURL: "https://foo.com:8443/api/", - }, - { - desc: "Stripped URL Regex with path redirect", - host: "foo.com", - sourceURL: "http://127.0.0.1:8888/api/bacon", - expectedURL: "https://foo.com:8443/api/bacon", - }, - { - desc: "Stripped URL Regex with path and trailing slash redirect", - host: "foo.com", - sourceURL: "http://127.0.0.1:8888/api/bacon/", - expectedURL: "https://foo.com:8443/api/bacon/", + desc: "Path modify with matching path segment redirect", + hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"}, + path: "/wtf/foo", }, } for _, test := range testCases { - test := test + sourceURL := fmt.Sprintf("http://127.0.0.1:8888%s", test.path) + for _, host := range test.hosts { + req, err := http.NewRequest("GET", sourceURL, nil) + c.Assert(err, checker.IsNil) + req.Host = host - req, err := http.NewRequest("GET", test.sourceURL, nil) - c.Assert(err, checker.IsNil) - req.Host = test.host + resp, err := client.Do(req) + c.Assert(err, checker.IsNil) + defer resp.Body.Close() - resp, err := client.Do(req) - c.Assert(err, checker.IsNil) - defer resp.Body.Close() + location := resp.Header.Get("Location") + expected := fmt.Sprintf("https://%s:8443%s", host, test.path) - location := resp.Header.Get("Location") - c.Assert(location, checker.Equals, test.expectedURL) + c.Assert(location, checker.Equals, expected) + } } } diff --git a/middlewares/redirect/redirect.go b/middlewares/redirect/redirect.go index 1b1dfe7dd..506d7db3f 100644 --- a/middlewares/redirect/redirect.go +++ b/middlewares/redirect/redirect.go @@ -88,19 +88,7 @@ func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http if stripPrefix, stripPrefixOk := req.Context().Value(middlewares.StripPrefixKey).(string); stripPrefixOk { if len(stripPrefix) > 0 { - tempPath := parsedURL.Path parsedURL.Path = stripPrefix - if len(tempPath) > 0 && tempPath != "/" { - parsedURL.Path = stripPrefix + tempPath - } - - if trailingSlash, trailingSlashOk := req.Context().Value(middlewares.StripPrefixSlashKey).(bool); trailingSlashOk { - if trailingSlash { - if !strings.HasSuffix(parsedURL.Path, "/") { - parsedURL.Path = fmt.Sprintf("%s/", parsedURL.Path) - } - } - } } } @@ -110,6 +98,12 @@ func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http } } + if replacePath, replacePathOk := req.Context().Value(middlewares.ReplacePathKey).(string); replacePathOk { + if len(replacePath) > 0 { + parsedURL.Path = replacePath + } + } + if newURL != oldURL { handler := &moveHandler{location: parsedURL, permanent: h.permanent} handler.ServeHTTP(rw, req) diff --git a/middlewares/replace_path.go b/middlewares/replace_path.go index 40211c771..d5bd03eac 100644 --- a/middlewares/replace_path.go +++ b/middlewares/replace_path.go @@ -1,11 +1,17 @@ package middlewares import ( + "context" "net/http" ) -// ReplacedPathHeader is the default header to set the old path to -const ReplacedPathHeader = "X-Replaced-Path" +const ( + // ReplacePathKey is the key within the request context used to + // store the replaced path + ReplacePathKey key = "ReplacePath" + // ReplacedPathHeader is the default header to set the old path to + ReplacedPathHeader = "X-Replaced-Path" +) // ReplacePath is a middleware used to replace the path of a URL request type ReplacePath struct { @@ -14,6 +20,7 @@ type ReplacePath struct { } func (s *ReplacePath) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r = r.WithContext(context.WithValue(r.Context(), ReplacePathKey, r.URL.Path)) r.Header.Add(ReplacedPathHeader, r.URL.Path) r.URL.Path = s.Path r.RequestURI = r.URL.RequestURI() diff --git a/middlewares/replace_path_regex.go b/middlewares/replace_path_regex.go index 4d97c0de5..d753e86c0 100644 --- a/middlewares/replace_path_regex.go +++ b/middlewares/replace_path_regex.go @@ -1,6 +1,7 @@ package middlewares import ( + "context" "net/http" "regexp" "strings" @@ -30,6 +31,7 @@ func NewReplacePathRegexHandler(regex string, replacement string, handler http.H func (s *ReplacePathRegex) ServeHTTP(w http.ResponseWriter, r *http.Request) { if s.Regexp != nil && len(s.Replacement) > 0 && s.Regexp.MatchString(r.URL.Path) { + r = r.WithContext(context.WithValue(r.Context(), ReplacePathKey, r.URL.Path)) r.Header.Add(ReplacedPathHeader, r.URL.Path) r.URL.Path = s.Regexp.ReplaceAllString(r.URL.Path, s.Replacement) r.RequestURI = r.URL.RequestURI() diff --git a/middlewares/stripPrefix.go b/middlewares/stripPrefix.go index f5295d94c..222eb33cd 100644 --- a/middlewares/stripPrefix.go +++ b/middlewares/stripPrefix.go @@ -10,9 +10,6 @@ const ( // StripPrefixKey is the key within the request context used to // store the stripped prefix StripPrefixKey key = "StripPrefix" - // StripPrefixSlashKey is the key within the request context used to - // store the stripped slash - StripPrefixSlashKey key = "StripPrefixSlash" // ForwardedPrefixHeader is the default header to set prefix ForwardedPrefixHeader = "X-Forwarded-Prefix" ) @@ -26,21 +23,20 @@ type StripPrefix struct { func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) { for _, prefix := range s.Prefixes { if strings.HasPrefix(r.URL.Path, prefix) { - trailingSlash := r.URL.Path == prefix+"/" + rawReqPath := r.URL.Path r.URL.Path = stripPrefix(r.URL.Path, prefix) if r.URL.RawPath != "" { r.URL.RawPath = stripPrefix(r.URL.RawPath, prefix) } - s.serveRequest(w, r, strings.TrimSpace(prefix), trailingSlash) + s.serveRequest(w, r, strings.TrimSpace(prefix), rawReqPath) return } } http.NotFound(w, r) } -func (s *StripPrefix) serveRequest(w http.ResponseWriter, r *http.Request, prefix string, trailingSlash bool) { - r = r.WithContext(context.WithValue(r.Context(), StripPrefixSlashKey, trailingSlash)) - r = r.WithContext(context.WithValue(r.Context(), StripPrefixKey, prefix)) +func (s *StripPrefix) serveRequest(w http.ResponseWriter, r *http.Request, prefix string, rawReqPath string) { + r = r.WithContext(context.WithValue(r.Context(), StripPrefixKey, rawReqPath)) r.Header.Add(ForwardedPrefixHeader, prefix) r.RequestURI = r.URL.RequestURI() s.Handler.ServeHTTP(w, r) diff --git a/middlewares/stripPrefixRegex.go b/middlewares/stripPrefixRegex.go index d86733dd6..c249f0fb2 100644 --- a/middlewares/stripPrefixRegex.go +++ b/middlewares/stripPrefixRegex.go @@ -39,16 +39,14 @@ func (s *StripPrefixRegex) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Error("Error in stripPrefix middleware", err) return } - - trailingSlash := r.URL.Path == prefix.Path+"/" + rawReqPath := r.URL.Path r.URL.Path = r.URL.Path[len(prefix.Path):] if r.URL.RawPath != "" { r.URL.RawPath = r.URL.RawPath[len(prefix.Path):] } - r = r.WithContext(context.WithValue(r.Context(), StripPrefixSlashKey, trailingSlash)) - r = r.WithContext(context.WithValue(r.Context(), StripPrefixKey, prefix.Path)) + r = r.WithContext(context.WithValue(r.Context(), StripPrefixKey, rawReqPath)) r.Header.Add(ForwardedPrefixHeader, prefix.Path) - r.RequestURI = r.URL.RequestURI() + r.RequestURI = ensureLeadingSlash(r.URL.RequestURI()) s.Handler.ServeHTTP(w, r) return } diff --git a/mkdocs.yml b/mkdocs.yml index b1cdede42..35e01a502 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,14 +16,11 @@ theme: include_sidebar: true favicon: img/traefik.icon.png logo: img/traefik.logo.png - palette: - primary: 'blue' - accent: 'light blue' - feature: - tabs: false palette: primary: 'cyan' accent: 'cyan' + feature: + tabs: false i18n: prev: 'Previous' next: 'Next' @@ -45,7 +42,7 @@ google_analytics: # - type: 'slack' # link: 'https://slack.traefik.io' # - type: 'twitter' -# link: 'https://twitter.com/traefikproxy' +# link: 'https://twitter.com/traefik' extra_css: - theme/styles/extra.css diff --git a/provider/acme/provider.go b/provider/acme/provider.go index 830bafa25..9db5e0657 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -62,6 +62,8 @@ type Provider struct { clientMutex sync.Mutex configFromListenerChan chan types.Configuration pool *safe.Pool + resolvingDomains map[string]struct{} + resolvingDomainsMutex sync.RWMutex } // Certificate is a struct which contains all data needed from an ACME certificate @@ -144,6 +146,9 @@ func (p *Provider) Init(_ types.Constraints) error { return fmt.Errorf("unable to get ACME certificates : %v", err) } + // Init the currently resolved domain map + p.resolvingDomains = make(map[string]struct{}) + return nil } @@ -309,6 +314,12 @@ func (p *Provider) initAccount() (*Account, error) { return nil, err } } + + // Set the KeyType if not already defined in the account + if len(p.account.KeyType) == 0 { + p.account.KeyType = GetKeyType(p.KeyType) + } + return p.account, nil } @@ -367,6 +378,9 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati return nil, nil } + p.addResolvingDomains(uncheckedDomains) + defer p.removeResolvingDomains(uncheckedDomains) + log.Debugf("Loading ACME certificates %+v...", uncheckedDomains) client, err := p.getClient() @@ -404,6 +418,24 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati return certificate, nil } +func (p *Provider) removeResolvingDomains(resolvingDomains []string) { + p.resolvingDomainsMutex.Lock() + defer p.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + delete(p.resolvingDomains, domain) + } +} + +func (p *Provider) addResolvingDomains(resolvingDomains []string) { + p.resolvingDomainsMutex.Lock() + defer p.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + p.resolvingDomains[domain] = struct{}{} + } +} + func (p *Provider) useCertificateWithRetry(domains []string) bool { // Check if we can use the retry mechanism only if we use the DNS Challenge and if is there are at least 2 domains to check if p.DNSChallenge != nil && len(domains) > 1 { @@ -630,6 +662,9 @@ func (p *Provider) renewCertificates() { // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurationDomains bool) []string { + p.resolvingDomainsMutex.RLock() + defer p.resolvingDomainsMutex.RUnlock() + log.Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck) allDomains := p.certificateStore.GetAllDomains() @@ -639,6 +674,11 @@ func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurati allDomains = append(allDomains, strings.Join(certificate.Domain.ToStrArray(), ",")) } + // Get currently resolved domains + for domain := range p.resolvingDomains { + allDomains = append(allDomains, domain) + } + // Get Configuration Domains if checkConfigurationDomains { for i := 0; i < len(p.Domains); i++ { @@ -658,7 +698,7 @@ func searchUncheckedDomains(domainsToCheck []string, existentDomains []string) [ } if len(uncheckedDomains) == 0 { - log.Debugf("No ACME certificate to generate for domains %q.", domainsToCheck) + log.Debugf("No ACME certificate generation required for domains %q.", domainsToCheck) } else { log.Debugf("Domains %q need ACME certificates generation for domains %q.", domainsToCheck, strings.Join(uncheckedDomains, ",")) } diff --git a/provider/acme/provider_test.go b/provider/acme/provider_test.go index abbe34a0f..b5287ba4e 100644 --- a/provider/acme/provider_test.go +++ b/provider/acme/provider_test.go @@ -8,6 +8,7 @@ import ( traefiktls "github.com/containous/traefik/tls" "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" + "github.com/xenolf/lego/acme" ) func TestGetUncheckedCertificates(t *testing.T) { @@ -27,6 +28,7 @@ func TestGetUncheckedCertificates(t *testing.T) { desc string dynamicCerts *safe.Safe staticCerts *safe.Safe + resolvingDomains map[string]struct{} acmeCertificates []*Certificate domains []string expectedDomains []string @@ -139,6 +141,40 @@ func TestGetUncheckedCertificates(t *testing.T) { }, expectedDomains: []string{"traefik.wtf"}, }, + { + desc: "all domains already managed by ACME", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + resolvingDomains: map[string]struct{}{ + "traefik.wtf": {}, + "foo.traefik.wtf": {}, + }, + expectedDomains: []string{}, + }, + { + desc: "one domain already managed by ACME", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + resolvingDomains: map[string]struct{}{ + "traefik.wtf": {}, + }, + expectedDomains: []string{"foo.traefik.wtf"}, + }, + { + desc: "wildcard domain already managed by ACME checks the domains", + domains: []string{"bar.traefik.wtf", "foo.traefik.wtf"}, + resolvingDomains: map[string]struct{}{ + "*.traefik.wtf": {}, + }, + expectedDomains: []string{}, + }, + { + desc: "wildcard domain already managed by ACME checks domains and another domain checks one other domain, one domain still unchecked", + domains: []string{"traefik.wtf", "bar.traefik.wtf", "foo.traefik.wtf", "acme.wtf"}, + resolvingDomains: map[string]struct{}{ + "*.traefik.wtf": {}, + "traefik.wtf": {}, + }, + expectedDomains: []string{"acme.wtf"}, + }, } for _, test := range testCases { @@ -146,12 +182,17 @@ func TestGetUncheckedCertificates(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() + if test.resolvingDomains == nil { + test.resolvingDomains = make(map[string]struct{}) + } + acmeProvider := Provider{ certificateStore: &traefiktls.CertificateStore{ DynamicCerts: test.dynamicCerts, StaticCerts: test.staticCerts, }, - certificates: test.acmeCertificates, + certificates: test.acmeCertificates, + resolvingDomains: test.resolvingDomains, } domains := acmeProvider.getUncheckedDomains(test.domains, false) @@ -562,3 +603,82 @@ func TestUseBackOffToObtainCertificate(t *testing.T) { }) } } + +func TestInitAccount(t *testing.T) { + testCases := []struct { + desc string + account *Account + email string + keyType string + expectedAccount *Account + }{ + { + desc: "Existing account with all information", + account: &Account{ + Email: "foo@foo.net", + KeyType: acme.EC256, + }, + expectedAccount: &Account{ + Email: "foo@foo.net", + KeyType: acme.EC256, + }, + }, + { + desc: "Account nil", + email: "foo@foo.net", + keyType: "EC256", + expectedAccount: &Account{ + Email: "foo@foo.net", + KeyType: acme.EC256, + }, + }, + { + desc: "Existing account with no email", + account: &Account{ + KeyType: acme.RSA4096, + }, + email: "foo@foo.net", + keyType: "EC256", + expectedAccount: &Account{ + Email: "foo@foo.net", + KeyType: acme.EC256, + }, + }, + { + desc: "Existing account with no key type", + account: &Account{ + Email: "foo@foo.net", + }, + email: "bar@foo.net", + keyType: "EC256", + expectedAccount: &Account{ + Email: "foo@foo.net", + KeyType: acme.EC256, + }, + }, + { + desc: "Existing account and provider with no key type", + account: &Account{ + Email: "foo@foo.net", + }, + email: "bar@foo.net", + expectedAccount: &Account{ + Email: "foo@foo.net", + KeyType: acme.RSA4096, + }, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + acmeProvider := Provider{account: test.account, Configuration: &Configuration{Email: test.email, KeyType: test.keyType}} + + actualAccount, err := acmeProvider.initAccount() + assert.Nil(t, err, "Init account in error") + assert.Equal(t, test.expectedAccount.Email, actualAccount.Email, "unexpected email account") + assert.Equal(t, test.expectedAccount.KeyType, actualAccount.KeyType, "unexpected keyType account") + }) + } +} diff --git a/provider/consulcatalog/config.go b/provider/consulcatalog/config.go index 96466ed9d..6db4045b9 100644 --- a/provider/consulcatalog/config.go +++ b/provider/consulcatalog/config.go @@ -55,7 +55,7 @@ func (p *Provider) buildConfiguration(catalog []catalogUpdate) *types.Configurat var services []*serviceUpdate for _, info := range catalog { if len(info.Nodes) > 0 { - services = append(services, info.Service) + services = append(services, p.generateFrontends(info.Service)...) allNodes = append(allNodes, info.Nodes...) } } @@ -135,6 +135,9 @@ func (p *Provider) setupFrontEndRuleTemplate() { // Specific functions func getServiceBackendName(service *serviceUpdate) string { + if service.ParentServiceName != "" { + return strings.ToLower(service.ParentServiceName) + } return strings.ToLower(service.ServiceName) } diff --git a/provider/consulcatalog/config_test.go b/provider/consulcatalog/config_test.go index 57b4294b9..441bf9245 100644 --- a/provider/consulcatalog/config_test.go +++ b/provider/consulcatalog/config_test.go @@ -120,6 +120,80 @@ func TestProviderBuildConfiguration(t *testing.T) { }, }, }, + { + desc: "Should build config which contains three frontends and one backend", + nodes: []catalogUpdate{ + { + Service: &serviceUpdate{ + ServiceName: "test", + Attributes: []string{ + "random.foo=bar", + label.Prefix + "frontend.rule=Host:A", + label.Prefix + "frontends.test1.rule=Host:B", + label.Prefix + "frontends.test2.rule=Host:C", + }, + }, + Nodes: []*api.ServiceEntry{ + { + Service: &api.AgentService{ + Service: "test", + Address: "127.0.0.1", + Port: 80, + Tags: []string{ + "random.foo=bar", + }, + }, + Node: &api.Node{ + Node: "localhost", + Address: "127.0.0.1", + }, + }, + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-test": { + Backend: "backend-test", + PassHostHeader: true, + Routes: map[string]types.Route{ + "route-host-test": { + Rule: "Host:A", + }, + }, + EntryPoints: []string{}, + }, + "frontend-test-test1": { + Backend: "backend-test", + PassHostHeader: true, + Routes: map[string]types.Route{ + "route-host-test-test1": { + Rule: "Host:B", + }, + }, + EntryPoints: []string{}, + }, + "frontend-test-test2": { + Backend: "backend-test", + PassHostHeader: true, + Routes: map[string]types.Route{ + "route-host-test-test2": { + Rule: "Host:C", + }, + }, + EntryPoints: []string{}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test": { + Servers: map[string]types.Server{ + "test-0-O0Tnh-SwzY69M6SurTKP3wNKkzI": { + URL: "http://127.0.0.1:80", + Weight: 1, + }, + }, + }, + }, + }, { desc: "Should build config with a basic auth with a backward compatibility", nodes: []catalogUpdate{ diff --git a/provider/consulcatalog/consul_catalog.go b/provider/consulcatalog/consul_catalog.go index 312fd6da5..297096206 100644 --- a/provider/consulcatalog/consul_catalog.go +++ b/provider/consulcatalog/consul_catalog.go @@ -50,9 +50,15 @@ type Service struct { } type serviceUpdate struct { - ServiceName string - Attributes []string - TraefikLabels map[string]string + ServiceName string + ParentServiceName string + Attributes []string + TraefikLabels map[string]string +} + +type frontendSegment struct { + Name string + Labels map[string]string } type catalogUpdate struct { @@ -560,3 +566,52 @@ func (p *Provider) getConstraintTags(tags []string) []string { return values } + +func (p *Provider) generateFrontends(service *serviceUpdate) []*serviceUpdate { + frontends := make([]*serviceUpdate, 0) + // to support .frontend.xxx + frontends = append(frontends, &serviceUpdate{ + ServiceName: service.ServiceName, + ParentServiceName: service.ServiceName, + Attributes: service.Attributes, + TraefikLabels: service.TraefikLabels, + }) + + // loop over children of .frontends.* + for _, frontend := range getSegments(p.Prefix+".frontends", p.Prefix, service.TraefikLabels) { + frontends = append(frontends, &serviceUpdate{ + ServiceName: service.ServiceName + "-" + frontend.Name, + ParentServiceName: service.ServiceName, + Attributes: service.Attributes, + TraefikLabels: frontend.Labels, + }) + } + + return frontends +} +func getSegments(path string, prefix string, tree map[string]string) []*frontendSegment { + segments := make([]*frontendSegment, 0) + // find segment names + segmentNames := make(map[string]bool) + for key := range tree { + if strings.HasPrefix(key, path+".") { + segmentNames[strings.SplitN(strings.TrimPrefix(key, path+"."), ".", 2)[0]] = true + } + } + + // get labels for each segment found + for segment := range segmentNames { + labels := make(map[string]string) + for key, value := range tree { + if strings.HasPrefix(key, path+"."+segment) { + labels[prefix+".frontend"+strings.TrimPrefix(key, path+"."+segment)] = value + } + } + segments = append(segments, &frontendSegment{ + Name: segment, + Labels: labels, + }) + } + + return segments +} diff --git a/provider/ecs/builder_test.go b/provider/ecs/builder_test.go new file mode 100644 index 000000000..8f1e313d1 --- /dev/null +++ b/provider/ecs/builder_test.go @@ -0,0 +1,80 @@ +package ecs + +import ( + "github.com/aws/aws-sdk-go/service/ecs" +) + +func instance(ops ...func(*ecsInstance)) ecsInstance { + e := &ecsInstance{ + containerDefinition: &ecs.ContainerDefinition{}, + } + + for _, op := range ops { + op(e) + } + + return *e +} + +func name(name string) func(*ecsInstance) { + return func(e *ecsInstance) { + e.Name = name + } +} + +func ID(ID string) func(*ecsInstance) { + return func(e *ecsInstance) { + e.ID = ID + } +} + +func iMachine(opts ...func(*machine)) func(*ecsInstance) { + return func(e *ecsInstance) { + e.machine = &machine{} + + for _, opt := range opts { + opt(e.machine) + } + } +} + +func mState(state string) func(*machine) { + return func(m *machine) { + m.state = state + } +} + +func mPrivateIP(ip string) func(*machine) { + return func(m *machine) { + m.privateIP = ip + } +} + +func mPorts(opts ...func(*portMapping)) func(*machine) { + return func(m *machine) { + for _, opt := range opts { + p := &portMapping{} + opt(p) + m.ports = append(m.ports, *p) + } + } +} + +func mPort(containerPort int32, hostPort int32) func(*portMapping) { + return func(pm *portMapping) { + pm.containerPort = int64(containerPort) + pm.hostPort = int64(hostPort) + } +} + +func labels(labels map[string]string) func(*ecsInstance) { + return func(c *ecsInstance) { + c.TraefikLabels = labels + } +} + +func dockerLabels(labels map[string]*string) func(*ecsInstance) { + return func(c *ecsInstance) { + c.containerDefinition.DockerLabels = labels + } +} diff --git a/provider/ecs/config.go b/provider/ecs/config.go index 00a497140..d7e6eef1f 100644 --- a/provider/ecs/config.go +++ b/provider/ecs/config.go @@ -1,6 +1,8 @@ package ecs import ( + "crypto/md5" + "encoding/hex" "fmt" "net" "strconv" @@ -17,18 +19,6 @@ import ( // buildConfiguration fills the config template with the given instances func (p *Provider) buildConfiguration(instances []ecsInstance) (*types.Configuration, error) { - services := make(map[string][]ecsInstance) - for _, instance := range instances { - backendName := getBackendName(instance) - if p.filterInstance(instance) { - if serviceInstances, ok := services[backendName]; ok { - services[backendName] = append(serviceInstances, instance) - } else { - services[backendName] = []ecsInstance{instance} - } - } - } - var ecsFuncMap = template.FuncMap{ // Backend functions "getHost": getHost, @@ -43,6 +33,7 @@ func (p *Provider) buildConfiguration(instances []ecsInstance) (*types.Configura // Frontend functions "filterFrontends": filterFrontends, "getFrontendRule": p.getFrontendRule, + "getFrontendName": p.getFrontendName, "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriority), @@ -56,6 +47,25 @@ func (p *Provider) buildConfiguration(instances []ecsInstance) (*types.Configura "getWhiteList": label.GetWhiteList, } + services := make(map[string][]ecsInstance) + for _, instance := range instances { + segmentProperties := label.ExtractTraefikLabels(instance.TraefikLabels) + + for segmentName, labels := range segmentProperties { + instance.SegmentLabels = labels + instance.SegmentName = segmentName + + backendName := getBackendName(instance) + if p.filterInstance(instance) { + if serviceInstances, ok := services[backendName]; ok { + services[backendName] = append(serviceInstances, instance) + } else { + services[backendName] = []ecsInstance{instance} + } + } + } + } + return p.GetConfiguration("templates/ecs.tmpl", ecsFuncMap, struct { Services map[string][]ecsInstance }{ @@ -101,25 +111,61 @@ func (p *Provider) filterInstance(i ecsInstance) bool { } func getBackendName(i ecsInstance) string { - if value := label.GetStringValue(i.TraefikLabels, label.TraefikBackend, ""); len(value) > 0 { - return value + if len(i.SegmentName) > 0 { + return getSegmentBackendName(i) } - return i.Name + + return getDefaultBackendName(i) +} + +func getSegmentBackendName(i ecsInstance) string { + if value := label.GetStringValue(i.SegmentLabels, label.TraefikBackend, ""); len(value) > 0 { + return provider.Normalize(i.Name + "-" + value) + } + + return provider.Normalize(i.Name + "-" + i.SegmentName) +} + +func getDefaultBackendName(i ecsInstance) string { + if value := label.GetStringValue(i.SegmentLabels, label.TraefikBackend, ""); len(value) != 0 { + return provider.Normalize(value) + } + + return provider.Normalize(i.Name) } func (p *Provider) getFrontendRule(i ecsInstance) string { - domain := label.GetStringValue(i.TraefikLabels, label.TraefikDomain, p.Domain) + if value := label.GetStringValue(i.SegmentLabels, label.TraefikFrontendRule, ""); len(value) != 0 { + return value + } + + domain := label.GetStringValue(i.SegmentLabels, label.TraefikDomain, p.Domain) defaultRule := "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + domain return label.GetStringValue(i.TraefikLabels, label.TraefikFrontendRule, defaultRule) } +func (p *Provider) getFrontendName(instance ecsInstance) string { + name := getBackendName(instance) + if len(instance.SegmentName) > 0 { + name = instance.SegmentName + "-" + name + } + + return provider.Normalize(name) +} + func getHost(i ecsInstance) string { return i.machine.privateIP } func getPort(i ecsInstance) string { - if value := label.GetStringValue(i.TraefikLabels, label.TraefikPort, ""); len(value) > 0 { + value := label.GetStringValue(i.SegmentLabels, label.TraefikPort, "") + + if len(value) == 0 { + value = label.GetStringValue(i.TraefikLabels, label.TraefikPort, "") + } + + if len(value) > 0 { port, err := strconv.ParseInt(value, 10, 64) if err == nil { for _, mapping := range i.machine.ports { @@ -138,6 +184,10 @@ func filterFrontends(instances []ecsInstance) []ecsInstance { return fun.Filter(func(i ecsInstance) bool { backendName := getBackendName(i) + if len(i.SegmentName) > 0 { + backendName = backendName + "-" + i.SegmentName + } + _, found := byName[backendName] if !found { byName[backendName] = struct{}{} @@ -154,14 +204,21 @@ func getServers(instances []ecsInstance) map[string]types.Server { servers = make(map[string]types.Server) } - protocol := label.GetStringValue(instance.TraefikLabels, label.TraefikProtocol, label.DefaultProtocol) + protocol := label.GetStringValue(instance.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol) host := getHost(instance) port := getPort(instance) - serverName := provider.Normalize(fmt.Sprintf("server-%s-%s", instance.Name, instance.ID)) + serverURL := fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(host, port)) + serverName := getServerName(instance, serverURL) + + if _, exist := servers[serverName]; exist { + log.Debugf("Skipping server %q with the same URL.", serverName) + continue + } + servers[serverName] = types.Server{ - URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(host, port)), - Weight: label.GetIntValue(instance.TraefikLabels, label.TraefikWeight, label.DefaultWeight), + URL: serverURL, + Weight: label.GetIntValue(instance.SegmentLabels, label.TraefikWeight, label.DefaultWeight), } } @@ -171,3 +228,18 @@ func getServers(instances []ecsInstance) map[string]types.Server { func isEnabled(i ecsInstance, exposedByDefault bool) bool { return label.GetBoolValue(i.TraefikLabels, label.TraefikEnable, exposedByDefault) } + +func getServerName(instance ecsInstance, url string) string { + hash := md5.New() + _, err := hash.Write([]byte(url)) + if err != nil { + // Impossible case + log.Errorf("Fail to hash server URL %q", url) + } + + if len(instance.SegmentName) > 0 { + return provider.Normalize(fmt.Sprintf("server-%s-%s-%s", instance.Name, instance.ID, hex.EncodeToString(hash.Sum(nil)))) + } + + return provider.Normalize(fmt.Sprintf("server-%s-%s", instance.Name, instance.ID)) +} diff --git a/provider/ecs/config_segment_test.go b/provider/ecs/config_segment_test.go new file mode 100644 index 000000000..133f3fbe5 --- /dev/null +++ b/provider/ecs/config_segment_test.go @@ -0,0 +1,852 @@ +package ecs + +import ( + "testing" + "time" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/containous/flaeg/parse" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSegmentBuildConfiguration(t *testing.T) { + testCases := []struct { + desc string + instanceInfo []ecsInstance + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "when no container", + instanceInfo: []ecsInstance{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "simple configuration", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.frontend.entryPoints": "http,https", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-863563a2e23c95502862016417ee95ea": { + URL: "http://127.0.0.1:2503", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "auth basic", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.frontend.entryPoints": "http,https", + label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsersFile: ".htpasswd", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRemoveHeader: "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + Auth: &types.Auth{ + HeaderField: "X-WebAuth-User", + Basic: &types.Basic{ + RemoveHeader: true, + Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + UsersFile: ".htpasswd", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-863563a2e23c95502862016417ee95ea": { + URL: "http://127.0.0.1:2503", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "auth basic backward compatibility", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.frontend.entryPoints": "http,https", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + Auth: &types.Auth{ + Basic: &types.Basic{ + Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-863563a2e23c95502862016417ee95ea": { + URL: "http://127.0.0.1:2503", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "auth digest", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.frontend.entryPoints": "http,https", + label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsersFile: ".htpasswd", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestRemoveHeader: "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + Auth: &types.Auth{ + HeaderField: "X-WebAuth-User", + Digest: &types.Digest{ + RemoveHeader: true, + Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + UsersFile: ".htpasswd", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-863563a2e23c95502862016417ee95ea": { + URL: "http://127.0.0.1:2503", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "auth forward", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.frontend.entryPoints": "http,https", + label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAddress: "auth.server", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTrustForwardHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCa: "ca.crt", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCaOptional: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCert: "server.crt", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSKey: "server.key", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSInsecureSkipVerify: "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + Auth: &types.Auth{ + HeaderField: "X-WebAuth-User", + Forward: &types.Forward{ + Address: "auth.server", + TrustForwardHeader: true, + TLS: &types.ClientTLS{ + CA: "ca.crt", + CAOptional: true, + Cert: "server.crt", + Key: "server.key", + InsecureSkipVerify: true, + }, + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-863563a2e23c95502862016417ee95ea": { + URL: "http://127.0.0.1:2503", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "when all labels are set", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + label.Prefix + "sauternes." + label.SuffixPort: "666", + label.Prefix + "sauternes." + label.SuffixProtocol: "https", + label.Prefix + "sauternes." + label.SuffixWeight: "12", + + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRemoveHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsersFile: ".htpasswd", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestRemoveHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsersFile: ".htpasswd", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAddress: "auth.server", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTrustForwardHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCa: "ca.crt", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCaOptional: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCert: "server.crt", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSKey: "server.key", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSInsecureSkipVerify: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User", + + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "sauternes." + label.SuffixFrontendEntryPoints: "http,https", + label.Prefix + "sauternes." + label.SuffixFrontendPassHostHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendPassTLSCert: "true", + label.Prefix + "sauternes." + label.SuffixFrontendPriority: "666", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectEntryPoint: "https", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectRegex: "nope", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectReplacement: "nope", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectPermanent: "true", + label.Prefix + "sauternes." + label.SuffixFrontendWhiteListSourceRange: "10.10.10.10", + label.Prefix + "sauternes." + label.SuffixFrontendWhiteListIPStrategyExcludedIPS: "10.10.10.10,10.10.10.11", + label.Prefix + "sauternes." + label.SuffixFrontendWhiteListIPStrategyDepth: "5", + + label.Prefix + "sauternes." + label.SuffixFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "sauternes." + label.SuffixFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersAllowedHosts: "foo,bar,bor", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersHostsProxyHeaders: "foo,bar,bor", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLHost: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomFrameOptionsValue: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentSecurityPolicy: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersPublicKey: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersReferrerPolicy: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomBrowserXSSValue: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSSeconds: "666", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLForceHost: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLRedirect: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLTemporaryRedirect: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSIncludeSubdomains: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSPreload: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersForceSTSHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersFrameDeny: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentTypeNosniff: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersBrowserXSSFilter: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersIsDevelopment: "true", + + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", + + label.Prefix + "sauternes." + label.SuffixFrontendRateLimitExtractorFunc: "client.ip", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 666), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + EntryPoints: []string{ + "http", + "https", + }, + PassHostHeader: true, + PassTLSCert: true, + Priority: 666, + Auth: &types.Auth{ + HeaderField: "X-WebAuth-User", + Basic: &types.Basic{ + RemoveHeader: true, + Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + UsersFile: ".htpasswd", + }, + }, + WhiteList: &types.WhiteList{ + SourceRange: []string{"10.10.10.10"}, + IPStrategy: &types.IPStrategy{ + Depth: 5, + ExcludedIPs: []string{"10.10.10.10", "10.10.10.11"}, + }, + }, + Headers: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{ + "foo", + "bar", + "bor", + }, + HostsProxyHeaders: []string{ + "foo", + "bar", + "bor", + }, + SSLRedirect: true, + SSLTemporaryRedirect: true, + SSLForceHost: true, + SSLHost: "foo", + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + STSSeconds: 666, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foo", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foo", + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + IsDevelopment: true, + }, + Errors: map[string]*types.ErrorPage{ + "foo": { + Status: []string{"404"}, + Query: "foo_query", + Backend: "backend-foobar", + }, + "bar": { + Status: []string{"500", "600"}, + Query: "bar_query", + Backend: "backend-foobar", + }, + }, + RateLimit: &types.RateLimit{ + ExtractorFunc: "client.ip", + RateSet: map[string]*types.Rate{ + "foo": { + Period: parse.Duration(6 * time.Second), + Average: 12, + Burst: 18, + }, + "bar": { + Period: parse.Duration(3 * time.Second), + Average: 6, + Burst: 9, + }, + }, + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + Regex: "", + Replacement: "", + Permanent: true, + }, + + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-7f6444e0dff3330c8b0ad2bbbd383b0f": { + URL: "https://127.0.0.1:666", + Weight: 12, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "several containers", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("test1"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.protocol": "https", + "traefik.sauternes.weight": "80", + "traefik.sauternes.backend": "foobar", + "traefik.sauternes.frontend.passHostHeader": "false", + "traefik.sauternes.frontend.rule": "Path:/mypath", + "traefik.sauternes.frontend.priority": "5000", + "traefik.sauternes.frontend.entryPoints": "http,https,ws", + "traefik.sauternes.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + "traefik.sauternes.frontend.redirect.entryPoint": "https", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + instance( + ID("abc987654321"), + name("test2"), + labels(map[string]string{ + "traefik.anothersauternes.port": "8079", + "traefik.anothersauternes.weight": "33", + "traefik.anothersauternes.frontend.rule": "Path:/anotherpath", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(80, 8079), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: false, + Priority: 5000, + EntryPoints: []string{"http", "https", "ws"}, + Auth: &types.Auth{ + Basic: &types.Basic{ + Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + }, + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-sauternes-test1-foobar": { + Rule: "Path:/mypath", + }, + }, + }, + "frontend-anothersauternes-test2-anothersauternes": { + Backend: "backend-test2-anothersauternes", + PassHostHeader: true, + EntryPoints: []string{}, + Routes: map[string]types.Route{ + "route-frontend-anothersauternes-test2-anothersauternes": { + Rule: "Path:/anotherpath", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test1-foobar": { + Servers: map[string]types.Server{ + "server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": { + URL: "https://127.0.0.1:2503", + Weight: 80, + }, + }, + CircuitBreaker: nil, + }, + "backend-test2-anothersauternes": { + Servers: map[string]types.Server{ + "server-test2-abc987654321-045e3e4aa5a744a325c099b803700a93": { + URL: "http://127.0.0.2:8079", + Weight: 33, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "several segments with the same backend name and same port", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("test1"), + labels(map[string]string{ + "traefik.port": "2503", + "traefik.protocol": "https", + "traefik.weight": "80", + "traefik.frontend.entryPoints": "http,https", + "traefik.frontend.redirect.entryPoint": "https", + + "traefik.sauternes.backend": "foobar", + "traefik.sauternes.frontend.rule": "Path:/sauternes", + "traefik.sauternes.frontend.priority": "5000", + + "traefik.arbois.backend": "foobar", + "traefik.arbois.frontend.rule": "Path:/arbois", + "traefik.arbois.frontend.priority": "3000", + }), + + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 5000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-sauternes-test1-foobar": { + Rule: "Path:/sauternes", + }, + }, + }, + "frontend-arbois-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 3000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-arbois-test1-foobar": { + Rule: "Path:/arbois", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test1-foobar": { + Servers: map[string]types.Server{ + "server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": { + URL: "https://127.0.0.1:2503", + Weight: 80, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "several segments with the same backend name and different port (wrong behavior)", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("test1"), + labels(map[string]string{ + "traefik.protocol": "https", + "traefik.frontend.entryPoints": "http,https", + "traefik.frontend.redirect.entryPoint": "https", + + "traefik.sauternes.port": "2503", + "traefik.sauternes.weight": "80", + "traefik.sauternes.backend": "foobar", + "traefik.sauternes.frontend.rule": "Path:/sauternes", + "traefik.sauternes.frontend.priority": "5000", + + "traefik.arbois.port": "2504", + "traefik.arbois.weight": "90", + "traefik.arbois.backend": "foobar", + "traefik.arbois.frontend.rule": "Path:/arbois", + "traefik.arbois.frontend.priority": "3000", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + mPort(80, 2504), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 5000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-sauternes-test1-foobar": { + Rule: "Path:/sauternes", + }, + }, + }, + "frontend-arbois-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 3000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-arbois-test1-foobar": { + Rule: "Path:/arbois", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test1-foobar": { + Servers: map[string]types.Server{ + "server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": { + URL: "https://127.0.0.1:2503", + Weight: 80, + }, + "server-test1-123456789abc-315a41140f1bd825b066e39686c18482": { + URL: "https://127.0.0.1:2504", + Weight: 90, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "several segments with the same backend name and different port binding", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("test1"), + labels(map[string]string{ + "traefik.protocol": "https", + "traefik.frontend.entryPoints": "http,https", + "traefik.frontend.redirect.entryPoint": "https", + + "traefik.sauternes.port": "2503", + "traefik.sauternes.weight": "80", + "traefik.sauternes.backend": "foobar", + "traefik.sauternes.frontend.rule": "Path:/sauternes", + "traefik.sauternes.frontend.priority": "5000", + + "traefik.arbois.port": "8080", + "traefik.arbois.weight": "90", + "traefik.arbois.backend": "foobar", + "traefik.arbois.frontend.rule": "Path:/arbois", + "traefik.arbois.frontend.priority": "3000", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + mPort(8080, 2504), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 5000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-sauternes-test1-foobar": { + Rule: "Path:/sauternes", + }, + }, + }, + "frontend-arbois-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 3000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-arbois-test1-foobar": { + Rule: "Path:/arbois", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test1-foobar": { + Servers: map[string]types.Server{ + "server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": { + URL: "https://127.0.0.1:2503", + Weight: 80, + }, + "server-test1-123456789abc-315a41140f1bd825b066e39686c18482": { + URL: "https://127.0.0.1:2504", + Weight: 90, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + } + + provider := &Provider{ + Domain: "ecs.localhost", + ExposedByDefault: true, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actualConfig, err := provider.buildConfiguration(test.instanceInfo) + + assert.NoError(t, err) + require.NotNil(t, actualConfig, "actualConfig") + + assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) + assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} diff --git a/provider/ecs/config_test.go b/provider/ecs/config_test.go index edb017378..1d5b05bd4 100644 --- a/provider/ecs/config_test.go +++ b/provider/ecs/config_test.go @@ -23,18 +23,18 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -63,20 +63,21 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully with health check labels", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikBackendHealthCheckPath: aws.String("/health"), - label.TraefikBackendHealthCheckInterval: aws.String("1s"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{ + label.TraefikBackendHealthCheckPath: aws.String("/health"), + label.TraefikBackendHealthCheckInterval: aws.String("1s"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -109,22 +110,23 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully with basic auth labels", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendAuthBasicUsersFile: aws.String(".htpasswd"), - label.TraefikFrontendAuthBasicRemoveHeader: aws.String("true"), - label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{ + label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendAuthBasicUsersFile: aws.String(".htpasswd"), + label.TraefikFrontendAuthBasicRemoveHeader: aws.String("true"), + label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -162,19 +164,20 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully with basic auth (backward compatibility) labels", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{ + label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -209,22 +212,23 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully with digest auth labels", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendAuthDigestRemoveHeader: aws.String("true"), - label.TraefikFrontendAuthDigestUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendAuthDigestUsersFile: aws.String(".htpasswd"), - label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{ + label.TraefikFrontendAuthDigestRemoveHeader: aws.String("true"), + label.TraefikFrontendAuthDigestUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendAuthDigestUsersFile: aws.String(".htpasswd"), + label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -262,25 +266,26 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully with forward auth labels", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendAuthForwardAddress: aws.String("auth.server"), - label.TraefikFrontendAuthForwardTrustForwardHeader: aws.String("true"), - label.TraefikFrontendAuthForwardTLSCa: aws.String("ca.crt"), - label.TraefikFrontendAuthForwardTLSCaOptional: aws.String("true"), - label.TraefikFrontendAuthForwardTLSCert: aws.String("server.crt"), - label.TraefikFrontendAuthForwardTLSKey: aws.String("server.key"), - label.TraefikFrontendAuthForwardTLSInsecureSkipVerify: aws.String("true"), label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{ + label.TraefikFrontendAuthForwardAddress: aws.String("auth.server"), + label.TraefikFrontendAuthForwardTrustForwardHeader: aws.String("true"), + label.TraefikFrontendAuthForwardTLSCa: aws.String("ca.crt"), + label.TraefikFrontendAuthForwardTLSCaOptional: aws.String("true"), + label.TraefikFrontendAuthForwardTLSCert: aws.String("server.crt"), + label.TraefikFrontendAuthForwardTLSKey: aws.String("server.key"), + label.TraefikFrontendAuthForwardTLSInsecureSkipVerify: aws.String("true"), label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -323,108 +328,109 @@ func TestBuildConfiguration(t *testing.T) { { desc: "when all labels are set", instances: []ecsInstance{ - { - Name: "testing-instance", - ID: "6", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikPort: aws.String("666"), - label.TraefikProtocol: aws.String("https"), - label.TraefikWeight: aws.String("12"), + instance( + name("testing-instance"), + ID("6"), + dockerLabels(map[string]*string{ + label.TraefikPort: aws.String("666"), + label.TraefikProtocol: aws.String("https"), + label.TraefikWeight: aws.String("12"), - label.TraefikBackend: aws.String("foobar"), + label.TraefikBackend: aws.String("foobar"), - label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), - label.TraefikBackendHealthCheckScheme: aws.String("http"), - label.TraefikBackendHealthCheckPath: aws.String("/health"), - label.TraefikBackendHealthCheckPort: aws.String("880"), - label.TraefikBackendHealthCheckInterval: aws.String("6"), - label.TraefikBackendHealthCheckHostname: aws.String("foo.com"), - label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), - label.TraefikBackendLoadBalancerMethod: aws.String("drr"), - label.TraefikBackendLoadBalancerStickiness: aws.String("true"), - label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), - label.TraefikBackendMaxConnAmount: aws.String("666"), - label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), - label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), + label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), + label.TraefikBackendHealthCheckScheme: aws.String("http"), + label.TraefikBackendHealthCheckPath: aws.String("/health"), + label.TraefikBackendHealthCheckPort: aws.String("880"), + label.TraefikBackendHealthCheckInterval: aws.String("6"), + label.TraefikBackendHealthCheckHostname: aws.String("foo.com"), + label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), + label.TraefikBackendLoadBalancerMethod: aws.String("drr"), + label.TraefikBackendLoadBalancerStickiness: aws.String("true"), + label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), + label.TraefikBackendMaxConnAmount: aws.String("666"), + label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), + label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), - label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendAuthBasicRemoveHeader: aws.String("true"), - label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendAuthBasicUsersFile: aws.String(".htpasswd"), - label.TraefikFrontendAuthDigestRemoveHeader: aws.String("true"), - label.TraefikFrontendAuthDigestUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendAuthDigestUsersFile: aws.String(".htpasswd"), - label.TraefikFrontendAuthForwardAddress: aws.String("auth.server"), - label.TraefikFrontendAuthForwardTrustForwardHeader: aws.String("true"), - label.TraefikFrontendAuthForwardTLSCa: aws.String("ca.crt"), - label.TraefikFrontendAuthForwardTLSCaOptional: aws.String("true"), - label.TraefikFrontendAuthForwardTLSCert: aws.String("server.crt"), - label.TraefikFrontendAuthForwardTLSKey: aws.String("server.key"), - label.TraefikFrontendAuthForwardTLSInsecureSkipVerify: aws.String("true"), - label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), + label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendAuthBasicRemoveHeader: aws.String("true"), + label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendAuthBasicUsersFile: aws.String(".htpasswd"), + label.TraefikFrontendAuthDigestRemoveHeader: aws.String("true"), + label.TraefikFrontendAuthDigestUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendAuthDigestUsersFile: aws.String(".htpasswd"), + label.TraefikFrontendAuthForwardAddress: aws.String("auth.server"), + label.TraefikFrontendAuthForwardTrustForwardHeader: aws.String("true"), + label.TraefikFrontendAuthForwardTLSCa: aws.String("ca.crt"), + label.TraefikFrontendAuthForwardTLSCaOptional: aws.String("true"), + label.TraefikFrontendAuthForwardTLSCert: aws.String("server.crt"), + label.TraefikFrontendAuthForwardTLSKey: aws.String("server.key"), + label.TraefikFrontendAuthForwardTLSInsecureSkipVerify: aws.String("true"), + label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), - label.TraefikFrontendEntryPoints: aws.String("http,https"), - label.TraefikFrontendPassHostHeader: aws.String("true"), - label.TraefikFrontendPassTLSCert: aws.String("true"), - label.TraefikFrontendPriority: aws.String("666"), - label.TraefikFrontendRedirectEntryPoint: aws.String("https"), - label.TraefikFrontendRedirectRegex: aws.String("nope"), - label.TraefikFrontendRedirectReplacement: aws.String("nope"), - label.TraefikFrontendRedirectPermanent: aws.String("true"), - label.TraefikFrontendRule: aws.String("Host:traefik.io"), - label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), - label.TraefikFrontendWhiteListIPStrategyExcludedIPS: aws.String("10.10.10.10,10.10.10.11"), - label.TraefikFrontendWhiteListIPStrategyDepth: aws.String("5"), + label.TraefikFrontendEntryPoints: aws.String("http,https"), + label.TraefikFrontendPassHostHeader: aws.String("true"), + label.TraefikFrontendPassTLSCert: aws.String("true"), + label.TraefikFrontendPriority: aws.String("666"), + label.TraefikFrontendRedirectEntryPoint: aws.String("https"), + label.TraefikFrontendRedirectRegex: aws.String("nope"), + label.TraefikFrontendRedirectReplacement: aws.String("nope"), + label.TraefikFrontendRedirectPermanent: aws.String("true"), + label.TraefikFrontendRule: aws.String("Host:traefik.io"), + label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), + label.TraefikFrontendWhiteListIPStrategyExcludedIPS: aws.String("10.10.10.10,10.10.10.11"), + label.TraefikFrontendWhiteListIPStrategyDepth: aws.String("5"), - label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), - label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), - label.TraefikFrontendSSLHost: aws.String("foo"), - label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), - label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), - label.TraefikFrontendPublicKey: aws.String("foo"), - label.TraefikFrontendReferrerPolicy: aws.String("foo"), - label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), - label.TraefikFrontendSTSSeconds: aws.String("666"), - label.TraefikFrontendSSLForceHost: aws.String("true"), - label.TraefikFrontendSSLRedirect: aws.String("true"), - label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), - label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), - label.TraefikFrontendSTSPreload: aws.String("true"), - label.TraefikFrontendForceSTSHeader: aws.String("true"), - label.TraefikFrontendFrameDeny: aws.String("true"), - label.TraefikFrontendContentTypeNosniff: aws.String("true"), - label.TraefikFrontendBrowserXSSFilter: aws.String("true"), - label.TraefikFrontendIsDevelopment: aws.String("true"), + label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), + label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), + label.TraefikFrontendSSLHost: aws.String("foo"), + label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), + label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), + label.TraefikFrontendPublicKey: aws.String("foo"), + label.TraefikFrontendReferrerPolicy: aws.String("foo"), + label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), + label.TraefikFrontendSTSSeconds: aws.String("666"), + label.TraefikFrontendSSLForceHost: aws.String("true"), + label.TraefikFrontendSSLRedirect: aws.String("true"), + label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), + label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), + label.TraefikFrontendSTSPreload: aws.String("true"), + label.TraefikFrontendForceSTSHeader: aws.String("true"), + label.TraefikFrontendFrameDeny: aws.String("true"), + label.TraefikFrontendContentTypeNosniff: aws.String("true"), + label.TraefikFrontendBrowserXSSFilter: aws.String("true"), + label.TraefikFrontendIsDevelopment: aws.String("true"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), - label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -585,176 +591,178 @@ func TestBuildConfiguration(t *testing.T) { { desc: "Containers with same backend name", instances: []ecsInstance{ - { - Name: "testing-instance-v1", - ID: "6", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikPort: aws.String("666"), - label.TraefikProtocol: aws.String("https"), - label.TraefikWeight: aws.String("12"), + instance( + name("testing-instance-v1"), + ID("6"), + dockerLabels(map[string]*string{ + label.TraefikPort: aws.String("666"), + label.TraefikProtocol: aws.String("https"), + label.TraefikWeight: aws.String("12"), - label.TraefikBackend: aws.String("foobar"), + label.TraefikBackend: aws.String("foobar"), - label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), - label.TraefikBackendHealthCheckScheme: aws.String("http"), - label.TraefikBackendHealthCheckPath: aws.String("/health"), - label.TraefikBackendHealthCheckPort: aws.String("880"), - label.TraefikBackendHealthCheckInterval: aws.String("6"), - label.TraefikBackendHealthCheckHostname: aws.String("foo.com"), - label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), - label.TraefikBackendLoadBalancerMethod: aws.String("drr"), - label.TraefikBackendLoadBalancerStickiness: aws.String("true"), - label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), - label.TraefikBackendMaxConnAmount: aws.String("666"), - label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), - label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), + label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), + label.TraefikBackendHealthCheckScheme: aws.String("http"), + label.TraefikBackendHealthCheckPath: aws.String("/health"), + label.TraefikBackendHealthCheckPort: aws.String("880"), + label.TraefikBackendHealthCheckInterval: aws.String("6"), + label.TraefikBackendHealthCheckHostname: aws.String("foo.com"), + label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), + label.TraefikBackendLoadBalancerMethod: aws.String("drr"), + label.TraefikBackendLoadBalancerStickiness: aws.String("true"), + label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), + label.TraefikBackendMaxConnAmount: aws.String("666"), + label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), + label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), - label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendEntryPoints: aws.String("http,https"), - label.TraefikFrontendPassHostHeader: aws.String("true"), - label.TraefikFrontendPassTLSCert: aws.String("true"), - label.TraefikFrontendPriority: aws.String("666"), - label.TraefikFrontendRedirectEntryPoint: aws.String("https"), - label.TraefikFrontendRedirectRegex: aws.String("nope"), - label.TraefikFrontendRedirectReplacement: aws.String("nope"), - label.TraefikFrontendRedirectPermanent: aws.String("true"), - label.TraefikFrontendRule: aws.String("Host:traefik.io"), - label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), + label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendEntryPoints: aws.String("http,https"), + label.TraefikFrontendPassHostHeader: aws.String("true"), + label.TraefikFrontendPassTLSCert: aws.String("true"), + label.TraefikFrontendPriority: aws.String("666"), + label.TraefikFrontendRedirectEntryPoint: aws.String("https"), + label.TraefikFrontendRedirectRegex: aws.String("nope"), + label.TraefikFrontendRedirectReplacement: aws.String("nope"), + label.TraefikFrontendRedirectPermanent: aws.String("true"), + label.TraefikFrontendRule: aws.String("Host:traefik.io"), + label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), - label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), - label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), - label.TraefikFrontendSSLHost: aws.String("foo"), - label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), - label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), - label.TraefikFrontendPublicKey: aws.String("foo"), - label.TraefikFrontendReferrerPolicy: aws.String("foo"), - label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), - label.TraefikFrontendSTSSeconds: aws.String("666"), - label.TraefikFrontendSSLForceHost: aws.String("true"), - label.TraefikFrontendSSLRedirect: aws.String("true"), - label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), - label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), - label.TraefikFrontendSTSPreload: aws.String("true"), - label.TraefikFrontendForceSTSHeader: aws.String("true"), - label.TraefikFrontendFrameDeny: aws.String("true"), - label.TraefikFrontendContentTypeNosniff: aws.String("true"), - label.TraefikFrontendBrowserXSSFilter: aws.String("true"), - label.TraefikFrontendIsDevelopment: aws.String("true"), + label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), + label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), + label.TraefikFrontendSSLHost: aws.String("foo"), + label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), + label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), + label.TraefikFrontendPublicKey: aws.String("foo"), + label.TraefikFrontendReferrerPolicy: aws.String("foo"), + label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), + label.TraefikFrontendSTSSeconds: aws.String("666"), + label.TraefikFrontendSSLForceHost: aws.String("true"), + label.TraefikFrontendSSLRedirect: aws.String("true"), + label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), + label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), + label.TraefikFrontendSTSPreload: aws.String("true"), + label.TraefikFrontendForceSTSHeader: aws.String("true"), + label.TraefikFrontendFrameDeny: aws.String("true"), + label.TraefikFrontendContentTypeNosniff: aws.String("true"), + label.TraefikFrontendBrowserXSSFilter: aws.String("true"), + label.TraefikFrontendIsDevelopment: aws.String("true"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), - label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, - { - Name: "testing-instance-v2", - ID: "6", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikPort: aws.String("555"), - label.TraefikProtocol: aws.String("https"), - label.TraefikWeight: aws.String("15"), + label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), + instance( + name("testing-instance-v2"), + ID("6"), + dockerLabels(map[string]*string{ + label.TraefikPort: aws.String("555"), + label.TraefikProtocol: aws.String("https"), + label.TraefikWeight: aws.String("15"), - label.TraefikBackend: aws.String("foobar"), + label.TraefikBackend: aws.String("foobar"), - label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), - label.TraefikBackendHealthCheckScheme: aws.String("http"), - label.TraefikBackendHealthCheckPath: aws.String("/health"), - label.TraefikBackendHealthCheckPort: aws.String("880"), - label.TraefikBackendHealthCheckInterval: aws.String("6"), - label.TraefikBackendHealthCheckHostname: aws.String("bar.com"), - label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), - label.TraefikBackendLoadBalancerMethod: aws.String("drr"), - label.TraefikBackendLoadBalancerStickiness: aws.String("true"), - label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), - label.TraefikBackendMaxConnAmount: aws.String("666"), - label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), - label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), + label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), + label.TraefikBackendHealthCheckScheme: aws.String("http"), + label.TraefikBackendHealthCheckPath: aws.String("/health"), + label.TraefikBackendHealthCheckPort: aws.String("880"), + label.TraefikBackendHealthCheckInterval: aws.String("6"), + label.TraefikBackendHealthCheckHostname: aws.String("bar.com"), + label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), + label.TraefikBackendLoadBalancerMethod: aws.String("drr"), + label.TraefikBackendLoadBalancerStickiness: aws.String("true"), + label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), + label.TraefikBackendMaxConnAmount: aws.String("666"), + label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), + label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), - label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendEntryPoints: aws.String("http,https"), - label.TraefikFrontendPassHostHeader: aws.String("true"), - label.TraefikFrontendPassTLSCert: aws.String("true"), - label.TraefikFrontendPriority: aws.String("666"), - label.TraefikFrontendRedirectEntryPoint: aws.String("https"), - label.TraefikFrontendRedirectRegex: aws.String("nope"), - label.TraefikFrontendRedirectReplacement: aws.String("nope"), - label.TraefikFrontendRedirectPermanent: aws.String("true"), - label.TraefikFrontendRule: aws.String("Host:traefik.io"), - label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), + label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendEntryPoints: aws.String("http,https"), + label.TraefikFrontendPassHostHeader: aws.String("true"), + label.TraefikFrontendPassTLSCert: aws.String("true"), + label.TraefikFrontendPriority: aws.String("666"), + label.TraefikFrontendRedirectEntryPoint: aws.String("https"), + label.TraefikFrontendRedirectRegex: aws.String("nope"), + label.TraefikFrontendRedirectReplacement: aws.String("nope"), + label.TraefikFrontendRedirectPermanent: aws.String("true"), + label.TraefikFrontendRule: aws.String("Host:traefik.io"), + label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), - label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), - label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), - label.TraefikFrontendSSLHost: aws.String("foo"), - label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), - label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), - label.TraefikFrontendPublicKey: aws.String("foo"), - label.TraefikFrontendReferrerPolicy: aws.String("foo"), - label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), - label.TraefikFrontendSTSSeconds: aws.String("666"), - label.TraefikFrontendSSLForceHost: aws.String("true"), - label.TraefikFrontendSSLRedirect: aws.String("true"), - label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), - label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), - label.TraefikFrontendSTSPreload: aws.String("true"), - label.TraefikFrontendForceSTSHeader: aws.String("true"), - label.TraefikFrontendFrameDeny: aws.String("true"), - label.TraefikFrontendContentTypeNosniff: aws.String("true"), - label.TraefikFrontendBrowserXSSFilter: aws.String("true"), - label.TraefikFrontendIsDevelopment: aws.String("true"), + label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), + label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), + label.TraefikFrontendSSLHost: aws.String("foo"), + label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), + label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), + label.TraefikFrontendPublicKey: aws.String("foo"), + label.TraefikFrontendReferrerPolicy: aws.String("foo"), + label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), + label.TraefikFrontendSTSSeconds: aws.String("666"), + label.TraefikFrontendSSLForceHost: aws.String("true"), + label.TraefikFrontendSSLRedirect: aws.String("true"), + label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), + label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), + label.TraefikFrontendSTSPreload: aws.String("true"), + label.TraefikFrontendForceSTSHeader: aws.String("true"), + label.TraefikFrontendFrameDeny: aws.String("true"), + label.TraefikFrontendContentTypeNosniff: aws.String("true"), + label.TraefikFrontendBrowserXSSFilter: aws.String("true"), + label.TraefikFrontendIsDevelopment: aws.String("true"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), - label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.2.2.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.2.2.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ diff --git a/provider/ecs/ecs.go b/provider/ecs/ecs.go index ce0e9fe30..bb9e4f5c8 100644 --- a/provider/ecs/ecs.go +++ b/provider/ecs/ecs.go @@ -45,6 +45,8 @@ type ecsInstance struct { containerDefinition *ecs.ContainerDefinition machine *machine TraefikLabels map[string]string + SegmentLabels map[string]string + SegmentName string } type portMapping struct { diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 954a3da43..e9739d63d 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -122,40 +122,36 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s pool.Go(func(stop chan bool) { operation := func() error { - for { - stopWatch := make(chan struct{}, 1) - defer close(stopWatch) - eventsChan, err := k8sClient.WatchAll(p.Namespaces, stopWatch) - if err != nil { - log.Errorf("Error watching kubernetes events: %v", err) - timer := time.NewTimer(1 * time.Second) - select { - case <-timer.C: - return err - case <-stop: - return nil - } + stopWatch := make(chan struct{}, 1) + defer close(stopWatch) + eventsChan, err := k8sClient.WatchAll(p.Namespaces, stopWatch) + if err != nil { + log.Errorf("Error watching kubernetes events: %v", err) + timer := time.NewTimer(1 * time.Second) + select { + case <-timer.C: + return err + case <-stop: + return nil } - for { - select { - case <-stop: - return nil - case event := <-eventsChan: - log.Debugf("Received Kubernetes event kind %T", event) - - templateObjects, err := p.loadIngresses(k8sClient) - if err != nil { - return err - } - - if reflect.DeepEqual(p.lastConfiguration.Get(), templateObjects) { - log.Debugf("Skipping Kubernetes event kind %T", event) - } else { - p.lastConfiguration.Set(templateObjects) - configurationChan <- types.ConfigMessage{ - ProviderName: "kubernetes", - Configuration: p.loadConfig(*templateObjects), - } + } + for { + select { + case <-stop: + return nil + case event := <-eventsChan: + log.Debugf("Received Kubernetes event kind %T", event) + templateObjects, err := p.loadIngresses(k8sClient) + if err != nil { + return err + } + if reflect.DeepEqual(p.lastConfiguration.Get(), templateObjects) { + log.Debugf("Skipping Kubernetes event kind %T", event) + } else { + p.lastConfiguration.Set(templateObjects) + configurationChan <- types.ConfigMessage{ + ProviderName: "kubernetes", + Configuration: p.loadConfig(*templateObjects), } } } @@ -224,7 +220,12 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) } for _, pa := range r.HTTP.Paths { + priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0) baseName := r.Host + pa.Path + if priority > 0 { + baseName = strconv.Itoa(priority) + "-" + baseName + } + if _, exists := templateObjects.Backends[baseName]; !exists { templateObjects.Backends[baseName] = &types.Backend{ Servers: make(map[string]types.Server), @@ -250,7 +251,6 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) passHostHeader := getBoolValue(i.Annotations, annotationKubernetesPreserveHost, !p.DisablePassHostHeaders) passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) - priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0) entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints) templateObjects.Frontends[baseName] = &types.Frontend{ @@ -883,7 +883,19 @@ func getFrontendRedirect(i *extensionsv1beta1.Ingress, baseName, path string) *t } redirectRegex := getStringValue(i.Annotations, annotationKubernetesRedirectRegex, "") + _, err := strconv.Unquote(`"` + redirectRegex + `"`) + if err != nil { + log.Debugf("Skipping Redirect on Ingress %s/%s due to invalid regex: %s", i.Namespace, i.Name, redirectRegex) + return nil + } + redirectReplacement := getStringValue(i.Annotations, annotationKubernetesRedirectReplacement, "") + _, err = strconv.Unquote(`"` + redirectReplacement + `"`) + if err != nil { + log.Debugf("Skipping Redirect on Ingress %s/%s due to invalid replacement: %q", i.Namespace, i.Name, redirectRegex) + return nil + } + if len(redirectRegex) > 0 && len(redirectReplacement) > 0 { return &types.Redirect{ Regex: redirectRegex, diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index acc089235..de856c28c 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -740,6 +740,34 @@ func TestGetPassTLSCert(t *testing.T) { assert.Equal(t, expected, actual) } +func TestInvalidRedirectAnnotation(t *testing.T) { + ingresses := []*extensionsv1beta1.Ingress{ + buildIngress(iNamespace("awesome"), + iAnnotation(annotationKubernetesRedirectRegex, `bad\.regex`), + iAnnotation(annotationKubernetesRedirectReplacement, "test"), + iRules(iRule( + iHost("foo"), + iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + buildIngress(iNamespace("awesome"), + iAnnotation(annotationKubernetesRedirectRegex, `test`), + iAnnotation(annotationKubernetesRedirectReplacement, `bad\.replacement`), + iRules(iRule( + iHost("foo"), + iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + } + + for _, ingress := range ingresses { + actual := getFrontendRedirect(ingress, "test", "/") + var expected *types.Redirect + + assert.Equal(t, expected, actual) + } +} + func TestOnlyReferencesServicesFromOwnNamespace(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress(iNamespace("awesome"), @@ -1847,13 +1875,13 @@ func TestPriorityHeaderValue(t *testing.T) { expected := buildConfiguration( backends( - backend("foo/bar", + backend("1337-foo/bar", servers(server("http://example.com", weight(1))), lbMethod("wrr"), ), ), frontends( - frontend("foo/bar", + frontend("1337-foo/bar", passHostHeader(), priority(1337), routes( diff --git a/script/crossbinary-others b/script/crossbinary-others index e05efacf7..91658887d 100755 --- a/script/crossbinary-others +++ b/script/crossbinary-others @@ -62,3 +62,13 @@ for OS in ${OS_PLATFORM_ARG[@]}; do done done done + +# Build ppc64le binaries +OS_PLATFORM_ARG=(linux) +OS_ARCH_ARG=(ppc64le) +for OS in ${OS_PLATFORM_ARG[@]}; do + for ARCH in ${OS_ARCH_ARG[@]}; do + echo "Building binary for ${OS}/${ARCH}..." + GOARCH=${ARCH} GOOS=${OS} CGO_ENABLED=0 ${GO_BUILD_CMD} "${GO_BUILD_OPT}" -o "dist/traefik_${OS}-${ARCH}" ./cmd/traefik/ + done +done diff --git a/server/server.go b/server/server.go index 9c458debc..e9b64ac26 100644 --- a/server/server.go +++ b/server/server.go @@ -163,6 +163,22 @@ func (s serverEntryPoint) Shutdown(ctx context.Context) { wg.Wait() } +// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted +// connections. +type tcpKeepAliveListener struct { + *net.TCPListener +} + +func (ln tcpKeepAliveListener) Accept() (net.Conn, error) { + tc, err := ln.AcceptTCP() + if err != nil { + return nil, err + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} + // NewServer returns an initialized Server. func NewServer(globalConfiguration configuration.GlobalConfiguration, provider provider.Provider, entrypoints map[string]EntryPoint) *Server { server := &Server{} @@ -549,6 +565,8 @@ func (s *Server) prepareServer(entryPointName string, entryPoint *configuration. return nil, nil, fmt.Errorf("error opening listener: %v", err) } + listener = tcpKeepAliveListener{listener.(*net.TCPListener)} + if entryPoint.ProxyProtocol != nil { listener, err = buildProxyProtocolListener(entryPoint, listener) if err != nil { diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index fa50ee39f..fbeac5e56 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -88,8 +88,8 @@ [frontends."frontend-{{ $service.ServiceName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 1e79af1dd..10f94de62 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -88,8 +88,8 @@ [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/ecs.tmpl b/templates/ecs.tmpl index d43fd4f6a..a072798a9 100644 --- a/templates/ecs.tmpl +++ b/templates/ecs.tmpl @@ -2,13 +2,13 @@ {{range $serviceName, $instances := .Services }} {{ $firstInstance := index $instances 0 }} - {{ $circuitBreaker := getCircuitBreaker $firstInstance.TraefikLabels }} + {{ $circuitBreaker := getCircuitBreaker $firstInstance.SegmentLabels }} {{if $circuitBreaker }} [backends."backend-{{ $serviceName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $firstInstance.TraefikLabels }} + {{ $loadBalancer := getLoadBalancer $firstInstance.SegmentLabels }} {{if $loadBalancer }} [backends."backend-{{ $serviceName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -18,14 +18,14 @@ {{end}} {{end}} - {{ $maxConn := getMaxConn $firstInstance.TraefikLabels }} + {{ $maxConn := getMaxConn $firstInstance.SegmentLabels }} {{if $maxConn }} [backends."backend-{{ $serviceName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $firstInstance.TraefikLabels }} + {{ $healthCheck := getHealthCheck $firstInstance.SegmentLabels }} {{if $healthCheck }} [backends."backend-{{ $serviceName }}".healthCheck] scheme = "{{ $healthCheck.Scheme }}" @@ -41,7 +41,7 @@ {{end}} {{end}} - {{ $buffering := getBuffering $firstInstance.TraefikLabels }} + {{ $buffering := getBuffering $firstInstance.SegmentLabels }} {{if $buffering }} [backends."backend-{{ $serviceName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -63,38 +63,40 @@ {{range $serviceName, $instances := .Services }} {{range $instance := filterFrontends $instances }} - [frontends."frontend-{{ $serviceName }}"] - backend = "backend-{{ $serviceName }}" - priority = {{ getPriority $instance.TraefikLabels }} - passHostHeader = {{ getPassHostHeader $instance.TraefikLabels }} - passTLSCert = {{ getPassTLSCert $instance.TraefikLabels }} + {{ $frontendName := getFrontendName $instance }} - entryPoints = [{{range getEntryPoints $instance.TraefikLabels }} + [frontends."frontend-{{ $frontendName }}"] + backend = "backend-{{ $serviceName }}" + priority = {{ getPriority $instance.SegmentLabels }} + passHostHeader = {{ getPassHostHeader $instance.SegmentLabels }} + passTLSCert = {{ getPassTLSCert $instance.SegmentLabels }} + + entryPoints = [{{range getEntryPoints $instance.SegmentLabels }} "{{.}}", {{end}}] - {{ $auth := getAuth $instance.TraefikLabels }} + {{ $auth := getAuth $instance.SegmentLabels }} {{if $auth }} - [frontends."frontend-{{ $serviceName }}".auth] + [frontends."frontend-{{ $frontendName }}".auth] headerField = "{{ $auth.HeaderField }}" {{if $auth.Forward }} - [frontends."frontend-{{ $serviceName }}".auth.forward] + [frontends."frontend-{{ $frontendName }}".auth.forward] address = "{{ $auth.Forward.Address }}" trustForwardHeader = {{ $auth.Forward.TrustForwardHeader }} {{if $auth.Forward.TLS }} - [frontends."frontend-{{ $serviceName }}".auth.forward.tls] + [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} {{if $auth.Basic }} - [frontends."frontend-{{ $serviceName }}".auth.basic] + [frontends."frontend-{{ $frontendName }}".auth.basic] removeHeader = {{ $auth.Basic.RemoveHeader }} {{if $auth.Basic.Users }} users = [{{range $auth.Basic.Users }} @@ -105,7 +107,7 @@ {{end}} {{if $auth.Digest }} - [frontends."frontend-{{ $serviceName }}".auth.digest] + [frontends."frontend-{{ $frontendName }}".auth.digest] removeHeader = {{ $auth.Digest.RemoveHeader }} {{if $auth.Digest.Users }} users = [{{range $auth.Digest.Users }} @@ -116,14 +118,14 @@ {{end}} {{end}} - {{ $whitelist := getWhiteList $instance.TraefikLabels }} + {{ $whitelist := getWhiteList $instance.SegmentLabels }} {{if $whitelist }} - [frontends."frontend-{{ $serviceName }}".whiteList] + [frontends."frontend-{{ $frontendName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} "{{.}}", {{end}}] {{if $whitelist.IPStrategy }} - [frontends."frontend-{{ $serviceName }}".whiteList.IPStrategy] + [frontends."frontend-{{ $frontendName }}".whiteList.IPStrategy] depth = {{ $whitelist.IPStrategy.Depth }} excludedIPs = [{{range $whitelist.IPStrategy.ExcludedIPs }} "{{.}}", @@ -131,20 +133,20 @@ {{end}} {{end}} - {{ $redirect := getRedirect $instance.TraefikLabels }} + {{ $redirect := getRedirect $instance.SegmentLabels }} {{if $redirect }} - [frontends."frontend-{{ $serviceName }}".redirect] + [frontends."frontend-{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" regex = "{{ $redirect.Regex }}" replacement = "{{ $redirect.Replacement }}" permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $instance.TraefikLabels }} + {{ $errorPages := getErrorPages $instance.SegmentLabels }} {{if $errorPages }} - [frontends."frontend-{{ $serviceName }}".errors] + [frontends."frontend-{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} - [frontends."frontend-{{ $serviceName }}".errors."{{ $pageName }}"] + [frontends."frontend-{{ $frontendName }}".errors."{{ $pageName }}"] status = [{{range $page.Status }} "{{.}}", {{end}}] @@ -153,22 +155,22 @@ {{end}} {{end}} - {{ $rateLimit := getRateLimit $instance.TraefikLabels }} + {{ $rateLimit := getRateLimit $instance.SegmentLabels }} {{if $rateLimit }} - [frontends."frontend-{{ $serviceName }}".rateLimit] + [frontends."frontend-{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" - [frontends."frontend-{{ $serviceName }}".rateLimit.rateSet] + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet] {{ range $limitName, $limit := $rateLimit.RateSet }} - [frontends."frontend-{{ $serviceName }}".rateLimit.rateSet."{{ $limitName }}"] + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet."{{ $limitName }}"] period = "{{ $limit.Period }}" average = {{ $limit.Average }} burst = {{ $limit.Burst }} {{end}} {{end}} - {{ $headers := getHeaders $instance.TraefikLabels }} + {{ $headers := getHeaders $instance.SegmentLabels }} {{if $headers }} - [frontends."frontend-{{ $serviceName }}".headers] + [frontends."frontend-{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }} SSLHost = "{{ $headers.SSLHost }}" @@ -200,28 +202,28 @@ {{end}} {{if $headers.CustomRequestHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.customRequestHeaders] + [frontends."frontend-{{ $frontendName }}".headers.customRequestHeaders] {{range $k, $v := $headers.CustomRequestHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{if $headers.CustomResponseHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.customResponseHeaders] + [frontends."frontend-{{ $frontendName }}".headers.customResponseHeaders] {{range $k, $v := $headers.CustomResponseHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{if $headers.SSLProxyHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.SSLProxyHeaders] + [frontends."frontend-{{ $frontendName }}".headers.SSLProxyHeaders] {{range $k, $v := $headers.SSLProxyHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{end}} - [frontends."frontend-{{ $serviceName }}".routes."route-frontend-{{ $serviceName }}"] + [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] rule = "{{ getFrontendRule $instance }}" {{end}} diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 39207eefa..a4eae54e8 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -80,8 +80,8 @@ trustForwardHeader = {{ $frontend.Auth.Forward.TrustForwardHeader }} {{if $frontend.Auth.Forward.TLS }} [frontends."{{ $frontendName }}".auth.forward.tls] - cert = "{{ $frontend.Auth.Forward.TLS.Cert }}" - key = "{{ $frontend.Auth.Forward.TLS.Key }}" + cert = """{{ $frontend.Auth.Forward.TLS.Cert }}""" + key = """{{ $frontend.Auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $frontend.Auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/kv.tmpl b/templates/kv.tmpl index 68edeeb16..3698df0f8 100644 --- a/templates/kv.tmpl +++ b/templates/kv.tmpl @@ -87,8 +87,8 @@ [frontends."{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index f750486f7..67da135d1 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -90,8 +90,8 @@ [frontends."{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/mesos.tmpl b/templates/mesos.tmpl index e7ec3d83a..a4f71a256 100644 --- a/templates/mesos.tmpl +++ b/templates/mesos.tmpl @@ -90,8 +90,8 @@ [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/rancher.tmpl b/templates/rancher.tmpl index 44b7f57ef..b731b24a7 100644 --- a/templates/rancher.tmpl +++ b/templates/rancher.tmpl @@ -88,8 +88,8 @@ [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/vendor/github.com/gorilla/websocket/client.go b/vendor/github.com/gorilla/websocket/client.go index 8e90de27f..2e32fd506 100644 --- a/vendor/github.com/gorilla/websocket/client.go +++ b/vendor/github.com/gorilla/websocket/client.go @@ -6,12 +6,14 @@ package websocket import ( "bytes" + "context" "crypto/tls" "errors" "io" "io/ioutil" "net" "net/http" + "net/http/httptrace" "net/url" "strings" "time" @@ -51,6 +53,10 @@ type Dialer struct { // NetDial is nil, net.Dial is used. NetDial func(network, addr string) (net.Conn, error) + // NetDialContext specifies the dial function for creating TCP connections. If + // NetDialContext is nil, net.DialContext is used. + NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error) + // Proxy specifies a function to return a proxy for a given // Request. If the function returns a non-nil error, the // request is aborted with the provided error. @@ -69,6 +75,17 @@ type Dialer struct { // do not limit the size of the messages that can be sent or received. ReadBufferSize, WriteBufferSize int + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + // Subprotocols specifies the client's requested subprotocols. Subprotocols []string @@ -84,6 +101,11 @@ type Dialer struct { Jar http.CookieJar } +// Dial creates a new client connection by calling DialContext with a background context. +func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + return d.DialContext(context.Background(), urlStr, requestHeader) +} + var errMalformedURL = errors.New("malformed ws or wss URL") func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { @@ -111,19 +133,20 @@ var DefaultDialer = &Dialer{ } // nilDialer is dialer to use when receiver is nil. -var nilDialer Dialer = *DefaultDialer +var nilDialer = *DefaultDialer -// Dial creates a new client connection. Use requestHeader to specify the +// DialContext creates a new client connection. Use requestHeader to specify the // origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). // Use the response.Header to get the selected subprotocol // (Sec-WebSocket-Protocol) and cookies (Set-Cookie). // +// The context will be used in the request and in the Dialer +// // If the WebSocket handshake fails, ErrBadHandshake is returned along with a // non-nil *http.Response so that callers can handle redirects, authentication, // etcetera. The response body may not contain the entire response and does not // need to be closed by the application. -func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { - +func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { if d == nil { d = &nilDialer } @@ -161,6 +184,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re Header: make(http.Header), Host: u.Host, } + req = req.WithContext(ctx) // Set the cookies present in the cookie jar of the dialer if d.Jar != nil { @@ -201,23 +225,33 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } if d.EnableCompression { - req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover") + req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"} } - var deadline time.Time if d.HandshakeTimeout != 0 { - deadline = time.Now().Add(d.HandshakeTimeout) + var cancel func() + ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout) + defer cancel() } // Get network dial function. - netDial := d.NetDial - if netDial == nil { - netDialer := &net.Dialer{Deadline: deadline} - netDial = netDialer.Dial + var netDial func(network, add string) (net.Conn, error) + + if d.NetDialContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialContext(ctx, network, addr) + } + } else if d.NetDial != nil { + netDial = d.NetDial + } else { + netDialer := &net.Dialer{} + netDial = func(network, addr string) (net.Conn, error) { + return netDialer.DialContext(ctx, network, addr) + } } // If needed, wrap the dial function to set the connection deadline. - if !deadline.Equal(time.Time{}) { + if deadline, ok := ctx.Deadline(); ok { forwardDial := netDial netDial = func(network, addr string) (net.Conn, error) { c, err := forwardDial(network, addr) @@ -249,7 +283,17 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } hostPort, hostNoPort := hostPortNoPort(u) + trace := httptrace.ContextClientTrace(ctx) + if trace != nil && trace.GetConn != nil { + trace.GetConn(hostPort) + } + netConn, err := netDial("tcp", hostPort) + if trace != nil && trace.GotConn != nil { + trace.GotConn(httptrace.GotConnInfo{ + Conn: netConn, + }) + } if err != nil { return nil, nil, err } @@ -267,22 +311,31 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } tlsConn := tls.Client(netConn, cfg) netConn = tlsConn - if err := tlsConn.Handshake(); err != nil { - return nil, nil, err + + var err error + if trace != nil { + err = doHandshakeWithTrace(trace, tlsConn, cfg) + } else { + err = doHandshake(tlsConn, cfg) } - if !cfg.InsecureSkipVerify { - if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { - return nil, nil, err - } + + if err != nil { + return nil, nil, err } } - conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize) + conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil) if err := req.Write(netConn); err != nil { return nil, nil, err } + if trace != nil && trace.GotFirstResponseByte != nil { + if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 { + trace.GotFirstResponseByte() + } + } + resp, err := http.ReadResponse(conn.br, req) if err != nil { return nil, nil, err @@ -328,3 +381,15 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re netConn = nil // to avoid close in defer. return conn, resp, nil } + +func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error { + if err := tlsConn.Handshake(); err != nil { + return err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/conn.go b/vendor/github.com/gorilla/websocket/conn.go index 5f46bf4a5..d2a21c148 100644 --- a/vendor/github.com/gorilla/websocket/conn.go +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -223,6 +223,20 @@ func isValidReceivedCloseCode(code int) bool { return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999) } +// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this +// interface. The type of the value stored in a pool is not specified. +type BufferPool interface { + // Get gets a value from the pool or returns nil if the pool is empty. + Get() interface{} + // Put adds a value to the pool. + Put(interface{}) +} + +// writePoolData is the type added to the write buffer pool. This wrapper is +// used to prevent applications from peeking at and depending on the values +// added to the pool. +type writePoolData struct{ buf []byte } + // The Conn type represents a WebSocket connection. type Conn struct { conn net.Conn @@ -232,6 +246,8 @@ type Conn struct { // Write fields mu chan bool // used as mutex to protect write to conn writeBuf []byte // frame is constructed in this buffer. + writePool BufferPool + writeBufSize int writeDeadline time.Time writer io.WriteCloser // the current writer returned to the application isWriting bool // for best-effort concurrent write detection @@ -263,64 +279,29 @@ type Conn struct { newDecompressionReader func(io.Reader) io.ReadCloser } -func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn { - return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil) -} +func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn { -type writeHook struct { - p []byte -} - -func (wh *writeHook) Write(p []byte) (int, error) { - wh.p = p - return len(p), nil -} - -func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn { - mu := make(chan bool, 1) - mu <- true - - var br *bufio.Reader - if readBufferSize == 0 && brw != nil && brw.Reader != nil { - // Reuse the supplied bufio.Reader if the buffer has a useful size. - // This code assumes that peek on a reader returns - // bufio.Reader.buf[:0]. - brw.Reader.Reset(conn) - if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 { - br = brw.Reader - } - } if br == nil { if readBufferSize == 0 { readBufferSize = defaultReadBufferSize - } - if readBufferSize < maxControlFramePayloadSize { + } else if readBufferSize < maxControlFramePayloadSize { + // must be large enough for control frame readBufferSize = maxControlFramePayloadSize } br = bufio.NewReaderSize(conn, readBufferSize) } - var writeBuf []byte - if writeBufferSize == 0 && brw != nil && brw.Writer != nil { - // Use the bufio.Writer's buffer if the buffer has a useful size. This - // code assumes that bufio.Writer.buf[:1] is passed to the - // bufio.Writer's underlying writer. - var wh writeHook - brw.Writer.Reset(&wh) - brw.Writer.WriteByte(0) - brw.Flush() - if cap(wh.p) >= maxFrameHeaderSize+256 { - writeBuf = wh.p[:cap(wh.p)] - } - } - - if writeBuf == nil { - if writeBufferSize == 0 { - writeBufferSize = defaultWriteBufferSize - } - writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize) + if writeBufferSize <= 0 { + writeBufferSize = defaultWriteBufferSize + } + writeBufferSize += maxFrameHeaderSize + + if writeBuf == nil && writeBufferPool == nil { + writeBuf = make([]byte, writeBufferSize) } + mu := make(chan bool, 1) + mu <- true c := &Conn{ isServer: isServer, br: br, @@ -328,6 +309,8 @@ func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize in mu: mu, readFinal: true, writeBuf: writeBuf, + writePool: writeBufferPool, + writeBufSize: writeBufferSize, enableWriteCompression: true, compressionLevel: defaultCompressionLevel, } @@ -370,6 +353,15 @@ func (c *Conn) writeFatal(err error) error { return err } +func (c *Conn) read(n int) ([]byte, error) { + p, err := c.br.Peek(n) + if err == io.EOF { + err = errUnexpectedEOF + } + c.br.Discard(len(p)) + return p, err +} + func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error { <-c.mu defer func() { c.mu <- true }() @@ -475,7 +467,19 @@ func (c *Conn) prepWrite(messageType int) error { c.writeErrMu.Lock() err := c.writeErr c.writeErrMu.Unlock() - return err + if err != nil { + return err + } + + if c.writeBuf == nil { + wpd, ok := c.writePool.Get().(writePoolData) + if ok { + c.writeBuf = wpd.buf + } else { + c.writeBuf = make([]byte, c.writeBufSize) + } + } + return nil } // NextWriter returns a writer for the next message to send. The writer's Close @@ -601,6 +605,10 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error { if final { c.writer = nil + if c.writePool != nil { + c.writePool.Put(writePoolData{buf: c.writeBuf}) + c.writeBuf = nil + } return nil } diff --git a/vendor/github.com/gorilla/websocket/conn_read.go b/vendor/github.com/gorilla/websocket/conn_read.go deleted file mode 100644 index 1ea15059e..000000000 --- a/vendor/github.com/gorilla/websocket/conn_read.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build go1.5 - -package websocket - -import "io" - -func (c *Conn) read(n int) ([]byte, error) { - p, err := c.br.Peek(n) - if err == io.EOF { - err = errUnexpectedEOF - } - c.br.Discard(len(p)) - return p, err -} diff --git a/vendor/github.com/gorilla/websocket/conn_read_legacy.go b/vendor/github.com/gorilla/websocket/conn_read_legacy.go deleted file mode 100644 index 018541cf6..000000000 --- a/vendor/github.com/gorilla/websocket/conn_read_legacy.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.5 - -package websocket - -import "io" - -func (c *Conn) read(n int) ([]byte, error) { - p, err := c.br.Peek(n) - if err == io.EOF { - err = errUnexpectedEOF - } - if len(p) > 0 { - // advance over the bytes just read - io.ReadFull(c.br, p) - } - return p, err -} diff --git a/vendor/github.com/gorilla/websocket/prepared.go b/vendor/github.com/gorilla/websocket/prepared.go index 1efffbd1e..74ec565d2 100644 --- a/vendor/github.com/gorilla/websocket/prepared.go +++ b/vendor/github.com/gorilla/websocket/prepared.go @@ -19,7 +19,6 @@ import ( type PreparedMessage struct { messageType int data []byte - err error mu sync.Mutex frames map[prepareKey]*preparedFrame } diff --git a/vendor/github.com/gorilla/websocket/proxy.go b/vendor/github.com/gorilla/websocket/proxy.go index 102538bd3..bf2478e43 100644 --- a/vendor/github.com/gorilla/websocket/proxy.go +++ b/vendor/github.com/gorilla/websocket/proxy.go @@ -14,7 +14,7 @@ import ( "strings" ) -type netDialerFunc func(netowrk, addr string) (net.Conn, error) +type netDialerFunc func(network, addr string) (net.Conn, error) func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) { return fn(network, addr) diff --git a/vendor/github.com/gorilla/websocket/server.go b/vendor/github.com/gorilla/websocket/server.go index 3e96a00c8..a761824b3 100644 --- a/vendor/github.com/gorilla/websocket/server.go +++ b/vendor/github.com/gorilla/websocket/server.go @@ -7,7 +7,7 @@ package websocket import ( "bufio" "errors" - "net" + "io" "net/http" "net/url" "strings" @@ -33,10 +33,23 @@ type Upgrader struct { // or received. ReadBufferSize, WriteBufferSize int + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + // Subprotocols specifies the server's supported protocols in order of - // preference. If this field is set, then the Upgrade method negotiates a + // preference. If this field is not nil, then the Upgrade method negotiates a // subprotocol by selecting the first match in this list with a protocol - // requested by the client. + // requested by the client. If there's no match, then no protocol is + // negotiated (the Sec-Websocket-Protocol header is not included in the + // handshake response). Subprotocols []string // Error specifies the function for generating HTTP error responses. If Error @@ -103,7 +116,7 @@ func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header // // The responseHeader is included in the response to the client's upgrade // request. Use the responseHeader to specify cookies (Set-Cookie) and the -// application negotiated subprotocol (Sec-Websocket-Protocol). +// application negotiated subprotocol (Sec-WebSocket-Protocol). // // If the upgrade fails, then Upgrade replies to the client with an HTTP error // response. @@ -127,7 +140,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade } if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { - return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported") + return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported") } checkOrigin := u.CheckOrigin @@ -140,7 +153,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade challengeKey := r.Header.Get("Sec-Websocket-Key") if challengeKey == "" { - return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-Websocket-Key' header is missing or blank") + return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-WebSocket-Key' header is missing or blank") } subprotocol := u.selectSubprotocol(r, responseHeader) @@ -157,17 +170,12 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade } } - var ( - netConn net.Conn - err error - ) - h, ok := w.(http.Hijacker) if !ok { return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") } var brw *bufio.ReadWriter - netConn, brw, err = h.Hijack() + netConn, brw, err := h.Hijack() if err != nil { return u.returnError(w, r, http.StatusInternalServerError, err.Error()) } @@ -177,7 +185,21 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade return nil, errors.New("websocket: client sent data before handshake is complete") } - c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw) + var br *bufio.Reader + if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 { + // Reuse hijacked buffered reader as connection reader. + br = brw.Reader + } + + buf := bufioWriterBuffer(netConn, brw.Writer) + + var writeBuf []byte + if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 { + // Reuse hijacked write buffer as connection buffer. + writeBuf = buf + } + + c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf) c.subprotocol = subprotocol if compress { @@ -185,17 +207,23 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade c.newDecompressionReader = decompressNoContextTakeover } - p := c.writeBuf[:0] + // Use larger of hijacked buffer and connection write buffer for header. + p := buf + if len(c.writeBuf) > len(p) { + p = c.writeBuf + } + p = p[:0] + p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) p = append(p, computeAcceptKey(challengeKey)...) p = append(p, "\r\n"...) if c.subprotocol != "" { - p = append(p, "Sec-Websocket-Protocol: "...) + p = append(p, "Sec-WebSocket-Protocol: "...) p = append(p, c.subprotocol...) p = append(p, "\r\n"...) } if compress { - p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) + p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) } for k, vs := range responseHeader { if k == "Sec-Websocket-Protocol" { @@ -296,3 +324,40 @@ func IsWebSocketUpgrade(r *http.Request) bool { return tokenListContainsValue(r.Header, "Connection", "upgrade") && tokenListContainsValue(r.Header, "Upgrade", "websocket") } + +// bufioReaderSize size returns the size of a bufio.Reader. +func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int { + // This code assumes that peek on a reset reader returns + // bufio.Reader.buf[:0]. + // TODO: Use bufio.Reader.Size() after Go 1.10 + br.Reset(originalReader) + if p, err := br.Peek(0); err == nil { + return cap(p) + } + return 0 +} + +// writeHook is an io.Writer that records the last slice passed to it vio +// io.Writer.Write. +type writeHook struct { + p []byte +} + +func (wh *writeHook) Write(p []byte) (int, error) { + wh.p = p + return len(p), nil +} + +// bufioWriterBuffer grabs the buffer from a bufio.Writer. +func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte { + // This code assumes that bufio.Writer.buf[:1] is passed to the + // bufio.Writer's underlying writer. + var wh writeHook + bw.Reset(&wh) + bw.WriteByte(0) + bw.Flush() + + bw.Reset(originalWriter) + + return wh.p[:cap(wh.p)] +} diff --git a/vendor/github.com/gorilla/websocket/trace.go b/vendor/github.com/gorilla/websocket/trace.go new file mode 100644 index 000000000..834f122a0 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/trace.go @@ -0,0 +1,19 @@ +// +build go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + if trace.TLSHandshakeStart != nil { + trace.TLSHandshakeStart() + } + err := doHandshake(tlsConn, cfg) + if trace.TLSHandshakeDone != nil { + trace.TLSHandshakeDone(tlsConn.ConnectionState(), err) + } + return err +} diff --git a/vendor/github.com/gorilla/websocket/trace_17.go b/vendor/github.com/gorilla/websocket/trace_17.go new file mode 100644 index 000000000..77d05a0b5 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/trace_17.go @@ -0,0 +1,12 @@ +// +build !go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + return doHandshake(tlsConn, cfg) +} diff --git a/vendor/github.com/gorilla/websocket/util.go b/vendor/github.com/gorilla/websocket/util.go index 385fa01be..354001e1e 100644 --- a/vendor/github.com/gorilla/websocket/util.go +++ b/vendor/github.com/gorilla/websocket/util.go @@ -178,7 +178,7 @@ headers: return false } -// parseExtensiosn parses WebSocket extensions from a header. +// parseExtensions parses WebSocket extensions from a header. func parseExtensions(header http.Header) []map[string]string { // From RFC 6455: // diff --git a/vendor/github.com/vulcand/oxy/forward/fwd.go b/vendor/github.com/vulcand/oxy/forward/fwd.go index cd057f59c..5a8e81c77 100644 --- a/vendor/github.com/vulcand/oxy/forward/fwd.go +++ b/vendor/github.com/vulcand/oxy/forward/fwd.go @@ -4,9 +4,11 @@ package forward import ( + "bytes" "crypto/tls" "errors" "fmt" + "io" "net" "net/http" "net/http/httptest" @@ -257,6 +259,8 @@ func New(setters ...optSetter) (*Forwarder, error) { errorHandler: f.errHandler, } + f.postConfig() + return f, nil } @@ -309,11 +313,6 @@ func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) { outReq.URL.RawQuery = u.RawQuery outReq.RequestURI = "" // Outgoing request should not have RequestURI - // Do not pass client Host header unless optsetter PassHostHeader is set. - if !f.passHost { - outReq.Host = target.Host - } - outReq.Proto = "HTTP/1.1" outReq.ProtoMajor = 1 outReq.ProtoMinor = 1 @@ -321,6 +320,11 @@ func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) { if f.rewriter != nil { f.rewriter.Rewrite(outReq) } + + // Do not pass client Host header unless optsetter PassHostHeader is set. + if !f.passHost { + outReq.Host = target.Host + } } // serveHTTP forwards websocket traffic @@ -340,7 +344,7 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, // WebSocket is only in http/1.1 dialer.TLSClientConfig.NextProtos = []string{"http/1.1"} } - targetConn, resp, err := dialer.Dial(outReq.URL.String(), outReq.Header) + targetConn, resp, err := dialer.DialContext(outReq.Context(), outReq.URL.String(), outReq.Header) if err != nil { if resp == nil { ctx.errHandler.ServeHTTP(w, req, err) @@ -396,16 +400,28 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, errBackend := make(chan error, 1) replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) { + forward := func(messageType int, reader io.Reader) error { + writer, err := dst.NextWriter(messageType) + if err != nil { + return err + } + _, err = io.Copy(writer, reader) + if err != nil { + return err + } + return writer.Close() + } + src.SetPingHandler(func(data string) error { - return dst.WriteMessage(websocket.PingMessage, []byte(data)) + return forward(websocket.PingMessage, bytes.NewReader([]byte(data))) }) src.SetPongHandler(func(data string) error { - return dst.WriteMessage(websocket.PongMessage, []byte(data)) + return forward(websocket.PongMessage, bytes.NewReader([]byte(data))) }) for { - msgType, msg, err := src.ReadMessage() + msgType, reader, err := src.NextReader() if err != nil { m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) @@ -423,11 +439,11 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, } errc <- err if m != nil { - dst.WriteMessage(websocket.CloseMessage, m) + forward(websocket.CloseMessage, bytes.NewReader([]byte(m))) } break } - err = dst.WriteMessage(msgType, msg) + err = forward(msgType, reader) if err != nil { errc <- err break diff --git a/vendor/github.com/vulcand/oxy/forward/post_config.go b/vendor/github.com/vulcand/oxy/forward/post_config.go new file mode 100644 index 000000000..1c4b12316 --- /dev/null +++ b/vendor/github.com/vulcand/oxy/forward/post_config.go @@ -0,0 +1,5 @@ +// +build go1.11 + +package forward + +func (f *Forwarder) postConfig() {} diff --git a/vendor/github.com/vulcand/oxy/forward/post_config_18.go b/vendor/github.com/vulcand/oxy/forward/post_config_18.go new file mode 100644 index 000000000..7fee6843c --- /dev/null +++ b/vendor/github.com/vulcand/oxy/forward/post_config_18.go @@ -0,0 +1,42 @@ +// +build !go1.11 + +package forward + +import ( + "context" + "net/http" +) + +type key string + +const ( + teHeader key = "TeHeader" +) + +type TeTrailerRoundTripper struct { + http.RoundTripper +} + +func (t *TeTrailerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + teHeader := req.Context().Value(teHeader) + if teHeader != nil { + req.Header.Set("Te", teHeader.(string)) + } + return t.RoundTripper.RoundTrip(req) +} + +type TeTrailerRewriter struct { + ReqRewriter +} + +func (t *TeTrailerRewriter) Rewrite(req *http.Request) { + if req.Header.Get("Te") == "trailers" { + *req = *req.WithContext(context.WithValue(req.Context(), teHeader, req.Header.Get("Te"))) + } + t.ReqRewriter.Rewrite(req) +} + +func (f *Forwarder) postConfig() { + f.roundTripper = &TeTrailerRoundTripper{RoundTripper: f.roundTripper} + f.rewriter = &TeTrailerRewriter{ReqRewriter: f.rewriter} +} diff --git a/vendor/github.com/vulcand/oxy/forward/rewrite.go b/vendor/github.com/vulcand/oxy/forward/rewrite.go index 60c1a1947..b5f8da154 100644 --- a/vendor/github.com/vulcand/oxy/forward/rewrite.go +++ b/vendor/github.com/vulcand/oxy/forward/rewrite.go @@ -69,12 +69,6 @@ func (rw *HeaderRewriter) Rewrite(req *http.Request) { if rw.Hostname != "" { req.Header.Set(XForwardedServer, rw.Hostname) } - - if !IsWebsocketRequest(req) { - // Remove hop-by-hop headers to the backend. Especially important is "Connection" because we want a persistent - // connection, regardless of what the client sent to us. - utils.RemoveHeaders(req.Header, HopHeaders...) - } } func forwardedPort(req *http.Request) string { diff --git a/vendor/github.com/vulcand/oxy/utils/handler.go b/vendor/github.com/vulcand/oxy/utils/handler.go index 22d9c6900..24b9e3a88 100644 --- a/vendor/github.com/vulcand/oxy/utils/handler.go +++ b/vendor/github.com/vulcand/oxy/utils/handler.go @@ -1,6 +1,7 @@ package utils import ( + "context" "io" "net" "net/http" @@ -8,6 +9,12 @@ import ( log "github.com/sirupsen/logrus" ) +// StatusClientClosedRequest non-standard HTTP status code for client disconnection +const StatusClientClosedRequest = 499 + +// StatusClientClosedRequestText non-standard HTTP status for client disconnection +const StatusClientClosedRequestText = "Client Closed Request" + // ErrorHandler error handler type ErrorHandler interface { ServeHTTP(w http.ResponseWriter, req *http.Request, err error) @@ -21,6 +28,7 @@ type StdHandler struct{} func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) { statusCode := http.StatusInternalServerError + if e, ok := err.(net.Error); ok { if e.Timeout() { statusCode = http.StatusGatewayTimeout @@ -29,10 +37,20 @@ func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err err } } else if err == io.EOF { statusCode = http.StatusBadGateway + } else if err == context.Canceled { + statusCode = StatusClientClosedRequest } + w.WriteHeader(statusCode) - w.Write([]byte(http.StatusText(statusCode))) - log.Debugf("'%d %s' caused by: %v", statusCode, http.StatusText(statusCode), err) + w.Write([]byte(statusText(statusCode))) + log.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err) +} + +func statusText(statusCode int) string { + if statusCode == StatusClientClosedRequest { + return StatusClientClosedRequestText + } + return http.StatusText(statusCode) } // ErrorHandlerFunc error handler function type