From aa0ab6d387801e3ca5a62d851a8345e6097e2383 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 27 Mar 2018 10:24:03 +0200 Subject: [PATCH 01/21] Update SF white list documentation section. --- docs/configuration/backends/servicefabric.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration/backends/servicefabric.md b/docs/configuration/backends/servicefabric.md index 69310833f..203cb7f35 100644 --- a/docs/configuration/backends/servicefabric.md +++ b/docs/configuration/backends/servicefabric.md @@ -120,7 +120,8 @@ Labels, set through extensions or the property manager, can be used on services | `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.
Must be set with `traefik.frontend.redirect.regex`. | | `traefik.frontend.redirect.permanent=true` | Return 301 instead of 302. | | `traefik.frontend.rule=EXPR` | Override the default frontend rule. Defaults to SF address. | -| `traefik.frontend.whitelistSourceRange=RANGE` | 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.sourceRange=RANGE` | 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.useXForwardedFor=true` | Use `X-Forwarded-For` header as valid source of IP for the white list. | ### Custom Headers From 30e048d4ab8d6291ffb2d985af34a2eb47d1ec21 Mon Sep 17 00:00:00 2001 From: NicoMen Date: Tue, 27 Mar 2018 12:22:03 +0200 Subject: [PATCH 02/21] Fix panic with wrong ACME configuration --- configuration/configuration.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/configuration/configuration.go b/configuration/configuration.go index 7417da93c..7ecaa7f8d 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -331,15 +331,15 @@ func (gc *GlobalConfiguration) ValidateConfiguration() { log.Fatalf("Unknown entrypoint %q for ACME configuration", gc.ACME.EntryPoint) } else { if gc.EntryPoints[gc.ACME.EntryPoint].TLS == nil { - log.Fatalf("Entrypoint without TLS %q for ACME configuration", gc.ACME.EntryPoint) + log.Fatalf("Entrypoint %q has no TLS configuration for ACME configuration", gc.ACME.EntryPoint) } } } else if acmeprovider.IsEnabled() { if _, ok := gc.EntryPoints[acmeprovider.Get().EntryPoint]; !ok { - log.Fatalf("Unknown entrypoint %q for provider ACME configuration", gc.ACME.EntryPoint) + log.Fatalf("Unknown entrypoint %q for provider ACME configuration", acmeprovider.Get().EntryPoint) } else { if gc.EntryPoints[acmeprovider.Get().EntryPoint].TLS == nil { - log.Fatalf("Entrypoint without TLS %q for provider ACME configuration", gc.ACME.EntryPoint) + log.Fatalf("Entrypoint %q has no TLS configuration for provider ACME configuration", acmeprovider.Get().EntryPoint) } } } From 4c85a41bfbd009a350697e262272557e12d83e9c Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 27 Mar 2018 14:58:03 +0200 Subject: [PATCH 03/21] Fix basic documentation --- docs/basics.md | 15 +++++++-------- docs/configuration/entrypoints.md | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/basics.md b/docs/basics.md index 9a20d200e..800f944e1 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -62,13 +62,12 @@ And here is another example with client certificate authentication: [entryPoints.https] address = ":443" [entryPoints.https.tls] - [entryPoints.https.tls] - [entryPoints.https.tls.ClientCA] - files = ["tests/clientca1.crt", "tests/clientca2.crt"] - optional = false - [[entryPoints.https.tls.certificates]] - certFile = "tests/traefik.crt" - keyFile = "tests/traefik.key" + [entryPoints.https.tls.ClientCA] + files = ["tests/clientca1.crt", "tests/clientca2.crt"] + optional = false + [[entryPoints.https.tls.certificates]] + certFile = "tests/traefik.crt" + keyFile = "tests/traefik.key" ``` - We enable SSL on `https` by giving a certificate and a key. @@ -483,7 +482,7 @@ Each item takes precedence over the item below it: It means that arguments override configuration file, and key-value store overrides arguments. -!!! note +!!! note the provider-enabling argument parameters (e.g., `--docker`) set all default values for the specific provider. It must not be used if a configuration source with less precedence wants to set a non-default provider value. diff --git a/docs/configuration/entrypoints.md b/docs/configuration/entrypoints.md index 10208916e..7dfcf714e 100644 --- a/docs/configuration/entrypoints.md +++ b/docs/configuration/entrypoints.md @@ -365,7 +365,7 @@ To enable IP white listing at the entry point level. [entryPoints.http] address = ":80" - [entryPoints.http] + [entryPoints.http.whiteList] sourceRange = ["127.0.0.1/32", "192.168.1.7"] # useXForwardedFor = true ``` From f1a05ab73cc29b16016e5ac83f3c39722394f4f0 Mon Sep 17 00:00:00 2001 From: Tait Clarridge Date: Tue, 27 Mar 2018 10:18:03 -0400 Subject: [PATCH 04/21] Add wildcard match to acme domains --- acme/account.go | 3 ++ acme/acme.go | 22 +++------- acme/acme_test.go | 92 ++++++++++++++++++++++++++++++++++++++- provider/acme/provider.go | 14 ++---- server/server.go | 9 ++-- types/domain_test.go | 92 +++++++++++++++++++++++++++++++++++++++ types/domains.go | 21 +++++++++ 7 files changed, 219 insertions(+), 34 deletions(-) diff --git a/acme/account.go b/acme/account.go index 3215257c8..e32e68948 100644 --- a/acme/account.go +++ b/acme/account.go @@ -219,6 +219,9 @@ func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*Do for _, domainsCertificate := range dc.Certs { for _, domain := range domainsCertificate.Domains.ToStrArray() { + if strings.HasPrefix(domain, "*.") && types.MatchDomain(domainToFind, domain) { + return domainsCertificate, true + } if domain == domainToFind { return domainsCertificate, true } diff --git a/acme/acme.go b/acme/acme.go index ec4da2ba6..8e8634654 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -11,7 +11,6 @@ import ( "net/http" "os" "reflect" - "regexp" "strings" "time" @@ -27,7 +26,7 @@ import ( "github.com/containous/traefik/tls/generate" "github.com/containous/traefik/types" "github.com/eapache/channels" - acme "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/providers/dns" ) @@ -555,15 +554,14 @@ func (a *ACME) getProvidedCertificate(domains string) *tls.Certificate { func searchProvidedCertificateForDomains(domain string, certs map[string]*tls.Certificate) *tls.Certificate { // Use regex to test for provided certs that might have been added into TLSConfig for certDomains := range certs { - domainCheck := false + domainChecked := false for _, certDomain := range strings.Split(certDomains, ",") { - selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.", -1) + "$" - domainCheck, _ = regexp.MatchString(selector, domain) - if domainCheck { + domainChecked = types.MatchDomain(domain, certDomain) + if domainChecked { break } } - if domainCheck { + if domainChecked { log.Debugf("Domain %q checked by provided certificate %q", domain, certDomains) return certs[certDomains] } @@ -684,15 +682,7 @@ func (a *ACME) getValidDomains(domains []string, wildcardAllowed bool) ([]string func isDomainAlreadyChecked(domainToCheck string, existentDomains map[string]*tls.Certificate) bool { for certDomains := range existentDomains { for _, certDomain := range strings.Split(certDomains, ",") { - // Use regex to test for provided existentDomains that might have been added into TLSConfig - selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.", -1) + "$" - domainCheck, err := regexp.MatchString(selector, domainToCheck) - if err != nil { - log.Errorf("Unable to compare %q and %q : %s", domainToCheck, certDomain, err) - continue - } - - if domainCheck { + if types.MatchDomain(domainToCheck, certDomain) { return true } } diff --git a/acme/acme_test.go b/acme/acme_test.go index 39ef373cb..076308c82 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -14,7 +14,7 @@ import ( "github.com/containous/traefik/tls/generate" "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" - acme "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acmev2" ) func TestDomainsSet(t *testing.T) { @@ -444,3 +444,93 @@ func TestAcme_getValidDomain(t *testing.T) { }) } } + +func TestAcme_getCertificateForDomain(t *testing.T) { + testCases := []struct { + desc string + domain string + dc *DomainsCertificates + expected *DomainsCertificate + expectedFound bool + }{ + { + desc: "non-wildcard exact match", + domain: "foo.traefik.wtf", + dc: &DomainsCertificates{ + Certs: []*DomainsCertificate{ + { + Domains: types.Domain{ + Main: "foo.traefik.wtf", + }, + }, + }, + }, + expected: &DomainsCertificate{ + Domains: types.Domain{ + Main: "foo.traefik.wtf", + }, + }, + expectedFound: true, + }, + { + desc: "non-wildcard no match", + domain: "bar.traefik.wtf", + dc: &DomainsCertificates{ + Certs: []*DomainsCertificate{ + { + Domains: types.Domain{ + Main: "foo.traefik.wtf", + }, + }, + }, + }, + expected: nil, + expectedFound: false, + }, + { + desc: "wildcard match", + domain: "foo.traefik.wtf", + dc: &DomainsCertificates{ + Certs: []*DomainsCertificate{ + { + Domains: types.Domain{ + Main: "*.traefik.wtf", + }, + }, + }, + }, + expected: &DomainsCertificate{ + Domains: types.Domain{ + Main: "*.traefik.wtf", + }, + }, + expectedFound: true, + }, + { + desc: "wildcard no match", + domain: "foo.traefik.wtf", + dc: &DomainsCertificates{ + Certs: []*DomainsCertificate{ + { + Domains: types.Domain{ + Main: "*.bar.traefik.wtf", + }, + }, + }, + }, + expected: nil, + expectedFound: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got, found := test.dc.getCertificateForDomain(test.domain) + assert.Equal(t, test.expectedFound, found) + assert.Equal(t, test.expected, got) + }) + } +} diff --git a/provider/acme/provider.go b/provider/acme/provider.go index 3be1f46b9..c45db2d77 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -10,7 +10,6 @@ import ( "net/http" "os" "reflect" - "regexp" "strings" "sync" "time" @@ -24,7 +23,7 @@ import ( traefikTLS "github.com/containous/traefik/tls" "github.com/containous/traefik/types" "github.com/pkg/errors" - acme "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/providers/dns" ) @@ -522,7 +521,7 @@ func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurati } func searchUncheckedDomains(domainsToCheck []string, existentDomains []string) []string { - uncheckedDomains := []string{} + var uncheckedDomains []string for _, domainToCheck := range domainsToCheck { if !isDomainAlreadyChecked(domainToCheck, existentDomains) { uncheckedDomains = append(uncheckedDomains, domainToCheck) @@ -583,14 +582,7 @@ func (p *Provider) getValidDomains(domain types.Domain, wildcardAllowed bool) ([ func isDomainAlreadyChecked(domainToCheck string, existentDomains []string) bool { for _, certDomains := range existentDomains { for _, certDomain := range strings.Split(certDomains, ",") { - // Use regex to test for provided existentDomains that might have been added into TLSConfig - selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.", -1) + "$" - domainCheck, err := regexp.MatchString(selector, domainToCheck) - if err != nil { - log.Errorf("Unable to compare %q and %q in ACME provider : %s", domainToCheck, certDomain, err) - continue - } - if domainCheck { + if types.MatchDomain(domainToCheck, certDomain) { return true } } diff --git a/server/server.go b/server/server.go index 34b128bd9..b32b0cb2a 100644 --- a/server/server.go +++ b/server/server.go @@ -15,7 +15,6 @@ import ( "os" "os/signal" "reflect" - "regexp" "sort" "strings" "sync" @@ -517,15 +516,13 @@ func (s *Server) loadHTTPSConfiguration(configurations types.Configurations, def return newEPCertificates, nil } -// getCertificate allows to customize tlsConfig.Getcertificate behaviour to get the certificates inserted dynamically +// getCertificate allows to customize tlsConfig.GetCertificate behaviour to get the certificates inserted dynamically func (s *serverEntryPoint) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { domainToCheck := types.CanonicalDomain(clientHello.ServerName) if s.certs.Get() != nil { for domains, cert := range s.certs.Get().(map[string]*tls.Certificate) { - for _, domain := range strings.Split(domains, ",") { - selector := "^" + strings.Replace(domain, "*.", "[^\\.]*\\.?", -1) + "$" - domainCheck, _ := regexp.MatchString(selector, domainToCheck) - if domainCheck { + for _, certDomain := range strings.Split(domains, ",") { + if types.MatchDomain(domainToCheck, certDomain) { return cert, nil } } diff --git a/types/domain_test.go b/types/domain_test.go index 911064a9c..dc97c7971 100644 --- a/types/domain_test.go +++ b/types/domain_test.go @@ -88,3 +88,95 @@ func TestDomain_Set(t *testing.T) { }) } } + +func TestMatchDomain(t *testing.T) { + testCases := []struct { + desc string + certDomain string + domain string + expected bool + }{ + { + desc: "exact match", + certDomain: "traefik.wtf", + domain: "traefik.wtf", + expected: true, + }, + { + desc: "wildcard and root domain", + certDomain: "*.traefik.wtf", + domain: "traefik.wtf", + expected: false, + }, + { + desc: "wildcard and sub domain", + certDomain: "*.traefik.wtf", + domain: "sub.traefik.wtf", + expected: true, + }, + { + desc: "wildcard and sub sub domain", + certDomain: "*.traefik.wtf", + domain: "sub.sub.traefik.wtf", + expected: false, + }, + { + desc: "double wildcard and sub sub domain", + certDomain: "*.*.traefik.wtf", + domain: "sub.sub.traefik.wtf", + expected: true, + }, + { + desc: "sub sub domain and invalid wildcard", + certDomain: "sub.*.traefik.wtf", + domain: "sub.sub.traefik.wtf", + expected: false, + }, + { + desc: "sub sub domain and valid wildcard", + certDomain: "*.sub.traefik.wtf", + domain: "sub.sub.traefik.wtf", + expected: true, + }, + { + desc: "dot replaced by a cahr", + certDomain: "sub.sub.traefik.wtf", + domain: "sub.sub.traefikiwtf", + expected: false, + }, + { + desc: "*", + certDomain: "*", + domain: "sub.sub.traefik.wtf", + expected: false, + }, + { + desc: "?", + certDomain: "?", + domain: "sub.sub.traefik.wtf", + expected: false, + }, + { + desc: "...................", + certDomain: "...................", + domain: "sub.sub.traefik.wtf", + expected: false, + }, + { + desc: "wildcard and *", + certDomain: "*.traefik.wtf", + domain: "*.*.traefik.wtf", + expected: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + domains := MatchDomain(test.domain, test.certDomain) + assert.Equal(t, test.expected, domains) + }) + } +} diff --git a/types/domains.go b/types/domains.go index 47bae5468..2cace3f64 100644 --- a/types/domains.go +++ b/types/domains.go @@ -65,3 +65,24 @@ func (ds *Domains) String() string { return fmt.Sprintf("%+v", *ds) } func (ds *Domains) SetValue(val interface{}) { *ds = val.([]Domain) } + +// MatchDomain return true if a domain match the cert domain +func MatchDomain(domain string, certDomain string) bool { + if domain == certDomain { + return true + } + + for len(certDomain) > 0 && certDomain[len(certDomain)-1] == '.' { + certDomain = certDomain[:len(certDomain)-1] + } + + labels := strings.Split(domain, ".") + for i := range labels { + labels[i] = "*" + candidate := strings.Join(labels, ".") + if certDomain == candidate { + return true + } + } + return false +} From 118b4eb07a940e09da59cc5cb64f9dfe9d2df5c8 Mon Sep 17 00:00:00 2001 From: NicoMen Date: Tue, 27 Mar 2018 16:48:05 +0200 Subject: [PATCH 05/21] Prepare release v1.6.0-rc2 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8970c0b9..c6beb553b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## [v1.6.0-rc2](https://github.com/containous/traefik/tree/v1.6.0-rc2) (2018-03-27) +[All Commits](https://github.com/containous/traefik/compare/v1.6.0-rc1...v1.6.0-rc2) + +**Bug fixes:** +- **[acme]** Fix panic with wrong ACME configuration ([#3084](https://github.com/containous/traefik/pull/3084) by [nmengin](https://github.com/nmengin)) +- **[acme]** Fix wildcard match to ACME domains in cluster mode ([#3080](https://github.com/containous/traefik/pull/3080) by [oldmantaiter](https://github.com/oldmantaiter)) + +**Documentation:** +- **[servicefabric]** Update SF white list documentation section. ([#3082](https://github.com/containous/traefik/pull/3082) by [ldez](https://github.com/ldez)) +- Fix basic documentation ([#3086](https://github.com/containous/traefik/pull/3086) by [mmatur](https://github.com/mmatur)) + ## [v1.6.0-rc1](https://github.com/containous/traefik/tree/v1.6.0-rc1) (2018-03-26) [All Commits](https://github.com/containous/traefik/compare/v1.5.0-rc1...v1.6.0-rc1) From a731b43b52b8199c5f4f800ee2540e6e15203f02 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 28 Mar 2018 17:18:04 +0200 Subject: [PATCH 06/21] Frontend rule and segment labels. --- autogen/gentemplates/gen.go | 6 +++--- provider/docker/config.go | 8 ++++---- provider/docker/config_container_docker_test.go | 7 ++----- provider/docker/config_container_swarm_test.go | 5 +---- provider/label/segment.go | 4 +++- provider/rancher/config.go | 10 +++++----- provider/rancher/config_test.go | 11 +++++++++-- provider/rancher/deprecated_config.go | 11 +++++++++-- provider/rancher/deprecated_config_test.go | 2 +- templates/docker.tmpl | 2 +- templates/rancher.tmpl | 4 ++-- 11 files changed, 40 insertions(+), 30 deletions(-) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index 8b5745066..f64d7b52d 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -631,7 +631,7 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} {{end}} [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] - rule = "{{ getFrontendRule $container }}" + rule = "{{ getFrontendRule $container $container.SegmentLabels }}" {{end}} `) @@ -2011,8 +2011,8 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} {{end}} {{end}} - [frontends."frontend-{{$frontendName}}".routes."route-frontend-{{$frontendName}}"] - rule = "{{getFrontendRule $service}}" + [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] + rule = "{{ getFrontendRule $service.Name $service.SegmentLabels }}" {{end}} `) diff --git a/provider/docker/config.go b/provider/docker/config.go index 6d747bb7d..4615030f5 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -116,7 +116,7 @@ func (p *Provider) containerFilter(container dockerData) bool { for segmentName, labels := range segmentProperties { errPort = checkSegmentPort(labels, segmentName) - if len(p.getFrontendRule(container)) == 0 { + if len(p.getFrontendRule(container, labels)) == 0 { log.Debugf("Filtering container with empty frontend rule %s %s", container.Name, segmentName) return false } @@ -160,14 +160,14 @@ func (p *Provider) getFrontendName(container dockerData, idx int) string { if len(container.SegmentName) > 0 { name = getBackendName(container) } else { - name = p.getFrontendRule(container) + "-" + strconv.Itoa(idx) + name = p.getFrontendRule(container, container.SegmentLabels) + "-" + strconv.Itoa(idx) } return provider.Normalize(name) } -func (p *Provider) getFrontendRule(container dockerData) string { - if value := label.GetStringValue(container.SegmentLabels, label.TraefikFrontendRule, ""); len(value) != 0 { +func (p *Provider) getFrontendRule(container dockerData, segmentLabels map[string]string) string { + if value := label.GetStringValue(segmentLabels, label.TraefikFrontendRule, ""); len(value) != 0 { return value } diff --git a/provider/docker/config_container_docker_test.go b/provider/docker/config_container_docker_test.go index 7c78a84d6..d6b96f3b1 100644 --- a/provider/docker/config_container_docker_test.go +++ b/provider/docker/config_container_docker_test.go @@ -634,9 +634,6 @@ func TestDockerTraefikFilter(t *testing.T) { t.Parallel() dData := parseContainer(test.container) - segmentProperties := label.ExtractTraefikLabels(dData.Labels) - dData.SegmentLabels = segmentProperties[""] - actual := test.provider.containerFilter(dData) if actual != test.expected { t.Errorf("expected %v for %+v, got %+v", test.expected, test, actual) @@ -746,12 +743,12 @@ func TestDockerGetFrontendRule(t *testing.T) { dData := parseContainer(test.container) segmentProperties := label.ExtractTraefikLabels(dData.Labels) - dData.SegmentLabels = segmentProperties[""] provider := &Provider{ Domain: "docker.localhost", } - actual := provider.getFrontendRule(dData) + + actual := provider.getFrontendRule(dData, segmentProperties[""]) assert.Equal(t, test.expected, actual) }) } diff --git a/provider/docker/config_container_swarm_test.go b/provider/docker/config_container_swarm_test.go index 4623dafd9..5cab4c4a3 100644 --- a/provider/docker/config_container_swarm_test.go +++ b/provider/docker/config_container_swarm_test.go @@ -469,8 +469,6 @@ func TestSwarmTraefikFilter(t *testing.T) { t.Parallel() dData := parseService(test.service, test.networks) - segmentProperties := label.ExtractTraefikLabels(dData.Labels) - dData.SegmentLabels = segmentProperties[""] actual := test.provider.containerFilter(dData) if actual != test.expected { @@ -583,14 +581,13 @@ func TestSwarmGetFrontendRule(t *testing.T) { dData := parseService(test.service, test.networks) segmentProperties := label.ExtractTraefikLabels(dData.Labels) - dData.SegmentLabels = segmentProperties[""] provider := &Provider{ Domain: "docker.localhost", SwarmMode: true, } - actual := provider.getFrontendRule(dData) + actual := provider.getFrontendRule(dData, segmentProperties[""]) assert.Equal(t, test.expected, actual) }) } diff --git a/provider/label/segment.go b/provider/label/segment.go index 8274e4490..6eee9857c 100644 --- a/provider/label/segment.go +++ b/provider/label/segment.go @@ -128,7 +128,9 @@ func ExtractTraefikLabels(originLabels map[string]string) SegmentProperties { allLabels[segmentName][Prefix+propertyName] = value } } - log.Debug(originLabels, allLabels) + + log.Debug("originLabels", originLabels) + log.Debug("allLabels", allLabels) allLabels.mergeDefault() diff --git a/provider/rancher/config.go b/provider/rancher/config.go index 29fb6d84e..110e6db94 100644 --- a/provider/rancher/config.go +++ b/provider/rancher/config.go @@ -88,7 +88,7 @@ func (p *Provider) serviceFilter(service rancherData) bool { return false } - if len(p.getFrontendRule(service)) == 0 { + if len(p.getFrontendRule(service.Name, labels)) == 0 { log.Debugf("Filtering container with empty frontend rule %s %s", service.Name, segmentName) return false } @@ -123,9 +123,9 @@ func (p *Provider) serviceFilter(service rancherData) bool { return true } -func (p *Provider) getFrontendRule(service rancherData) string { - defaultRule := "Host:" + strings.ToLower(strings.Replace(service.Name, "/", ".", -1)) + "." + p.Domain - return label.GetStringValue(service.Labels, label.TraefikFrontendRule, defaultRule) +func (p *Provider) getFrontendRule(serviceName string, labels map[string]string) string { + defaultRule := "Host:" + strings.ToLower(strings.Replace(serviceName, "/", ".", -1)) + "." + p.Domain + return label.GetStringValue(labels, label.TraefikFrontendRule, defaultRule) } func (p *Provider) getFrontendName(service rancherData) string { @@ -133,7 +133,7 @@ func (p *Provider) getFrontendName(service rancherData) string { if len(service.SegmentName) > 0 { name = getBackendName(service) } else { - name = p.getFrontendRule(service) + name = p.getFrontendRule(service.Name, service.SegmentLabels) } return provider.Normalize(name) diff --git a/provider/rancher/config_test.go b/provider/rancher/config_test.go index 19da23e6e..88aca6fb6 100644 --- a/provider/rancher/config_test.go +++ b/provider/rancher/config_test.go @@ -260,6 +260,7 @@ func TestProviderBuildConfiguration(t *testing.T) { label.Prefix + "sauternes." + label.SuffixProtocol: "https", label.Prefix + "sauternes." + label.SuffixWeight: "12", + label.Prefix + "sauternes." + label.SuffixFrontendRule: "Host:traefik.wtf", 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", @@ -319,7 +320,7 @@ func TestProviderBuildConfiguration(t *testing.T) { Backend: "backend-sauternes", Routes: map[string]types.Route{ "route-frontend-sauternes": { - Rule: "Host:.rancher.localhost", + Rule: "Host:traefik.wtf", }, }, PassHostHeader: true, @@ -697,6 +698,9 @@ func TestProviderGetFrontendName(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() + segmentProperties := label.ExtractTraefikLabels(test.service.Labels) + test.service.SegmentLabels = segmentProperties[""] + actual := provider.getFrontendName(test.service) assert.Equal(t, test.expected, actual) }) @@ -762,7 +766,10 @@ func TestProviderGetFrontendRule(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := provider.getFrontendRule(test.service) + segmentProperties := label.ExtractTraefikLabels(test.service.Labels) + test.service.SegmentLabels = segmentProperties[""] + + actual := provider.getFrontendRule(test.service.Name, test.service.SegmentLabels) assert.Equal(t, test.expected, actual) }) } diff --git a/provider/rancher/deprecated_config.go b/provider/rancher/deprecated_config.go index 003eb4a33..fa30b3fad 100644 --- a/provider/rancher/deprecated_config.go +++ b/provider/rancher/deprecated_config.go @@ -1,6 +1,7 @@ package rancher import ( + "strings" "text/template" "github.com/BurntSushi/ty/fun" @@ -31,7 +32,7 @@ func (p *Provider) buildConfigurationV1(services []rancherData) *types.Configura // Frontend functions "getBackend": getBackendNameV1, - "getFrontendRule": p.getFrontendRule, + "getFrontendRule": p.getFrontendRuleV1, "getPriority": getFuncIntV1(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), "getPassHostHeader": getFuncBoolV1(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), "getEntryPoints": getFuncSliceStringV1(label.TraefikFrontendEntryPoints), @@ -109,9 +110,15 @@ func (p *Provider) serviceFilterV1(service rancherData) bool { return true } +// Deprecated +func (p *Provider) getFrontendRuleV1(service rancherData) string { + defaultRule := "Host:" + strings.ToLower(strings.Replace(service.Name, "/", ".", -1)) + "." + p.Domain + return label.GetStringValue(service.Labels, label.TraefikFrontendRule, defaultRule) +} + // Deprecated func (p *Provider) getFrontendNameV1(service rancherData) string { - return provider.Normalize(p.getFrontendRule(service)) + return provider.Normalize(p.getFrontendRuleV1(service)) } // Deprecated diff --git a/provider/rancher/deprecated_config_test.go b/provider/rancher/deprecated_config_test.go index c0d6d9836..ab9d6d4af 100644 --- a/provider/rancher/deprecated_config_test.go +++ b/provider/rancher/deprecated_config_test.go @@ -459,7 +459,7 @@ func TestProviderGetFrontendRuleV1(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := provider.getFrontendRule(test.service) + actual := provider.getFrontendRule(test.service.Name, test.service.Labels) assert.Equal(t, test.expected, actual) }) } diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 06a47c663..307275914 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -171,6 +171,6 @@ {{end}} [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] - rule = "{{ getFrontendRule $container }}" + rule = "{{ getFrontendRule $container $container.SegmentLabels }}" {{end}} diff --git a/templates/rancher.tmpl b/templates/rancher.tmpl index 32c30edbf..e78e5d9aa 100644 --- a/templates/rancher.tmpl +++ b/templates/rancher.tmpl @@ -169,7 +169,7 @@ {{end}} {{end}} - [frontends."frontend-{{$frontendName}}".routes."route-frontend-{{$frontendName}}"] - rule = "{{getFrontendRule $service}}" + [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] + rule = "{{ getFrontendRule $service.Name $service.SegmentLabels }}" {{end}} From bfb12f415c3c1653b1c0d8ff04d6daf5fe16c5ca Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 28 Mar 2018 17:56:04 +0200 Subject: [PATCH 07/21] Prepare release v1.6.0-rc3. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6beb553b..b87f997e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [v1.6.0-rc3](https://github.com/containous/traefik/tree/v1.6.0-rc3) (2018-03-28) +[All Commits](https://github.com/containous/traefik/compare/v1.6.0-rc2...v1.6.0-rc3) + +**Bug fixes:** +- **[docker,rancher]** Frontend rule and segment labels. ([#3091](https://github.com/containous/traefik/pull/3091) by [ldez](https://github.com/ldez)) + ## [v1.6.0-rc2](https://github.com/containous/traefik/tree/v1.6.0-rc2) (2018-03-27) [All Commits](https://github.com/containous/traefik/compare/v1.6.0-rc1...v1.6.0-rc2) From a820585f5614f7a2ab528043a105b31179714195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Kar=C5=9F=C4=B1yakal=C4=B1?= Date: Fri, 30 Mar 2018 12:18:03 +0300 Subject: [PATCH 08/21] Fixed documentation urls on README.md --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ea2074957..21465d20a 100644 --- a/README.md +++ b/README.md @@ -70,18 +70,18 @@ _(But if you'd rather configure some of your routes manually, Træfik supports t ## Supported Backends -- [Docker](docs/configuration/backends/docker/) / [Swarm mode](docs/configuration/backends/docker/#docker-swarm-mode) -- [Kubernetes](docs/configuration/backends/kubernetes/) -- [Mesos](docs/configuration/backends/mesos/) / [Marathon](docs/configuration/backends/marathon/) -- [Rancher](docs/configuration/backends/rancher/) (API, Metadata) -- [Service Fabric](docs/configuration/backends/servicefabric/) -- [Consul Catalog](docs/configuration/backends/consulcatalog/) -- [Consul](docs/configuration/backends/consul/) / [Etcd](docs/configuration/backends/etcd/) / [Zookeeper](docs/configuration/backends/zookeeper/) / [BoltDB](docs/configuration/backends/boltdb/) -- [Eureka](docs/configuration/backends/eureka/) -- [Amazon ECS](docs/configuration/backends/ecs/) -- [Amazon DynamoDB](docs/configuration/backends/dynamodb/) -- [File](docs/configuration/backends/file/) -- [Rest](docs/configuration/backends/rest/) +- [Docker](https://docs.traefik.io/configuration/backends/docker) / [Swarm mode](https://docs.traefik.io/configuration/backends/docker#docker-swarm-mode) +- [Kubernetes](https://docs.traefik.io/configuration/backends/kubernetes) +- [Mesos](https://docs.traefik.io/configuration/backends/mesos) / [Marathon](https://docs.traefik.io/configuration/backends/marathon) +- [Rancher](https://docs.traefik.io/configuration/backends/rancher) (API, Metadata) +- [Service Fabric](https://docs.traefik.io/configuration/backends/servicefabric) +- [Consul Catalog](https://docs.traefik.io/configuration/backends/consulcatalog) +- [Consul](https://docs.traefik.io/configuration/backends/consul) / [Etcd](https://docs.traefik.io/configuration/backends/etcd) / [Zookeeper](https://docs.traefik.io/configuration/backends/zookeeper) / [BoltDB](https://docs.traefik.io/configuration/backends/boltdb) +- [Eureka](https://docs.traefik.io/configuration/backends/eureka) +- [Amazon ECS](https://docs.traefik.io/configuration/backends/ecs) +- [Amazon DynamoDB](https://docs.traefik.io/configuration/backends/dynamodb) +- [File](https://docs.traefik.io/configuration/backends/file) +- [Rest](https://docs.traefik.io/configuration/backends/rest) ## Quickstart From a179c3b399391080262b1e4925a3f91c92d29f91 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 3 Apr 2018 09:40:04 +0200 Subject: [PATCH 09/21] Fixes prefixed annotations support. --- provider/kubernetes/annotations.go | 4 +- provider/kubernetes/annotations_test.go | 52 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 provider/kubernetes/annotations_test.go diff --git a/provider/kubernetes/annotations.go b/provider/kubernetes/annotations.go index 9b2e6ab89..4b254ef1a 100644 --- a/provider/kubernetes/annotations.go +++ b/provider/kubernetes/annotations.go @@ -77,13 +77,13 @@ func getAnnotationName(annotations map[string]string, name string) string { } if _, ok := annotations[label.Prefix+name]; ok { - return name + return label.Prefix + name } // TODO [breaking] remove label support if lbl, compat := compatibilityMapping[name]; compat { if _, ok := annotations[lbl]; ok { - return name + return lbl } } diff --git a/provider/kubernetes/annotations_test.go b/provider/kubernetes/annotations_test.go new file mode 100644 index 000000000..d84e0d272 --- /dev/null +++ b/provider/kubernetes/annotations_test.go @@ -0,0 +1,52 @@ +package kubernetes + +import ( + "testing" + + "github.com/containous/traefik/provider/label" + "github.com/stretchr/testify/assert" +) + +func TestGetAnnotationName(t *testing.T) { + testCases := []struct { + desc string + annotations map[string]string + name string + expected string + }{ + { + desc: "with standard annotation", + name: annotationKubernetesPreserveHost, + annotations: map[string]string{ + annotationKubernetesPreserveHost: "true", + }, + expected: annotationKubernetesPreserveHost, + }, + { + desc: "with prefixed annotation", + name: annotationKubernetesPreserveHost, + annotations: map[string]string{ + label.Prefix + annotationKubernetesPreserveHost: "true", + }, + expected: label.Prefix + annotationKubernetesPreserveHost, + }, + { + desc: "with label", + name: annotationKubernetesPreserveHost, + annotations: map[string]string{ + label.TraefikFrontendPassHostHeader: "true", + }, + expected: label.TraefikFrontendPassHostHeader, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getAnnotationName(test.annotations, test.name) + assert.Equal(t, test.expected, actual) + }) + } +} From ff325293452f0212488ba1f909ece239bb7486fc Mon Sep 17 00:00:00 2001 From: Alex Antonov Date: Tue, 3 Apr 2018 03:00:07 -0500 Subject: [PATCH 10/21] Moved /api/cluster/leadership handler under public routes (requires no authentication) --- server/server.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/server.go b/server/server.go index b32b0cb2a..30ea7bf6f 100644 --- a/server/server.go +++ b/server/server.go @@ -744,9 +744,6 @@ func (s *Server) addInternalRoutes(entryPointName string, router *mux.Router) { if s.globalConfiguration.API != nil && s.globalConfiguration.API.EntryPoint == entryPointName { s.globalConfiguration.API.AddRoutes(router) - if s.leadership != nil { - s.leadership.AddRoutes(router) - } } } @@ -754,6 +751,10 @@ func (s *Server) addInternalPublicRoutes(entryPointName string, router *mux.Rout if s.globalConfiguration.Ping != nil && s.globalConfiguration.Ping.EntryPoint != "" && s.globalConfiguration.Ping.EntryPoint == entryPointName { s.globalConfiguration.Ping.AddRoutes(router) } + + if s.globalConfiguration.API != nil && s.globalConfiguration.API.EntryPoint == entryPointName && s.leadership != nil { + s.leadership.AddRoutes(router) + } } func (s *Server) addACMERoutes(entryPointName string, router *mux.Router) { From 197a5fbcf46036669662314c82da285b11e5087c Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Tue, 3 Apr 2018 10:30:03 +0200 Subject: [PATCH 11/21] Update kubernetes.md --- docs/user-guide/kubernetes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index a970af6ca..1d428542e 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -371,7 +371,7 @@ spec: serviceName: traefik-web-ui servicePort: 80 tls: - secretName: traefik-ui-tls-cert + - secretName: traefik-ui-tls-cert ``` In addition to the modified ingress you need to provide the TLS certificate via a Kubernetes secret in the same namespace as the ingress. The following two commands will generate a new certificate and create a secret containing the key and cert files. From 1b935515729f7872962827f7b303dd4556f81ccd Mon Sep 17 00:00:00 2001 From: jakeprem Date: Tue, 3 Apr 2018 05:14:04 -0400 Subject: [PATCH 12/21] Update docker-and-lets-encrypt example to show traefik:1.5.4 --- docs/user-guide/docker-and-lets-encrypt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/docker-and-lets-encrypt.md b/docs/user-guide/docker-and-lets-encrypt.md index 1e415251d..d44610626 100644 --- a/docs/user-guide/docker-and-lets-encrypt.md +++ b/docs/user-guide/docker-and-lets-encrypt.md @@ -50,7 +50,7 @@ version: '2' services: traefik: - image: traefik:1.3.5 + image: traefik:1.5.4 restart: always ports: - 80:80 From 5921909ef59b4db0ec9d807989d18c09bb124196 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 3 Apr 2018 18:36:03 +0200 Subject: [PATCH 13/21] Add tests on IPWhiteLister. --- middlewares/ip_whitelister.go | 2 +- middlewares/ip_whitelister_test.go | 129 +++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 middlewares/ip_whitelister_test.go diff --git a/middlewares/ip_whitelister.go b/middlewares/ip_whitelister.go index 082c12348..60ff8a30e 100644 --- a/middlewares/ip_whitelister.go +++ b/middlewares/ip_whitelister.go @@ -32,7 +32,7 @@ func NewIPWhiteLister(whiteList []string, useXForwardedFor bool) (*IPWhiteLister whiteLister.whiteLister = ip whiteLister.handler = negroni.HandlerFunc(whiteLister.handle) - log.Debugf("configured %u IP white list: %s", len(whiteList), whiteList) + log.Debugf("configured IP white list: %s", whiteList) return &whiteLister, nil } diff --git a/middlewares/ip_whitelister_test.go b/middlewares/ip_whitelister_test.go new file mode 100644 index 000000000..33b0c56e5 --- /dev/null +++ b/middlewares/ip_whitelister_test.go @@ -0,0 +1,129 @@ +package middlewares + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/containous/traefik/whitelist" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewIPWhiteLister(t *testing.T) { + testCases := []struct { + desc string + whiteList []string + useXForwardedFor bool + expectedError string + }{ + { + desc: "invalid IP", + whiteList: []string{"foo"}, + useXForwardedFor: false, + expectedError: "parsing CIDR whitelist [foo]: parsing CIDR white list : invalid CIDR address: foo", + }, + { + desc: "valid IP", + whiteList: []string{"10.10.10.10"}, + useXForwardedFor: false, + expectedError: "", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + whiteLister, err := NewIPWhiteLister(test.whiteList, test.useXForwardedFor) + + if len(test.expectedError) > 0 { + assert.EqualError(t, err, test.expectedError) + } else { + require.NoError(t, err) + assert.NotNil(t, whiteLister) + } + }) + } +} + +func TestIPWhiteLister_ServeHTTP(t *testing.T) { + testCases := []struct { + desc string + whiteList []string + useXForwardedFor bool + remoteAddr string + xForwardedFor []string + expected int + }{ + { + desc: "authorized with remote address", + whiteList: []string{"20.20.20.20"}, + useXForwardedFor: false, + remoteAddr: "20.20.20.20:1234", + xForwardedFor: nil, + expected: 200, + }, + { + desc: "non authorized with remote address", + whiteList: []string{"20.20.20.20"}, + useXForwardedFor: false, + remoteAddr: "20.20.20.21:1234", + xForwardedFor: nil, + expected: 403, + }, + { + desc: "non authorized with remote address (X-Forwarded-For possible)", + whiteList: []string{"20.20.20.20"}, + useXForwardedFor: false, + remoteAddr: "20.20.20.21:1234", + xForwardedFor: []string{"20.20.20.20", "40.40.40.40"}, + expected: 403, + }, + { + desc: "authorized with X-Forwarded-For", + whiteList: []string{"30.30.30.30"}, + useXForwardedFor: true, + xForwardedFor: []string{"30.30.30.30", "40.40.40.40"}, + expected: 200, + }, + { + desc: "non authorized with X-Forwarded-For", + whiteList: []string{"30.30.30.30"}, + useXForwardedFor: true, + xForwardedFor: []string{"30.30.30.31", "40.40.40.40"}, + expected: 403, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + whiteLister, err := NewIPWhiteLister(test.whiteList, test.useXForwardedFor) + require.NoError(t, err) + + recorder := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "http://10.10.10.10", nil) + + if len(test.remoteAddr) > 0 { + req.RemoteAddr = test.remoteAddr + } + + if len(test.xForwardedFor) > 0 { + for _, xff := range test.xForwardedFor { + req.Header.Add(whitelist.XForwardedFor, xff) + } + } + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + whiteLister.ServeHTTP(recorder, req, next) + + assert.Equal(t, test.expected, recorder.Code) + }) + } +} From 46db91ce73042d4f8d9cc08da74ea4cc9f032e76 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Wed, 28 Mar 2018 02:13:48 +0200 Subject: [PATCH 14/21] refactor: ECS labels. --- configuration/configuration.go | 9 + provider/ecs/config.go | 425 +------------ provider/ecs/config_root.go | 12 + provider/ecs/config_test.go | 834 ++----------------------- provider/ecs/deprecated_config.go | 196 ++++++ provider/ecs/deprecated_config_test.go | 292 +++++++++ provider/ecs/ecs.go | 23 +- templates/ecs-v1.tmpl | 44 ++ templates/ecs.tmpl | 32 +- 9 files changed, 643 insertions(+), 1224 deletions(-) create mode 100644 provider/ecs/config_root.go create mode 100644 provider/ecs/deprecated_config.go create mode 100644 provider/ecs/deprecated_config_test.go create mode 100644 templates/ecs-v1.tmpl diff --git a/configuration/configuration.go b/configuration/configuration.go index 7ecaa7f8d..f3fb3516c 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -237,6 +237,15 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) { } } + if gc.ECS != nil { + if len(gc.ECS.Filename) != 0 && gc.ECS.TemplateVersion != 2 { + log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.") + gc.ECS.TemplateVersion = 1 + } else { + gc.ECS.TemplateVersion = 2 + } + } + if gc.Rancher != nil { if len(gc.Rancher.Filename) != 0 && gc.Rancher.TemplateVersion != 2 { log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.") diff --git a/provider/ecs/config.go b/provider/ecs/config.go index a61add0b1..6a226dbd2 100644 --- a/provider/ecs/config.go +++ b/provider/ecs/config.go @@ -2,65 +2,45 @@ package ecs import ( "fmt" - "math" "strconv" "strings" "text/template" "github.com/BurntSushi/ty/fun" "github.com/aws/aws-sdk-go/aws" - "github.com/containous/traefik/log" "github.com/containous/traefik/provider" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" ) // buildConfiguration fills the config template with the given instances -func (p *Provider) buildConfiguration(services map[string][]ecsInstance) (*types.Configuration, error) { +func (p *Provider) buildConfigurationV2(services map[string][]ecsInstance) (*types.Configuration, error) { var ecsFuncMap = template.FuncMap{ // Backend functions "getHost": getHost, "getPort": getPort, - "getCircuitBreaker": getCircuitBreaker, - "getLoadBalancer": getLoadBalancer, - "getMaxConn": getMaxConn, - "getHealthCheck": getHealthCheck, - "getBuffering": getBuffering, + "getCircuitBreaker": label.GetCircuitBreaker, + "getLoadBalancer": label.GetLoadBalancer, + "getMaxConn": label.GetMaxConn, + "getHealthCheck": label.GetHealthCheck, + "getBuffering": label.GetBuffering, "getServers": getServers, - // TODO Deprecated [breaking] - "getProtocol": getFuncStringValue(label.TraefikProtocol, label.DefaultProtocol), - // TODO Deprecated [breaking] - "getWeight": getFuncIntValue(label.TraefikWeight, label.DefaultWeightInt), - // TODO Deprecated [breaking] - "getLoadBalancerMethod": getFuncFirstStringValue(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), - // TODO Deprecated [breaking] - "getSticky": getSticky, - // TODO Deprecated [breaking] - "hasStickinessLabel": getFuncFirstBoolValue(label.TraefikBackendLoadBalancerStickiness, false), - // TODO Deprecated [breaking] - "getStickinessCookieName": getFuncFirstStringValue(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), - // TODO Deprecated [breaking] - "hasHealthCheckLabels": hasFuncFirst(label.TraefikBackendHealthCheckPath), - // TODO Deprecated [breaking] - "getHealthCheckPath": getFuncFirstStringValue(label.TraefikBackendHealthCheckPath, ""), - // TODO Deprecated [breaking] - "getHealthCheckInterval": getFuncFirstStringValue(label.TraefikBackendHealthCheckInterval, ""), - // Frontend functions "filterFrontends": filterFrontends, "getFrontendRule": p.getFrontendRule, - "getPassHostHeader": getFuncBoolValue(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), - "getPassTLSCert": getFuncBoolValue(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), - "getPriority": getFuncIntValue(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), - "getBasicAuth": getFuncSliceString(label.TraefikFrontendAuthBasic), - "getEntryPoints": getFuncSliceString(label.TraefikFrontendEntryPoints), - "getRedirect": getRedirect, - "getErrorPages": getErrorPages, - "getRateLimit": getRateLimit, - "getHeaders": getHeaders, - "getWhiteList": getWhiteList, + "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), + "getEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints), + "getRedirect": label.GetRedirect, + "getErrorPages": label.GetErrorPages, + "getRateLimit": label.GetRateLimit, + "getHeaders": label.GetHeaders, + "getWhiteList": label.GetWhiteList, } + return p.GetConfiguration("templates/ecs.tmpl", ecsFuncMap, struct { Services map[string][]ecsInstance }{ @@ -70,27 +50,7 @@ func (p *Provider) buildConfiguration(services map[string][]ecsInstance) (*types func (p *Provider) getFrontendRule(i ecsInstance) string { defaultRule := "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + p.Domain - return getStringValue(i, label.TraefikFrontendRule, defaultRule) -} - -// TODO: Deprecated -// replaced by Stickiness -// Deprecated -func getSticky(instances []ecsInstance) bool { - if hasFirst(instances, label.TraefikBackendLoadBalancerSticky) { - log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) - } - return getFirstBoolValue(instances, label.TraefikBackendLoadBalancerSticky, false) -} - -// TODO: Deprecated -// replaced by Stickiness -// Deprecated -func getStickyOne(instance ecsInstance) bool { - if hasLabel(instance, label.TraefikBackendLoadBalancerSticky) { - log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) - } - return getBoolValue(instance, label.TraefikBackendLoadBalancerSticky, false) + return label.GetStringValue(i.TraefikLabels, label.TraefikFrontendRule, defaultRule) } func getHost(i ecsInstance) string { @@ -98,7 +58,7 @@ func getHost(i ecsInstance) string { } func getPort(i ecsInstance) string { - if value := getStringValue(i, label.TraefikPort, ""); len(value) > 0 { + if value := label.GetStringValue(i.TraefikLabels, label.TraefikPort, ""); len(value) > 0 { return value } return strconv.FormatInt(aws.Int64Value(i.container.NetworkBindings[0].HostPort), 10) @@ -116,79 +76,6 @@ func filterFrontends(instances []ecsInstance) []ecsInstance { }, instances).([]ecsInstance) } -func getCircuitBreaker(instance ecsInstance) *types.CircuitBreaker { - expression := getStringValue(instance, label.TraefikBackendCircuitBreakerExpression, "") - if len(expression) == 0 { - return nil - } - - return &types.CircuitBreaker{Expression: expression} -} - -func getLoadBalancer(instance ecsInstance) *types.LoadBalancer { - if !hasPrefix(instance, label.TraefikBackendLoadBalancer) { - return nil - } - - method := getStringValue(instance, label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod) - - lb := &types.LoadBalancer{ - Method: method, - Sticky: getStickyOne(instance), - } - - if getBoolValue(instance, label.TraefikBackendLoadBalancerStickiness, false) { - cookieName := getStringValue(instance, label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName) - lb.Stickiness = &types.Stickiness{CookieName: cookieName} - } - - return lb -} - -func getMaxConn(instance ecsInstance) *types.MaxConn { - amount := getInt64Value(instance, label.TraefikBackendMaxConnAmount, math.MinInt64) - extractorFunc := getStringValue(instance, label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) - - if amount == math.MinInt64 || len(extractorFunc) == 0 { - return nil - } - - return &types.MaxConn{ - Amount: amount, - ExtractorFunc: extractorFunc, - } -} - -func getHealthCheck(instance ecsInstance) *types.HealthCheck { - path := getStringValue(instance, label.TraefikBackendHealthCheckPath, "") - if len(path) == 0 { - return nil - } - - port := getIntValue(instance, label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort) - interval := getStringValue(instance, label.TraefikBackendHealthCheckInterval, "") - - return &types.HealthCheck{ - Path: path, - Port: port, - Interval: interval, - } -} - -func getBuffering(instance ecsInstance) *types.Buffering { - if !hasPrefix(instance, label.TraefikBackendBuffering) { - return nil - } - - return &types.Buffering{ - MaxRequestBodyBytes: getInt64Value(instance, label.TraefikBackendBufferingMaxRequestBodyBytes, 0), - MaxResponseBodyBytes: getInt64Value(instance, label.TraefikBackendBufferingMaxResponseBodyBytes, 0), - MemRequestBodyBytes: getInt64Value(instance, label.TraefikBackendBufferingMemRequestBodyBytes, 0), - MemResponseBodyBytes: getInt64Value(instance, label.TraefikBackendBufferingMemResponseBodyBytes, 0), - RetryExpression: getStringValue(instance, label.TraefikBackendBufferingRetryExpression, ""), - } -} - func getServers(instances []ecsInstance) map[string]types.Server { var servers map[string]types.Server @@ -197,288 +84,20 @@ func getServers(instances []ecsInstance) map[string]types.Server { servers = make(map[string]types.Server) } - protocol := getStringValue(instance, label.TraefikProtocol, label.DefaultProtocol) + protocol := label.GetStringValue(instance.TraefikLabels, label.TraefikProtocol, label.DefaultProtocol) host := getHost(instance) port := getPort(instance) serverName := provider.Normalize(fmt.Sprintf("server-%s-%s", instance.Name, instance.ID)) servers[serverName] = types.Server{ URL: fmt.Sprintf("%s://%s:%s", protocol, host, port), - Weight: getIntValue(instance, label.TraefikWeight, 0), + Weight: label.GetIntValue(instance.TraefikLabels, label.TraefikWeight, 0), } } return servers } -func getWhiteList(instance ecsInstance) *types.WhiteList { - ranges := getSliceString(instance, label.TraefikFrontendWhiteListSourceRange) - if len(ranges) > 0 { - return &types.WhiteList{ - SourceRange: ranges, - UseXForwardedFor: getBoolValue(instance, label.TraefikFrontendWhiteListUseXForwardedFor, false), - } - } - - return nil -} - -func getRedirect(instance ecsInstance) *types.Redirect { - permanent := getBoolValue(instance, label.TraefikFrontendRedirectPermanent, false) - - if hasLabel(instance, label.TraefikFrontendRedirectEntryPoint) { - return &types.Redirect{ - EntryPoint: getStringValue(instance, label.TraefikFrontendRedirectEntryPoint, ""), - Permanent: permanent, - } - } - - if hasLabel(instance, label.TraefikFrontendRedirectRegex) && - hasLabel(instance, label.TraefikFrontendRedirectReplacement) { - return &types.Redirect{ - Regex: getStringValue(instance, label.TraefikFrontendRedirectRegex, ""), - Replacement: getStringValue(instance, label.TraefikFrontendRedirectReplacement, ""), - Permanent: permanent, - } - } - - return nil -} - -func getErrorPages(instance ecsInstance) map[string]*types.ErrorPage { - labels := mapPToMap(instance.containerDefinition.DockerLabels) - if len(labels) == 0 { - return nil - } - - prefix := label.Prefix + label.BaseFrontendErrorPage - return label.ParseErrorPages(labels, prefix, label.RegexpFrontendErrorPage) -} - -func getRateLimit(instance ecsInstance) *types.RateLimit { - extractorFunc := getStringValue(instance, label.TraefikFrontendRateLimitExtractorFunc, "") - if len(extractorFunc) == 0 { - return nil - } - - labels := mapPToMap(instance.containerDefinition.DockerLabels) - prefix := label.Prefix + label.BaseFrontendRateLimit - limits := label.ParseRateSets(labels, prefix, label.RegexpFrontendRateLimit) - - return &types.RateLimit{ - ExtractorFunc: extractorFunc, - RateSet: limits, - } -} - -func getHeaders(instance ecsInstance) *types.Headers { - headers := &types.Headers{ - CustomRequestHeaders: getMapString(instance, label.TraefikFrontendRequestHeaders), - CustomResponseHeaders: getMapString(instance, label.TraefikFrontendResponseHeaders), - SSLProxyHeaders: getMapString(instance, label.TraefikFrontendSSLProxyHeaders), - AllowedHosts: getSliceString(instance, label.TraefikFrontendAllowedHosts), - HostsProxyHeaders: getSliceString(instance, label.TraefikFrontendHostsProxyHeaders), - STSSeconds: getInt64Value(instance, label.TraefikFrontendSTSSeconds, 0), - SSLRedirect: getBoolValue(instance, label.TraefikFrontendSSLRedirect, false), - SSLTemporaryRedirect: getBoolValue(instance, label.TraefikFrontendSSLTemporaryRedirect, false), - STSIncludeSubdomains: getBoolValue(instance, label.TraefikFrontendSTSIncludeSubdomains, false), - STSPreload: getBoolValue(instance, label.TraefikFrontendSTSPreload, false), - ForceSTSHeader: getBoolValue(instance, label.TraefikFrontendForceSTSHeader, false), - FrameDeny: getBoolValue(instance, label.TraefikFrontendFrameDeny, false), - ContentTypeNosniff: getBoolValue(instance, label.TraefikFrontendContentTypeNosniff, false), - BrowserXSSFilter: getBoolValue(instance, label.TraefikFrontendBrowserXSSFilter, false), - IsDevelopment: getBoolValue(instance, label.TraefikFrontendIsDevelopment, false), - SSLHost: getStringValue(instance, label.TraefikFrontendSSLHost, ""), - CustomFrameOptionsValue: getStringValue(instance, label.TraefikFrontendCustomFrameOptionsValue, ""), - ContentSecurityPolicy: getStringValue(instance, label.TraefikFrontendContentSecurityPolicy, ""), - PublicKey: getStringValue(instance, label.TraefikFrontendPublicKey, ""), - ReferrerPolicy: getStringValue(instance, label.TraefikFrontendReferrerPolicy, ""), - CustomBrowserXSSValue: getStringValue(instance, label.TraefikFrontendCustomBrowserXSSValue, ""), - } - - if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { - return nil - } - - return headers -} - -// Label functions - -func getFuncStringValue(labelName string, defaultValue string) func(i ecsInstance) string { - return func(i ecsInstance) string { - return getStringValue(i, labelName, defaultValue) - } -} - -func getFuncBoolValue(labelName string, defaultValue bool) func(i ecsInstance) bool { - return func(i ecsInstance) bool { - return getBoolValue(i, labelName, defaultValue) - } -} - -func getFuncIntValue(labelName string, defaultValue int) func(i ecsInstance) int { - return func(i ecsInstance) int { - return getIntValue(i, labelName, defaultValue) - } -} - -func getFuncSliceString(labelName string) func(i ecsInstance) []string { - return func(i ecsInstance) []string { - return getSliceString(i, labelName) - } -} - -// Deprecated -func hasFuncFirst(labelName string) func(instances []ecsInstance) bool { - return func(instances []ecsInstance) bool { - return hasFirst(instances, labelName) - } -} - -// Deprecated -func getFuncFirstStringValue(labelName string, defaultValue string) func(instances []ecsInstance) string { - return func(instances []ecsInstance) string { - return getFirstStringValue(instances, labelName, defaultValue) - } -} - -// Deprecated -func getFuncFirstBoolValue(labelName string, defaultValue bool) func(instances []ecsInstance) bool { - return func(instances []ecsInstance) bool { - if len(instances) < 0 { - return defaultValue - } - return getBoolValue(instances[0], labelName, defaultValue) - } -} - -func hasLabel(i ecsInstance, labelName string) bool { - value, ok := i.containerDefinition.DockerLabels[labelName] - return ok && value != nil && len(aws.StringValue(value)) > 0 -} - -func hasPrefix(i ecsInstance, prefix string) bool { - for name, value := range i.containerDefinition.DockerLabels { - if strings.HasPrefix(name, prefix) && value != nil && len(aws.StringValue(value)) > 0 { - return true - } - } - return false -} - -func getStringValue(i ecsInstance, labelName string, defaultValue string) string { - if v, ok := i.containerDefinition.DockerLabels[labelName]; ok { - if v == nil { - return defaultValue - } - if len(aws.StringValue(v)) == 0 { - return defaultValue - } - return aws.StringValue(v) - } - return defaultValue -} - -func getBoolValue(i ecsInstance, labelName string, defaultValue bool) bool { - rawValue, ok := i.containerDefinition.DockerLabels[labelName] - if ok { - if rawValue != nil { - v, err := strconv.ParseBool(aws.StringValue(rawValue)) - if err == nil { - return v - } - } - } - return defaultValue -} - -func getIntValue(i ecsInstance, labelName string, defaultValue int) int { - rawValue, ok := i.containerDefinition.DockerLabels[labelName] - if ok { - if rawValue != nil { - v, err := strconv.Atoi(aws.StringValue(rawValue)) - if err == nil { - return v - } - } - } - return defaultValue -} - -func getInt64Value(i ecsInstance, labelName string, defaultValue int64) int64 { - rawValue, ok := i.containerDefinition.DockerLabels[labelName] - if ok { - if rawValue != nil { - v, err := strconv.ParseInt(aws.StringValue(rawValue), 10, 64) - if err == nil { - return v - } - } - } - return defaultValue -} - -func getSliceString(i ecsInstance, labelName string) []string { - if value, ok := i.containerDefinition.DockerLabels[labelName]; ok { - if value == nil { - return nil - } - if len(aws.StringValue(value)) == 0 { - return nil - } - return label.SplitAndTrimString(aws.StringValue(value), ",") - } - return nil -} - -func getMapString(i ecsInstance, labelName string) map[string]string { - if value, ok := i.containerDefinition.DockerLabels[labelName]; ok { - if value == nil { - return nil - } - if len(aws.StringValue(value)) == 0 { - return nil - } - return label.ParseMapValue(labelName, aws.StringValue(value)) - } - return nil -} - -// Deprecated -func hasFirst(instances []ecsInstance, labelName string) bool { - if len(instances) == 0 { - return false - } - return hasLabel(instances[0], labelName) -} - -// Deprecated -func getFirstStringValue(instances []ecsInstance, labelName string, defaultValue string) string { - if len(instances) == 0 { - return defaultValue - } - return getStringValue(instances[0], labelName, defaultValue) -} - -// Deprecated -func getFirstBoolValue(instances []ecsInstance, labelName string, defaultValue bool) bool { - if len(instances) == 0 { - return defaultValue - } - return getBoolValue(instances[0], labelName, defaultValue) -} - -func mapPToMap(src map[string]*string) map[string]string { - result := make(map[string]string) - for key, value := range src { - if value != nil && len(aws.StringValue(value)) > 0 { - result[key] = aws.StringValue(value) - } - } - return result -} - func isEnabled(i ecsInstance, exposedByDefault bool) bool { - return getBoolValue(i, label.TraefikEnable, exposedByDefault) + return label.GetBoolValue(i.TraefikLabels, label.TraefikEnable, exposedByDefault) } diff --git a/provider/ecs/config_root.go b/provider/ecs/config_root.go new file mode 100644 index 000000000..318feddfd --- /dev/null +++ b/provider/ecs/config_root.go @@ -0,0 +1,12 @@ +package ecs + +import ( + "github.com/containous/traefik/types" +) + +func (p *Provider) buildConfiguration(services map[string][]ecsInstance) (*types.Configuration, error) { + if p.TemplateVersion == 1 { + return p.buildConfigurationV1(services) + } + return p.buildConfigurationV2(services) +} diff --git a/provider/ecs/config_test.go b/provider/ecs/config_test.go index 9860869b9..d9af4de67 100644 --- a/provider/ecs/config_test.go +++ b/provider/ecs/config_test.go @@ -14,7 +14,7 @@ import ( ) func TestBuildConfiguration(t *testing.T) { - tests := []struct { + testCases := []struct { desc string services map[string][]ecsInstance expected *types.Configuration @@ -346,14 +346,17 @@ func TestBuildConfiguration(t *testing.T) { }, } - for _, test := range tests { + for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() provider := &Provider{} - got, err := provider.buildConfiguration(test.services) - assert.Equal(t, test.err, err) + + services := fakeLoadTraefikLabels(test.services) + + got, err := provider.buildConfiguration(services) + assert.Equal(t, test.err, err) // , err.Error() assert.Equal(t, test.expected, got, test.desc) }) } @@ -381,7 +384,7 @@ func TestFilterInstance(t *testing.T) { label.TraefikPort: aws.String("80"), }) - tests := []struct { + testCases := []struct { desc string instanceInfo ecsInstance exposedByDefault bool @@ -459,7 +462,7 @@ func TestFilterInstance(t *testing.T) { }, } - for _, test := range tests { + for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() @@ -475,7 +478,7 @@ func TestFilterInstance(t *testing.T) { func TestChunkedTaskArns(t *testing.T) { testVal := "a" - tests := []struct { + testCases := []struct { desc string count int expectedLengths []int @@ -532,7 +535,7 @@ func TestChunkedTaskArns(t *testing.T) { }, } - for _, test := range tests { + for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() @@ -555,7 +558,7 @@ func TestChunkedTaskArns(t *testing.T) { } func TestGetHost(t *testing.T) { - tests := []struct { + testCases := []struct { desc string expected string instanceInfo ecsInstance @@ -567,7 +570,7 @@ func TestGetHost(t *testing.T) { }, } - for _, test := range tests { + for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() @@ -579,7 +582,7 @@ func TestGetHost(t *testing.T) { } func TestGetPort(t *testing.T) { - tests := []struct { + testCases := []struct { desc string expected string instanceInfo ecsInstance @@ -605,7 +608,7 @@ func TestGetPort(t *testing.T) { }, } - for _, test := range tests { + for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() @@ -617,7 +620,7 @@ func TestGetPort(t *testing.T) { } func TestGetFuncStringValue(t *testing.T) { - tests := []struct { + testCases := []struct { desc string expected string instanceInfo ecsInstance @@ -643,19 +646,19 @@ func TestGetFuncStringValue(t *testing.T) { }, } - for _, test := range tests { + for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := getFuncStringValue(label.TraefikProtocol, label.DefaultProtocol)(test.instanceInfo) + actual := getFuncStringValueV1(label.TraefikProtocol, label.DefaultProtocol)(test.instanceInfo) assert.Equal(t, test.expected, actual) }) } } func TestGetFuncSliceString(t *testing.T) { - tests := []struct { + testCases := []struct { desc string expected []string instanceInfo ecsInstance @@ -681,12 +684,12 @@ func TestGetFuncSliceString(t *testing.T) { }, } - for _, test := range tests { + for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := getFuncSliceString(label.TraefikFrontendEntryPoints)(test.instanceInfo) + actual := getFuncSliceStringV1(label.TraefikFrontendEntryPoints)(test.instanceInfo) assert.Equal(t, test.expected, actual) }) } @@ -707,7 +710,7 @@ func makeEcsInstance(containerDef *ecs.ContainerDefinition) ecsInstance { } } - return ecsInstance{ + instance := ecsInstance{ Name: "foo-http", ID: "123456789abc", task: &ecs.Task{ @@ -725,6 +728,12 @@ func makeEcsInstance(containerDef *ecs.ContainerDefinition) ecsInstance { }, }, } + + if containerDef != nil { + instance.TraefikLabels = aws.StringValueMap(containerDef.DockerLabels) + } + + return instance } func simpleEcsInstance(labels map[string]*string) ecsInstance { @@ -747,782 +756,15 @@ func simpleEcsInstanceNoNetwork(labels map[string]*string) ecsInstance { }) } -func TestGetCircuitBreaker(t *testing.T) { - testCases := []struct { - desc string - instance ecsInstance - expected *types.CircuitBreaker - }{ - { - desc: "should return nil when no CB label", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - }, - expected: nil, - }, - { - desc: "", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), - }}}, - expected: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getCircuitBreaker(test.instance) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetLoadBalancer(t *testing.T) { - testCases := []struct { - desc string - instance ecsInstance - expected *types.LoadBalancer - }{ - { - desc: "should return nil when no LB labels", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - }, - expected: nil, - }, - { - desc: "should return a struct when labels are set", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikBackendLoadBalancerMethod: aws.String("drr"), - label.TraefikBackendLoadBalancerSticky: aws.String("true"), - label.TraefikBackendLoadBalancerStickiness: aws.String("true"), - label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("foo"), - }}}, - expected: &types.LoadBalancer{ - Method: "drr", - Sticky: true, - Stickiness: &types.Stickiness{ - CookieName: "foo", - }, - }, - }, - { - desc: "should return a nil Stickiness when Stickiness is not set", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikBackendLoadBalancerMethod: aws.String("drr"), - label.TraefikBackendLoadBalancerSticky: aws.String("true"), - label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("foo"), - }}}, - expected: &types.LoadBalancer{ - Method: "drr", - Sticky: true, - Stickiness: nil, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getLoadBalancer(test.instance) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetMaxConn(t *testing.T) { - testCases := []struct { - desc string - instance ecsInstance - expected *types.MaxConn - }{ - { - desc: "should return nil when no max conn labels", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - }, - expected: nil, - }, - { - desc: "should return nil when no amount label", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), - }}}, - expected: nil, - }, - { - desc: "should return default when no empty extractorFunc label", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikBackendMaxConnExtractorFunc: aws.String(""), - label.TraefikBackendMaxConnAmount: aws.String("666"), - }}}, - expected: &types.MaxConn{ - ExtractorFunc: "request.host", - Amount: 666, - }, - }, - { - desc: "should return a struct when max conn labels are set", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), - label.TraefikBackendMaxConnAmount: aws.String("666"), - }}}, - expected: &types.MaxConn{ - ExtractorFunc: "client.ip", - Amount: 666, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getMaxConn(test.instance) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetHealthCheck(t *testing.T) { - testCases := []struct { - desc string - instance ecsInstance - expected *types.HealthCheck - }{ - { - desc: "should return nil when no health check labels", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }}, - expected: nil, - }, - { - desc: "should return nil when no health check Path label", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikBackendHealthCheckPort: aws.String("80"), - label.TraefikBackendHealthCheckInterval: aws.String("6"), - }}}, - expected: nil, - }, - { - desc: "should return a struct when health check labels are set", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikBackendHealthCheckPath: aws.String("/health"), - label.TraefikBackendHealthCheckPort: aws.String("80"), - label.TraefikBackendHealthCheckInterval: aws.String("6"), - }}}, - expected: &types.HealthCheck{ - Path: "/health", - Port: 80, - Interval: "6", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getHealthCheck(test.instance) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetBuffering(t *testing.T) { - testCases := []struct { - desc string - instance ecsInstance - expected *types.Buffering - }{ - { - desc: "should return nil when no buffering labels", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }}, - expected: nil, - }, - { - desc: "should return a struct when health check labels are set", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - 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"), - }}}, - expected: &types.Buffering{ - MaxResponseBodyBytes: 10485760, - MemResponseBodyBytes: 2097152, - MaxRequestBodyBytes: 10485760, - MemRequestBodyBytes: 2097152, - RetryExpression: "IsNetworkError() && Attempts() <= 2", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getBuffering(test.instance) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetServers(t *testing.T) { - testCases := []struct { - desc string - instances []ecsInstance - expected map[string]types.Server - }{ - { - desc: "should return a dumb server when no IP and no ", - instances: []ecsInstance{{ - Name: "test", - ID: "0", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - machine: &ec2.Instance{ - PrivateIpAddress: nil, - }, - container: &ecs.Container{ - NetworkBindings: []*ecs.NetworkBinding{{ - HostPort: nil, - }}, - }}}, - expected: map[string]types.Server{ - "server-test-0": {URL: "http://:0", Weight: 0}, - }, - }, - { - desc: "should use default weight when invalid weight value", - instances: []ecsInstance{{ - Name: "test", - ID: "0", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikWeight: aws.String("oops"), - }}, - machine: &ec2.Instance{ - PrivateIpAddress: aws.String("10.10.10.0"), - }, - container: &ecs.Container{ - NetworkBindings: []*ecs.NetworkBinding{{ - HostPort: aws.Int64(80), - }}, - }}}, - expected: map[string]types.Server{ - "server-test-0": {URL: "http://10.10.10.0:80", Weight: 0}, - }, - }, - { - desc: "should return a map when configuration keys are defined", - instances: []ecsInstance{ - { - Name: "test", - ID: "0", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikWeight: aws.String("6"), - }}, - machine: &ec2.Instance{ - PrivateIpAddress: aws.String("10.10.10.0"), - }, - container: &ecs.Container{ - NetworkBindings: []*ecs.NetworkBinding{{ - HostPort: aws.Int64(80), - }}, - }}, - { - Name: "test", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}}, - machine: &ec2.Instance{ - PrivateIpAddress: aws.String("10.10.10.1"), - }, - container: &ecs.Container{ - NetworkBindings: []*ecs.NetworkBinding{{ - HostPort: aws.Int64(90), - }}, - }}, - }, - expected: map[string]types.Server{ - "server-test-0": { - URL: "http://10.10.10.0:80", - Weight: 6, - }, - "server-test-1": { - URL: "http://10.10.10.1:90", - Weight: 0, - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getServers(test.instances) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestWhiteList(t *testing.T) { - testCases := []struct { - desc string - instance ecsInstance - expected *types.WhiteList - }{ - { - desc: "should return nil when no white list labels", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - }, - expected: nil, - }, - { - desc: "should return a struct when only range", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), - }}, - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: false, - }, - }, - { - desc: "should return a struct when range and UseXForwardedFor", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), - label.TraefikFrontendWhiteListUseXForwardedFor: aws.String("true"), - }}, - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: true, - }, - }, - { - desc: "should return nil when only UseXForwardedFor", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendWhiteListUseXForwardedFor: aws.String("true"), - }}, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getWhiteList(test.instance) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetRedirect(t *testing.T) { - testCases := []struct { - desc string - instance ecsInstance - expected *types.Redirect - }{ - { - desc: "should return nil when no redirect labels", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - }, - expected: nil, - }, - { - desc: "should use only entry point tag when mix regex redirect and entry point redirect", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendRedirectEntryPoint: aws.String("https"), - label.TraefikFrontendRedirectRegex: aws.String("(.*)"), - label.TraefikFrontendRedirectReplacement: aws.String("$1"), - }}, - }, - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendRedirectEntryPoint: aws.String("https"), - }}, - }, - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label (permanent)", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendRedirectEntryPoint: aws.String("https"), - label.TraefikFrontendRedirectPermanent: aws.String("true"), - }}, - }, - expected: &types.Redirect{ - EntryPoint: "https", - Permanent: true, - }, - }, - { - desc: "should return a struct when regex redirect labels", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendRedirectRegex: aws.String("(.*)"), - label.TraefikFrontendRedirectReplacement: aws.String("$1"), - }}, - }, - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - }, - }, - { - desc: "should return a struct when regex redirect tags (permanent)", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendRedirectRegex: aws.String("(.*)"), - label.TraefikFrontendRedirectReplacement: aws.String("$1"), - label.TraefikFrontendRedirectPermanent: aws.String("true"), - }}, - }, - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - Permanent: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getRedirect(test.instance) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetErrorPages(t *testing.T) { - testCases := []struct { - desc string - instance ecsInstance - expected map[string]*types.ErrorPage - }{ - { - desc: "", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - }, - expected: nil, - }, - { - desc: "2 errors pages", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foo_backend"), - 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("bar_backend"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), - }}}, - expected: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - Query: "foo_query", - Backend: "foo_backend", - }, - "bar": { - Status: []string{"500", "600"}, - Query: "bar_query", - Backend: "bar_backend", - }, - }, - }, - { - desc: "only status field", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), - }}}, - expected: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getErrorPages(test.instance) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetRateLimit(t *testing.T) { - testCases := []struct { - desc string - instance ecsInstance - expected *types.RateLimit - }{ - { - desc: "should return nil when no rate limit labels", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - }, - expected: nil, - }, - { - desc: "should return a struct when rate limit labels are defined", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - 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"), - }, - }, - }, - expected: &types.RateLimit{ - ExtractorFunc: "client.ip", - RateSet: map[string]*types.Rate{ - "foo": { - Period: flaeg.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - "bar": { - Period: flaeg.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - }, - }, - }, - { - desc: "should return nil when ExtractorFunc is missing", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - 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"), - }, - }, - }, - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getRateLimit(test.instance) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetHeaders(t *testing.T) { - testCases := []struct { - desc string - instance ecsInstance - expected *types.Headers - }{ - { - desc: "", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - }, - expected: nil, - }, - { - desc: "should return nil when no custom headers options are set", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - }, - expected: nil, - }, - { - desc: "should return a struct when all custom headers options are set", - instance: ecsInstance{ - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - 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.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"), - }, - }, - }, - expected: &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", - }, - SSLProxyHeaders: 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"}, - SSLHost: "foo", - CustomFrameOptionsValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - CustomBrowserXSSValue: "foo", - STSSeconds: 666, - SSLRedirect: true, - SSLTemporaryRedirect: true, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - ContentTypeNosniff: true, - BrowserXSSFilter: true, - IsDevelopment: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getHeaders(test.instance) - - assert.Equal(t, test.expected, actual) - }) +func fakeLoadTraefikLabels(services map[string][]ecsInstance) map[string][]ecsInstance { + result := make(map[string][]ecsInstance) + for name, srcInstances := range services { + var instances []ecsInstance + for _, instance := range srcInstances { + instance.TraefikLabels = aws.StringValueMap(instance.containerDefinition.DockerLabels) + instances = append(instances, instance) + } + result[name] = instances } + return result } diff --git a/provider/ecs/deprecated_config.go b/provider/ecs/deprecated_config.go new file mode 100644 index 000000000..14cc0e0be --- /dev/null +++ b/provider/ecs/deprecated_config.go @@ -0,0 +1,196 @@ +package ecs + +import ( + "strconv" + "text/template" + + "github.com/aws/aws-sdk-go/aws" + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" +) + +// buildConfiguration fills the config template with the given instances +// Deprecated +func (p *Provider) buildConfigurationV1(services map[string][]ecsInstance) (*types.Configuration, error) { + var ecsFuncMap = template.FuncMap{ + // Backend functions + "getHost": getHost, + "getPort": getPort, + + "getProtocol": getFuncStringValueV1(label.TraefikProtocol, label.DefaultProtocol), + "getWeight": getFuncIntValueV1(label.TraefikWeight, label.DefaultWeightInt), + "getLoadBalancerMethod": getFuncFirstStringValueV1(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), + "getLoadBalancerSticky": getStickyV1, + "hasStickinessLabel": getFuncFirstBoolValueV1(label.TraefikBackendLoadBalancerStickiness, false), + "getStickinessCookieName": getFuncFirstStringValueV1(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), + "hasHealthCheckLabels": hasFuncFirstV1(label.TraefikBackendHealthCheckPath), + "getHealthCheckPath": getFuncFirstStringValueV1(label.TraefikBackendHealthCheckPath, ""), + "getHealthCheckInterval": getFuncFirstStringValueV1(label.TraefikBackendHealthCheckInterval, ""), + + // Frontend functions + "filterFrontends": filterFrontends, + "getFrontendRule": p.getFrontendRule, + "getPassHostHeader": getFuncBoolValueV1(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": getFuncBoolValueV1(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getPriority": getFuncIntValueV1(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getBasicAuth": getFuncSliceStringV1(label.TraefikFrontendAuthBasic), + "getEntryPoints": getFuncSliceStringV1(label.TraefikFrontendEntryPoints), + } + + return p.GetConfiguration("templates/ecs-v1.tmpl", ecsFuncMap, struct { + Services map[string][]ecsInstance + }{ + Services: services, + }) +} + +// TODO: Deprecated +// replaced by Stickiness +// Deprecated +func getStickyV1(instances []ecsInstance) bool { + if hasFirstV1(instances, label.TraefikBackendLoadBalancerSticky) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + } + return getFirstBoolValueV1(instances, label.TraefikBackendLoadBalancerSticky, false) +} + +// Label functions + +// Deprecated +func getFuncStringValueV1(labelName string, defaultValue string) func(i ecsInstance) string { + return func(i ecsInstance) string { + return getStringValueV1(i, labelName, defaultValue) + } +} + +// Deprecated +func getFuncBoolValueV1(labelName string, defaultValue bool) func(i ecsInstance) bool { + return func(i ecsInstance) bool { + return getBoolValueV1(i, labelName, defaultValue) + } +} + +// Deprecated +func getFuncIntValueV1(labelName string, defaultValue int) func(i ecsInstance) int { + return func(i ecsInstance) int { + return getIntValueV1(i, labelName, defaultValue) + } +} + +// Deprecated +func getFuncSliceStringV1(labelName string) func(i ecsInstance) []string { + return func(i ecsInstance) []string { + return getSliceStringV1(i, labelName) + } +} + +// Deprecated +func hasLabelV1(i ecsInstance, labelName string) bool { + value, ok := i.containerDefinition.DockerLabels[labelName] + return ok && value != nil && len(aws.StringValue(value)) > 0 +} + +// Deprecated +func getStringValueV1(i ecsInstance, labelName string, defaultValue string) string { + if v, ok := i.containerDefinition.DockerLabels[labelName]; ok { + if v == nil { + return defaultValue + } + if len(aws.StringValue(v)) == 0 { + return defaultValue + } + return aws.StringValue(v) + } + return defaultValue +} + +// Deprecated +func getBoolValueV1(i ecsInstance, labelName string, defaultValue bool) bool { + rawValue, ok := i.containerDefinition.DockerLabels[labelName] + if ok { + if rawValue != nil { + v, err := strconv.ParseBool(aws.StringValue(rawValue)) + if err == nil { + return v + } + } + } + return defaultValue +} + +// Deprecated +func getIntValueV1(i ecsInstance, labelName string, defaultValue int) int { + rawValue, ok := i.containerDefinition.DockerLabels[labelName] + if ok { + if rawValue != nil { + v, err := strconv.Atoi(aws.StringValue(rawValue)) + if err == nil { + return v + } + } + } + return defaultValue +} + +// Deprecated +func getSliceStringV1(i ecsInstance, labelName string) []string { + if value, ok := i.containerDefinition.DockerLabels[labelName]; ok { + if value == nil { + return nil + } + if len(aws.StringValue(value)) == 0 { + return nil + } + return label.SplitAndTrimString(aws.StringValue(value), ",") + } + return nil +} + +// Deprecated +func hasFuncFirstV1(labelName string) func(instances []ecsInstance) bool { + return func(instances []ecsInstance) bool { + return hasFirstV1(instances, labelName) + } +} + +// Deprecated +func getFuncFirstStringValueV1(labelName string, defaultValue string) func(instances []ecsInstance) string { + return func(instances []ecsInstance) string { + return getFirstStringValueV1(instances, labelName, defaultValue) + } +} + +// Deprecated +func getFuncFirstBoolValueV1(labelName string, defaultValue bool) func(instances []ecsInstance) bool { + return func(instances []ecsInstance) bool { + if len(instances) < 0 { + return defaultValue + } + return getBoolValueV1(instances[0], labelName, defaultValue) + } +} + +// Deprecated +func hasFirstV1(instances []ecsInstance, labelName string) bool { + if len(instances) == 0 { + return false + } + return hasLabelV1(instances[0], labelName) +} + +// Deprecated +func getFirstStringValueV1(instances []ecsInstance, labelName string, defaultValue string) string { + if len(instances) == 0 { + return defaultValue + } + return getStringValueV1(instances[0], labelName, defaultValue) +} + +// Deprecated +func getFirstBoolValueV1(instances []ecsInstance, labelName string, defaultValue bool) bool { + if len(instances) == 0 { + return defaultValue + } + return getBoolValueV1(instances[0], labelName, defaultValue) +} diff --git a/provider/ecs/deprecated_config_test.go b/provider/ecs/deprecated_config_test.go new file mode 100644 index 000000000..0e73bf05b --- /dev/null +++ b/provider/ecs/deprecated_config_test.go @@ -0,0 +1,292 @@ +package ecs + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/stretchr/testify/assert" +) + +func TestBuildConfigurationV1(t *testing.T) { + testCases := []struct { + desc string + services map[string][]ecsInstance + expected *types.Configuration + err error + }{ + { + desc: "config parsed successfully", + services: map[string][]ecsInstance{ + "testing": {{ + Name: "testing", + ID: "1", + containerDefinition: &ecs.ContainerDefinition{ + DockerLabels: map[string]*string{}, + }, + machine: &ec2.Instance{ + PrivateIpAddress: aws.String("10.0.0.1"), + }, + container: &ecs.Container{ + NetworkBindings: []*ecs.NetworkBinding{{ + HostPort: aws.Int64(1337), + }}, + }, + }}, + }, + expected: &types.Configuration{ + Backends: map[string]*types.Backend{ + "backend-testing": { + Servers: map[string]types.Server{ + "server-testing1": { + URL: "http://10.0.0.1:1337", + }}, + LoadBalancer: &types.LoadBalancer{ + Method: "wrr", + }, + }, + }, + Frontends: map[string]*types.Frontend{ + "frontend-testing": { + EntryPoints: []string{}, + Backend: "backend-testing", + Routes: map[string]types.Route{ + "route-frontend-testing": { + Rule: "Host:testing.", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + }, + }, + }, + }, + { + desc: "config parsed successfully with health check labels", + services: map[string][]ecsInstance{ + "testing": {{ + Name: "testing", + ID: "1", + containerDefinition: &ecs.ContainerDefinition{ + DockerLabels: map[string]*string{ + label.TraefikBackendHealthCheckPath: aws.String("/health"), + label.TraefikBackendHealthCheckInterval: aws.String("1s"), + }}, + machine: &ec2.Instance{ + PrivateIpAddress: aws.String("10.0.0.1"), + }, + container: &ecs.Container{ + NetworkBindings: []*ecs.NetworkBinding{{ + HostPort: aws.Int64(1337), + }}, + }, + }}, + }, + expected: &types.Configuration{ + Backends: map[string]*types.Backend{ + "backend-testing": { + HealthCheck: &types.HealthCheck{ + Path: "/health", + Interval: "1s", + }, + Servers: map[string]types.Server{ + "server-testing1": { + URL: "http://10.0.0.1:1337", + }}, + LoadBalancer: &types.LoadBalancer{ + Method: "wrr", + }, + }, + }, + Frontends: map[string]*types.Frontend{ + "frontend-testing": { + EntryPoints: []string{}, + Backend: "backend-testing", + Routes: map[string]types.Route{ + "route-frontend-testing": { + Rule: "Host:testing.", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + }, + }, + }, + }, + { + desc: "when all labels are set", + services: map[string][]ecsInstance{ + "testing-instance": {{ + 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"), + + label.TraefikBackend: aws.String("foobar"), + + label.TraefikBackendHealthCheckPath: aws.String("/health"), + label.TraefikBackendHealthCheckInterval: aws.String("6"), + label.TraefikBackendLoadBalancerMethod: aws.String("drr"), + label.TraefikBackendLoadBalancerSticky: aws.String("true"), + label.TraefikBackendLoadBalancerStickiness: aws.String("true"), + label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), + + 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.TraefikFrontendPriority: aws.String("666"), + label.TraefikFrontendRule: aws.String("Host:traefik.io"), + }}, + machine: &ec2.Instance{ + PrivateIpAddress: aws.String("10.0.0.1"), + }, + container: &ecs.Container{ + NetworkBindings: []*ecs.NetworkBinding{{ + HostPort: aws.Int64(1337), + }}, + }, + }}, + }, + expected: &types.Configuration{ + Backends: map[string]*types.Backend{ + "backend-testing-instance": { + Servers: map[string]types.Server{ + "server-testing-instance6": { + URL: "https://10.0.0.1:666", + Weight: 12, + }, + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "chocolate", + }, + }, + HealthCheck: &types.HealthCheck{ + Path: "/health", + Interval: "6", + }, + }, + }, + Frontends: map[string]*types.Frontend{ + "frontend-testing-instance": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backend-testing-instance", + Routes: map[string]types.Route{ + "route-frontend-testing-instance": { + Rule: "Host:traefik.io", + }, + }, + PassHostHeader: true, + Priority: 666, + BasicAuth: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + provider := &Provider{} + + services := fakeLoadTraefikLabels(test.services) + + got, err := provider.buildConfigurationV1(services) + assert.Equal(t, test.err, err) // , err.Error() + assert.Equal(t, test.expected, got, test.desc) + }) + } +} + +func TestGetFuncStringValueV1(t *testing.T) { + testCases := []struct { + desc string + expected string + instanceInfo ecsInstance + }{ + { + desc: "Protocol label is not set should return a string equals to http", + expected: "http", + instanceInfo: simpleEcsInstance(map[string]*string{}), + }, + { + desc: "Protocol label is set to http should return a string equals to http", + expected: "http", + instanceInfo: simpleEcsInstance(map[string]*string{ + label.TraefikProtocol: aws.String("http"), + }), + }, + { + desc: "Protocol label is set to https should return a string equals to https", + expected: "https", + instanceInfo: simpleEcsInstance(map[string]*string{ + label.TraefikProtocol: aws.String("https"), + }), + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getFuncStringValueV1(label.TraefikProtocol, label.DefaultProtocol)(test.instanceInfo) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetFuncSliceStringV1(t *testing.T) { + testCases := []struct { + desc string + expected []string + instanceInfo ecsInstance + }{ + { + desc: "Frontend entrypoints label not set should return empty array", + expected: nil, + instanceInfo: simpleEcsInstance(map[string]*string{}), + }, + { + desc: "Frontend entrypoints label set to http should return a string array of 1 element", + expected: []string{"http"}, + instanceInfo: simpleEcsInstance(map[string]*string{ + label.TraefikFrontendEntryPoints: aws.String("http"), + }), + }, + { + desc: "Frontend entrypoints label set to http,https should return a string array of 2 elements", + expected: []string{"http", "https"}, + instanceInfo: simpleEcsInstance(map[string]*string{ + label.TraefikFrontendEntryPoints: aws.String("http,https"), + }), + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getFuncSliceStringV1(label.TraefikFrontendEntryPoints)(test.instanceInfo) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/provider/ecs/ecs.go b/provider/ecs/ecs.go index 45678ea06..187e3d88d 100644 --- a/provider/ecs/ecs.go +++ b/provider/ecs/ecs.go @@ -51,6 +51,7 @@ type ecsInstance struct { container *ecs.Container containerDefinition *ecs.ContainerDefinition machine *ec2.Instance + TraefikLabels map[string]string } type awsClient struct { @@ -201,7 +202,6 @@ func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*types // Find all running Provider tasks in a cluster, also collect the task definitions (for docker labels) // and the EC2 instance data func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsInstance, error) { - var instances []ecsInstance var clustersArn []*string var clusters Clusters @@ -233,7 +233,11 @@ func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsI } else { clusters = p.Clusters } + + var instances []ecsInstance + log.Debugf("ECS Clusters: %s", clusters) + for _, c := range clusters { req, _ := client.ecs.ListTasksRequest(&ecs.ListTasksInput{ @@ -317,13 +321,14 @@ func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsI } instances = append(instances, ecsInstance{ - fmt.Sprintf("%s-%s", strings.Replace(aws.StringValue(task.Group), ":", "-", 1), *container.Name), - (aws.StringValue(task.TaskArn))[len(aws.StringValue(task.TaskArn))-12:], - task, - taskDefinition, - container, - containerDefinition, - machines[machineIdx], + Name: fmt.Sprintf("%s-%s", strings.Replace(aws.StringValue(task.Group), ":", "-", 1), *container.Name), + ID: (aws.StringValue(task.TaskArn))[len(aws.StringValue(task.TaskArn))-12:], + task: task, + taskDefinition: taskDefinition, + container: container, + containerDefinition: containerDefinition, + machine: machines[machineIdx], + TraefikLabels: aws.StringValueMap(containerDefinition.DockerLabels), }) } } @@ -398,7 +403,7 @@ func (p *Provider) lookupTaskDefinitions(ctx context.Context, client *awsClient, func (p *Provider) filterInstance(i ecsInstance) bool { - if labelPort := getStringValue(i, label.TraefikPort, ""); len(i.container.NetworkBindings) == 0 && labelPort == "" { + if labelPort := getStringValueV1(i, label.TraefikPort, ""); len(i.container.NetworkBindings) == 0 && labelPort == "" { log.Debugf("Filtering ecs instance without port %s (%s)", i.Name, i.ID) return false } diff --git a/templates/ecs-v1.tmpl b/templates/ecs-v1.tmpl new file mode 100644 index 000000000..e8b33c3f6 --- /dev/null +++ b/templates/ecs-v1.tmpl @@ -0,0 +1,44 @@ +[backends] +{{range $serviceName, $instances := .Services }} + [backends."backend-{{ $serviceName }}".loadBalancer] + method = "{{ getLoadBalancerMethod $instances }}" + sticky = {{ getLoadBalancerSticky $instances }} + + {{if hasStickinessLabel $instances }} + [backends."backend-{{ $serviceName }}".loadBalancer.stickiness] + cookieName = "{{ getStickinessCookieName $instances }}" + {{end}} + + {{ if hasHealthCheckLabels $instances }} + [backends."backend-{{ $serviceName }}".healthCheck] + path = "{{ getHealthCheckPath $instances }}" + interval = "{{ getHealthCheckInterval $instances }}" + {{end}} + + {{range $index, $i := $instances }} + [backends."backend-{{ $i.Name }}".servers."server-{{ $i.Name }}{{ $i.ID }}"] + url = "{{ getProtocol $i }}://{{ getHost $i }}:{{ getPort $i }}" + weight = {{ getWeight $i }} + {{end}} +{{end}} + +[frontends] +{{range $serviceName, $instances := .Services}} +{{range filterFrontends $instances }} + [frontends."frontend-{{ $serviceName }}"] + backend = "backend-{{ $serviceName }}" + passHostHeader = {{ getPassHostHeader . }} + priority = {{ getPriority . }} + + entryPoints = [{{range getEntryPoints . }} + "{{.}}", + {{end}}] + + basicAuth = [{{range getBasicAuth . }} + "{{.}}", + {{end}}] + + [frontends."frontend-{{ $serviceName }}".routes."route-frontend-{{ $serviceName }}"] + rule = "{{getFrontendRule .}}" +{{end}} +{{end}} \ No newline at end of file diff --git a/templates/ecs.tmpl b/templates/ecs.tmpl index 0030e83c4..719e56f55 100644 --- a/templates/ecs.tmpl +++ b/templates/ecs.tmpl @@ -2,13 +2,13 @@ {{range $serviceName, $instances := .Services }} {{ $firstInstance := index $instances 0 }} - {{ $circuitBreaker := getCircuitBreaker $firstInstance }} + {{ $circuitBreaker := getCircuitBreaker $firstInstance.TraefikLabels }} {{if $circuitBreaker }} [backends."backend-{{ $serviceName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $firstInstance }} + {{ $loadBalancer := getLoadBalancer $firstInstance.TraefikLabels }} {{if $loadBalancer }} [backends."backend-{{ $serviceName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -19,14 +19,14 @@ {{end}} {{end}} - {{ $maxConn := getMaxConn $firstInstance }} + {{ $maxConn := getMaxConn $firstInstance.TraefikLabels }} {{if $maxConn }} [backends."backend-{{ $serviceName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $firstInstance }} + {{ $healthCheck := getHealthCheck $firstInstance.TraefikLabels }} {{if $healthCheck }} [backends."backend-{{ $serviceName }}".healthCheck] path = "{{ $healthCheck.Path }}" @@ -34,7 +34,7 @@ interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $firstInstance }} + {{ $buffering := getBuffering $firstInstance.TraefikLabels }} {{if $buffering }} [backends."backend-{{ $serviceName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -58,19 +58,19 @@ [frontends."frontend-{{ $serviceName }}"] backend = "backend-{{ $serviceName }}" - priority = {{ getPriority $instance }} - passHostHeader = {{ getPassHostHeader $instance }} - passTLSCert = {{ getPassTLSCert $instance }} + priority = {{ getPriority $instance.TraefikLabels }} + passHostHeader = {{ getPassHostHeader $instance.TraefikLabels }} + passTLSCert = {{ getPassTLSCert $instance.TraefikLabels }} - entryPoints = [{{range getEntryPoints $instance }} + entryPoints = [{{range getEntryPoints $instance.TraefikLabels }} "{{.}}", {{end}}] - basicAuth = [{{range getBasicAuth $instance }} + basicAuth = [{{range getBasicAuth $instance.TraefikLabels }} "{{.}}", {{end}}] - {{ $whitelist := getWhiteList $instance }} + {{ $whitelist := getWhiteList $instance.TraefikLabels }} {{if $whitelist }} [frontends."frontend-{{ $serviceName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} @@ -79,7 +79,7 @@ useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $instance }} + {{ $redirect := getRedirect $instance.TraefikLabels }} {{if $redirect }} [frontends."frontend-{{ $serviceName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -88,7 +88,7 @@ permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $instance }} + {{ $errorPages := getErrorPages $instance.TraefikLabels }} {{if $errorPages }} [frontends."frontend-{{ $serviceName }}".errors] {{range $pageName, $page := $errorPages }} @@ -101,7 +101,7 @@ {{end}} {{end}} - {{ $rateLimit := getRateLimit $instance }} + {{ $rateLimit := getRateLimit $instance.TraefikLabels }} {{if $rateLimit }} [frontends."frontend-{{ $serviceName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" @@ -114,7 +114,7 @@ {{end}} {{end}} - {{ $headers := getHeaders $instance }} + {{ $headers := getHeaders $instance.TraefikLabels }} {{if $headers }} [frontends."frontend-{{ $serviceName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} @@ -169,7 +169,7 @@ {{end}} [frontends."frontend-{{ $serviceName }}".routes."route-frontend-{{ $serviceName }}"] - rule = "{{getFrontendRule $instance}}" + rule = "{{ getFrontendRule $instance }}" {{end}} {{end}} \ No newline at end of file From ff61cc971e635f980d0221b8583ed74e35c3c69a Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Wed, 28 Mar 2018 02:18:07 +0200 Subject: [PATCH 15/21] refactor: Consul Catalog labels. --- configuration/configuration.go | 9 + integration/consul_catalog_test.go | 33 +- provider/consulcatalog/config.go | 273 ++++ provider/consulcatalog/config_root.go | 10 + provider/consulcatalog/config_test.go | 518 ++++++ provider/consulcatalog/consul_catalog.go | 26 +- .../consulcatalog/consul_catalog_config.go | 589 ------- .../consul_catalog_config_test.go | 1387 ----------------- provider/consulcatalog/convert_types.go | 29 + provider/consulcatalog/convert_types_test.go | 64 + provider/consulcatalog/deprecated_config.go | 250 +++ .../consulcatalog/deprecated_config_test.go | 444 ++++++ templates/consul_catalog-v1.tmpl | 56 + templates/consul_catalog.tmpl | 45 +- 14 files changed, 1715 insertions(+), 2018 deletions(-) create mode 100644 provider/consulcatalog/config.go create mode 100644 provider/consulcatalog/config_root.go create mode 100644 provider/consulcatalog/config_test.go delete mode 100644 provider/consulcatalog/consul_catalog_config.go delete mode 100644 provider/consulcatalog/consul_catalog_config_test.go create mode 100644 provider/consulcatalog/convert_types.go create mode 100644 provider/consulcatalog/convert_types_test.go create mode 100644 provider/consulcatalog/deprecated_config.go create mode 100644 provider/consulcatalog/deprecated_config_test.go create mode 100644 templates/consul_catalog-v1.tmpl diff --git a/configuration/configuration.go b/configuration/configuration.go index f3fb3516c..10c3557d9 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -246,6 +246,15 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) { } } + if gc.ConsulCatalog != nil { + if len(gc.ConsulCatalog.Filename) != 0 && gc.ConsulCatalog.TemplateVersion != 2 { + log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.") + gc.ConsulCatalog.TemplateVersion = 1 + } else { + gc.ConsulCatalog.TemplateVersion = 2 + } + } + if gc.Rancher != nil { if len(gc.Rancher.Filename) != 0 && gc.Rancher.TemplateVersion != 2 { log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.") diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 4fd4ef4df..fdf35b41a 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/containous/traefik/integration/try" + "github.com/containous/traefik/provider/label" "github.com/go-check/check" "github.com/hashicorp/consul/api" checker "github.com/vdemeester/shakers" @@ -160,7 +161,6 @@ func (s *ConsulCatalogSuite) TestSingleService(c *check.C) { s.deregisterService("test", whoami.NetworkSettings.IPAddress) err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusNotFound), try.HasBody()) c.Assert(err, checker.IsNil) - } func (s *ConsulCatalogSuite) TestExposedByDefaultFalseSingleService(c *check.C) { @@ -202,13 +202,12 @@ func (s *ConsulCatalogSuite) TestExposedByDefaultFalseSimpleServiceMultipleNode( defer cmd.Process.Kill() whoami := s.composeProject.Container(c, "whoami1") - whoami2 := s.composeProject.Container(c, "whoami2") - err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress) - err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 80, []string{"traefik.enable=true"}) + whoami2 := s.composeProject.Container(c, "whoami2") + err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 80, []string{label.TraefikEnable + "=true"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami2.NetworkSettings.IPAddress) @@ -326,7 +325,7 @@ func (s *ConsulCatalogSuite) TestBasicAuthSimpleService(c *check.C) { whoami := s.composeProject.Container(c, "whoami1") err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{ - "traefik.frontend.auth.basic=test:$2a$06$O5NksJPAcgrC9MuANkSoE.Xe9DSg7KcLLFYNr1Lj6hPcMmvgwxhme,test2:$2y$10$xP1SZ70QbZ4K2bTGKJOhpujkpcLxQcB3kEPF6XAV19IdcqsZTyDEe", + label.TraefikFrontendAuthBasic + "=test:$2a$06$O5NksJPAcgrC9MuANkSoE.Xe9DSg7KcLLFYNr1Lj6hPcMmvgwxhme,test2:$2y$10$xP1SZ70QbZ4K2bTGKJOhpujkpcLxQcB3kEPF6XAV19IdcqsZTyDEe", }) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress) @@ -362,7 +361,8 @@ func (s *ConsulCatalogSuite) TestRefreshConfigTagChange(c *check.C) { whoami := s.composeProject.Container(c, "whoami1") - err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=false", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, + []string{"name=whoami1", label.TraefikEnable + "=false", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress) @@ -370,7 +370,8 @@ func (s *ConsulCatalogSuite) TestRefreshConfigTagChange(c *check.C) { try.BodyContains(whoami.NetworkSettings.IPAddress)) c.Assert(err, checker.NotNil) - err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true", "traefik.backend.circuitbreaker=ResponseCodeRatio(500, 600, 0, 600) > 0.5"}) + err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, + []string{"name=whoami1", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=ResponseCodeRatio(500, 600, 0, 600) > 0.5"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) @@ -403,16 +404,20 @@ func (s *ConsulCatalogSuite) TestCircuitBreaker(c *check.C) { defer cmd.Process.Kill() whoami := s.composeProject.Container(c, "whoami1") - whoami2 := s.composeProject.Container(c, "whoami2") - whoami3 := s.composeProject.Container(c, "whoami3") - - err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, + []string{"name=whoami1", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress) - err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 42, []string{"name=whoami2", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + + whoami2 := s.composeProject.Container(c, "whoami2") + err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 42, + []string{"name=whoami2", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami2.NetworkSettings.IPAddress) - err = s.registerService("test", whoami3.NetworkSettings.IPAddress, 42, []string{"name=whoami3", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + + whoami3 := s.composeProject.Container(c, "whoami3") + err = s.registerService("test", whoami3.NetworkSettings.IPAddress, 42, + []string{"name=whoami3", label.TraefikEnable + "=true", label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami3.NetworkSettings.IPAddress) @@ -452,7 +457,7 @@ func (s *ConsulCatalogSuite) TestRefreshConfigPortChange(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, try.BodyContains(whoami.NetworkSettings.IPAddress)) c.Assert(err, checker.IsNil) - err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true"}) + err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", label.TraefikEnable + "=true"}) c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress) diff --git a/provider/consulcatalog/config.go b/provider/consulcatalog/config.go new file mode 100644 index 000000000..ea5bea174 --- /dev/null +++ b/provider/consulcatalog/config.go @@ -0,0 +1,273 @@ +package consulcatalog + +import ( + "bytes" + "crypto/sha1" + "encoding/base64" + "fmt" + "sort" + "strconv" + "strings" + "text/template" + + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/hashicorp/consul/api" +) + +func (p *Provider) buildConfigurationV2(catalog []catalogUpdate) *types.Configuration { + var funcMap = template.FuncMap{ + "getAttribute": p.getAttribute, + "getTag": getTag, + "hasTag": hasTag, + + // Backend functions + "getNodeBackendName": getNodeBackendName, + "getServiceBackendName": getServiceBackendName, + "getBackendAddress": getBackendAddress, + "getServerName": getServerName, + "getCircuitBreaker": getCircuitBreaker, + "getLoadBalancer": getLoadBalancer, + "getMaxConn": label.GetMaxConn, + "getHealthCheck": label.GetHealthCheck, + "getBuffering": label.GetBuffering, + "getServer": p.getServer, + + // Frontend functions + "getFrontendRule": p.getFrontendRule, + "getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), + "getFrontEndEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints), + "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getWhiteList": label.GetWhiteList, + "getRedirect": label.GetRedirect, + "getErrorPages": label.GetErrorPages, + "getRateLimit": label.GetRateLimit, + "getHeaders": label.GetHeaders, + } + + var allNodes []*api.ServiceEntry + var services []*serviceUpdate + for _, info := range catalog { + if len(info.Nodes) > 0 { + services = append(services, info.Service) + allNodes = append(allNodes, info.Nodes...) + } + } + // Ensure a stable ordering of nodes so that identical configurations may be detected + sort.Sort(nodeSorter(allNodes)) + + templateObjects := struct { + Services []*serviceUpdate + Nodes []*api.ServiceEntry + }{ + Services: services, + Nodes: allNodes, + } + + configuration, err := p.GetConfiguration("templates/consul_catalog.tmpl", funcMap, templateObjects) + if err != nil { + log.WithError(err).Error("Failed to create config") + } + + return configuration +} + +// Specific functions + +func (p *Provider) getFrontendRule(service serviceUpdate) string { + customFrontendRule := label.GetStringValue(service.TraefikLabels, label.TraefikFrontendRule, "") + if customFrontendRule == "" { + customFrontendRule = p.FrontEndRule + } + + tmpl := p.frontEndRuleTemplate + tmpl, err := tmpl.Parse(customFrontendRule) + if err != nil { + log.Errorf("Failed to parse Consul Catalog custom frontend rule: %v", err) + return "" + } + + templateObjects := struct { + ServiceName string + Domain string + Attributes []string + }{ + ServiceName: service.ServiceName, + Domain: p.Domain, + Attributes: service.Attributes, + } + + var buffer bytes.Buffer + err = tmpl.Execute(&buffer, templateObjects) + if err != nil { + log.Errorf("Failed to execute Consul Catalog custom frontend rule template: %v", err) + return "" + } + + return buffer.String() +} + +func (p *Provider) getServer(node *api.ServiceEntry) types.Server { + scheme := p.getAttribute(label.SuffixProtocol, node.Service.Tags, label.DefaultProtocol) + address := getBackendAddress(node) + + return types.Server{ + URL: fmt.Sprintf("%s://%s:%d", scheme, address, node.Service.Port), + Weight: p.getWeight(node.Service.Tags), + } +} + +func (p *Provider) setupFrontEndRuleTemplate() { + var FuncMap = template.FuncMap{ + "getAttribute": p.getAttribute, + "getTag": getTag, + "hasTag": hasTag, + } + p.frontEndRuleTemplate = template.New("consul catalog frontend rule").Funcs(FuncMap) +} + +// Specific functions + +// Only for compatibility +// Deprecated +func getLoadBalancer(labels map[string]string) *types.LoadBalancer { + if v, ok := labels[label.TraefikBackendLoadBalancer]; ok { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancer, label.TraefikBackendLoadBalancerMethod) + if !label.Has(labels, label.TraefikBackendLoadBalancerMethod) { + labels[label.TraefikBackendLoadBalancerMethod] = v + } + } + + return label.GetLoadBalancer(labels) +} + +// Only for compatibility +// Deprecated +func getCircuitBreaker(labels map[string]string) *types.CircuitBreaker { + if v, ok := labels[label.TraefikBackendCircuitBreaker]; ok { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendCircuitBreaker, label.TraefikBackendCircuitBreakerExpression) + if !label.Has(labels, label.TraefikBackendCircuitBreakerExpression) { + labels[label.TraefikBackendCircuitBreakerExpression] = v + } + } + + return label.GetCircuitBreaker(labels) +} + +func getServiceBackendName(service *serviceUpdate) string { + return strings.ToLower(service.ServiceName) +} + +func getNodeBackendName(node *api.ServiceEntry) string { + return strings.ToLower(node.Service.Service) +} + +func getBackendAddress(node *api.ServiceEntry) string { + if node.Service.Address != "" { + return node.Service.Address + } + return node.Node.Address +} + +func getServerName(node *api.ServiceEntry, index int) string { + serviceName := node.Service.Service + node.Service.Address + strconv.Itoa(node.Service.Port) + // TODO sort tags ? + serviceName += strings.Join(node.Service.Tags, "") + + hash := sha1.New() + _, err := hash.Write([]byte(serviceName)) + if err != nil { + // Impossible case + log.Error(err) + } else { + serviceName = base64.URLEncoding.EncodeToString(hash.Sum(nil)) + } + + // unique int at the end + return provider.Normalize(node.Service.Service + "-" + strconv.Itoa(index) + "-" + serviceName) +} + +func (p *Provider) getWeight(tags []string) int { + weight := p.getIntAttribute(label.SuffixWeight, tags, label.DefaultWeightInt) + + // Deprecated + deprecatedWeightTag := "backend." + label.SuffixWeight + if p.hasAttribute(deprecatedWeightTag, tags) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", + p.getPrefixedName(deprecatedWeightTag), p.getPrefixedName(label.SuffixWeight)) + + weight = p.getIntAttribute(deprecatedWeightTag, tags, label.DefaultWeightInt) + } + + return weight +} + +// Base functions + +func (p *Provider) hasAttribute(name string, tags []string) bool { + return hasTag(p.getPrefixedName(name), tags) +} + +func (p *Provider) getAttribute(name string, tags []string, defaultValue string) string { + return getTag(p.getPrefixedName(name), tags, defaultValue) +} + +func (p *Provider) getPrefixedName(name string) string { + if len(p.Prefix) > 0 && len(name) > 0 { + return p.Prefix + "." + name + } + return name +} + +func hasTag(name string, tags []string) bool { + lowerName := strings.ToLower(name) + + for _, tag := range tags { + lowerTag := strings.ToLower(tag) + + // Given the nature of Consul tags, which could be either singular markers, or key=value pairs + if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName { + return true + } + } + return false +} + +func hasTagPrefix(name string, tags []string) bool { + lowerName := strings.ToLower(name) + + for _, tag := range tags { + lowerTag := strings.ToLower(tag) + + if strings.HasPrefix(lowerTag, lowerName) { + return true + } + } + return false +} + +func getTag(name string, tags []string, defaultValue string) string { + lowerName := strings.ToLower(name) + + for _, tag := range tags { + lowerTag := strings.ToLower(tag) + + // Given the nature of Consul tags, which could be either singular markers, or key=value pairs + if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName { + // In case, where a tag might be a key=value, try to split it by the first '=' + kv := strings.SplitN(tag, "=", 2) + + // If the returned result is a key=value pair, return the 'value' component + if len(kv) == 2 { + return kv[1] + } + // If the returned result is a singular marker, return the 'key' component + return kv[0] + } + } + return defaultValue +} diff --git a/provider/consulcatalog/config_root.go b/provider/consulcatalog/config_root.go new file mode 100644 index 000000000..fc2e14889 --- /dev/null +++ b/provider/consulcatalog/config_root.go @@ -0,0 +1,10 @@ +package consulcatalog + +import "github.com/containous/traefik/types" + +func (p *Provider) buildConfiguration(catalog []catalogUpdate) *types.Configuration { + if p.TemplateVersion == 1 { + return p.buildConfigurationV1(catalog) + } + return p.buildConfigurationV2(catalog) +} diff --git a/provider/consulcatalog/config_test.go b/provider/consulcatalog/config_test.go new file mode 100644 index 000000000..960f8a139 --- /dev/null +++ b/provider/consulcatalog/config_test.go @@ -0,0 +1,518 @@ +package consulcatalog + +import ( + "testing" + "text/template" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/assert" +) + +func TestProviderBuildConfiguration(t *testing.T) { + p := &Provider{ + Domain: "localhost", + Prefix: "traefik", + ExposedByDefault: false, + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), + } + + testCases := []struct { + desc string + nodes []catalogUpdate + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "Should build config of nothing", + nodes: []catalogUpdate{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "Should build config with no frontend and backend", + nodes: []catalogUpdate{ + { + Service: &serviceUpdate{ + ServiceName: "test", + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "Should build config who contains one frontend and one backend", + nodes: []catalogUpdate{ + { + Service: &serviceUpdate{ + ServiceName: "test", + Attributes: []string{ + "random.foo=bar", + label.TraefikBackendLoadBalancerMethod + "=drr", + label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5", + label.TraefikBackendMaxConnAmount + "=1000", + label.TraefikBackendMaxConnExtractorFunc + "=client.ip", + label.TraefikFrontendAuthBasic + "=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + }, + Nodes: []*api.ServiceEntry{ + { + Service: &api.AgentService{ + Service: "test", + Address: "127.0.0.1", + Port: 80, + Tags: []string{ + "random.foo=bar", + label.Prefix + "backend.weight=42", // Deprecated label + label.TraefikFrontendPassHostHeader + "=true", + label.TraefikProtocol + "=https", + }, + }, + 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:test.localhost", + }, + }, + EntryPoints: []string{}, + BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test": { + Servers: map[string]types.Server{ + "test-0-us4-27hAOu2ARV7nNrmv6GoKlcA": { + URL: "https://127.0.0.1:80", + Weight: 42, + }, + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + MaxConn: &types.MaxConn{ + Amount: 1000, + ExtractorFunc: "client.ip", + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + nodes := fakeLoadTraefikLabelsSlice(test.nodes, p.Prefix) + + actualConfig := p.buildConfigurationV2(nodes) + assert.NotNil(t, actualConfig) + assert.Equal(t, test.expectedBackends, actualConfig.Backends) + assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + +func TestGetTag(t *testing.T) { + testCases := []struct { + desc string + tags []string + key string + defaultValue string + expected string + }{ + { + desc: "Should return value of foo.bar key", + tags: []string{ + "foo.bar=random", + "traefik.backend.weight=42", + "management", + }, + key: "foo.bar", + defaultValue: "0", + expected: "random", + }, + { + desc: "Should return default value when nonexistent key", + tags: []string{ + "foo.bar.foo.bar=random", + "traefik.backend.weight=42", + "management", + }, + key: "foo.bar", + defaultValue: "0", + expected: "0", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getTag(test.key, test.tags, test.defaultValue) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestHasTag(t *testing.T) { + testCases := []struct { + desc string + name string + tags []string + expected bool + }{ + { + desc: "tag without value", + name: "foo", + tags: []string{"foo"}, + expected: true, + }, + { + desc: "tag with value", + name: "foo", + tags: []string{"foo=true"}, + expected: true, + }, + { + desc: "missing tag", + name: "foo", + tags: []string{"foobar=true"}, + expected: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := hasTag(test.name, test.tags) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestProviderGetPrefixedName(t *testing.T) { + testCases := []struct { + desc string + name string + prefix string + expected string + }{ + { + desc: "empty name with prefix", + name: "", + prefix: "foo", + expected: "", + }, + { + desc: "empty name without prefix", + name: "", + prefix: "", + expected: "", + }, + { + desc: "with prefix", + name: "bar", + prefix: "foo", + expected: "foo.bar", + }, + { + desc: "without prefix", + name: "bar", + prefix: "", + expected: "bar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{Prefix: test.prefix} + + actual := p.getPrefixedName(test.name) + assert.Equal(t, test.expected, actual) + }) + } + +} + +func TestProviderGetAttribute(t *testing.T) { + testCases := []struct { + desc string + tags []string + key string + defaultValue string + prefix string + expected string + }{ + { + desc: "Should return tag value 42", + prefix: "traefik", + tags: []string{ + "foo.bar=ramdom", + "traefik.backend.weight=42", + }, + key: "backend.weight", + defaultValue: "0", + expected: "42", + }, + { + desc: "Should return tag default value 0", + prefix: "traefik", + tags: []string{ + "foo.bar=ramdom", + "traefik.backend.wei=42", + }, + key: "backend.weight", + defaultValue: "0", + expected: "0", + }, + { + desc: "Should return tag value 42 when empty prefix", + tags: []string{ + "foo.bar=ramdom", + "backend.weight=42", + }, + key: "backend.weight", + defaultValue: "0", + expected: "42", + }, + { + desc: "Should return default value 0 when empty prefix", + tags: []string{ + "foo.bar=ramdom", + "backend.wei=42", + }, + key: "backend.weight", + defaultValue: "0", + expected: "0", + }, + { + desc: "Should return for.bar key value random when empty prefix", + tags: []string{ + "foo.bar=ramdom", + "backend.wei=42", + }, + key: "foo.bar", + defaultValue: "random", + expected: "ramdom", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + Domain: "localhost", + Prefix: test.prefix, + } + + actual := p.getAttribute(test.key, test.tags, test.defaultValue) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestProviderGetFrontendRule(t *testing.T) { + testCases := []struct { + desc string + service serviceUpdate + expected string + }{ + { + desc: "Should return default host foo.localhost", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{}, + }, + expected: "Host:foo.localhost", + }, + { + desc: "Should return host *.example.com", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=Host:*.example.com", + }, + }, + expected: "Host:*.example.com", + }, + { + desc: "Should return host foo.example.com", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=Host:{{.ServiceName}}.example.com", + }, + }, + expected: "Host:foo.example.com", + }, + { + desc: "Should return path prefix /bar", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=PathPrefix:{{getTag \"contextPath\" .Attributes \"/\"}}", + "contextPath=/bar", + }, + }, + expected: "PathPrefix:/bar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + Domain: "localhost", + Prefix: "traefik", + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), + } + p.setupFrontEndRuleTemplate() + + labels := tagsToNeutralLabels(test.service.Attributes, p.Prefix) + test.service.TraefikLabels = labels + + actual := p.getFrontendRule(test.service) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetBackendAddress(t *testing.T) { + testCases := []struct { + desc string + node *api.ServiceEntry + expected string + }{ + { + desc: "Should return the address of the service", + node: &api.ServiceEntry{ + Node: &api.Node{ + Address: "10.1.0.1", + }, + Service: &api.AgentService{ + Address: "10.2.0.1", + }, + }, + expected: "10.2.0.1", + }, + { + desc: "Should return the address of the node", + node: &api.ServiceEntry{ + Node: &api.Node{ + Address: "10.1.0.1", + }, + Service: &api.AgentService{ + Address: "", + }, + }, + expected: "10.1.0.1", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getBackendAddress(test.node) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetServerName(t *testing.T) { + testCases := []struct { + desc string + node *api.ServiceEntry + expected string + }{ + { + desc: "Should create backend name without tags", + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{}, + }, + }, + expected: "api-0-eUSiqD6uNvvh6zxsY-OeRi8ZbaE", + }, + { + desc: "Should create backend name with multiple tags", + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{"traefik.weight=42", "traefik.enable=true"}, + }, + }, + expected: "api-1-eJ8MR2JxjXyZgs1bhurVa0-9OI8", + }, + { + desc: "Should create backend name with one tag", + node: &api.ServiceEntry{ + Service: &api.AgentService{ + Service: "api", + Address: "10.0.0.1", + Port: 80, + Tags: []string{"a funny looking tag"}, + }, + }, + expected: "api-2-lMCDCsG7sh0SCXOHo4oBOQB-9D4", + }, + } + + for i, test := range testCases { + test := test + i := i + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getServerName(test.node, i) + assert.Equal(t, test.expected, actual) + }) + } +} + +func fakeLoadTraefikLabelsSlice(nodes []catalogUpdate, prefix string) []catalogUpdate { + var result []catalogUpdate + + for _, node := range nodes { + labels := tagsToNeutralLabels(node.Service.Attributes, prefix) + node.Service.TraefikLabels = labels + result = append(result, node) + } + + return result +} diff --git a/provider/consulcatalog/consul_catalog.go b/provider/consulcatalog/consul_catalog.go index e79fe09d4..65c44ac49 100644 --- a/provider/consulcatalog/consul_catalog.go +++ b/provider/consulcatalog/consul_catalog.go @@ -2,6 +2,7 @@ package consulcatalog import ( "errors" + "strconv" "strings" "sync" "text/template" @@ -48,8 +49,9 @@ type Service struct { } type serviceUpdate struct { - ServiceName string - Attributes []string + ServiceName string + Attributes []string + TraefikLabels map[string]string } type catalogUpdate struct { @@ -446,10 +448,13 @@ func (p *Provider) healthyNodes(service string) (catalogUpdate, error) { ).(map[string]bool)).([]string) }, []string{}, nodes).([]string) + labels := tagsToNeutralLabels(tags, p.Prefix) + return catalogUpdate{ Service: &serviceUpdate{ - ServiceName: service, - Attributes: tags, + ServiceName: service, + Attributes: tags, + TraefikLabels: labels, }, Nodes: nodes, }, nil @@ -473,7 +478,18 @@ func (p *Provider) nodeFilter(service string, node *api.ServiceEntry) bool { } func (p *Provider) isServiceEnabled(node *api.ServiceEntry) bool { - return p.getBoolAttribute(label.SuffixEnable, node.Service.Tags, p.ExposedByDefault) + rawValue := getTag(p.getPrefixedName(label.SuffixEnable), node.Service.Tags, "") + + if len(rawValue) == 0 { + return p.ExposedByDefault + } + + value, err := strconv.ParseBool(rawValue) + if err != nil { + log.Errorf("Invalid value for %s: %s", label.SuffixEnable, rawValue) + return p.ExposedByDefault + } + return value } func (p *Provider) getConstraintTags(tags []string) []string { diff --git a/provider/consulcatalog/consul_catalog_config.go b/provider/consulcatalog/consul_catalog_config.go deleted file mode 100644 index 14ab512b6..000000000 --- a/provider/consulcatalog/consul_catalog_config.go +++ /dev/null @@ -1,589 +0,0 @@ -package consulcatalog - -import ( - "bytes" - "crypto/sha1" - "encoding/base64" - "math" - "sort" - "strconv" - "strings" - "text/template" - - "github.com/containous/traefik/log" - "github.com/containous/traefik/provider" - "github.com/containous/traefik/provider/label" - "github.com/containous/traefik/types" - "github.com/hashicorp/consul/api" -) - -func (p *Provider) buildConfiguration(catalog []catalogUpdate) *types.Configuration { - var FuncMap = template.FuncMap{ - "getAttribute": p.getAttribute, - "getTag": getTag, - "hasTag": hasTag, - - // Backend functions - "getBackend": getNodeBackendName, // TODO Deprecated [breaking] getBackend -> getNodeBackendName - "getNodeBackendName": getNodeBackendName, - "getServiceBackendName": getServiceBackendName, - "getBackendAddress": getBackendAddress, - "getBackendName": getServerName, // TODO Deprecated [breaking] getBackendName -> getServerName - "getServerName": getServerName, - "hasMaxconnAttributes": p.hasMaxConnAttributes, // TODO Deprecated [breaking] - "getSticky": p.getSticky, // TODO Deprecated [breaking] - "hasStickinessLabel": p.hasStickinessLabel, // TODO Deprecated [breaking] - "getStickinessCookieName": p.getStickinessCookieName, // TODO Deprecated [breaking] - "getWeight": p.getWeight, // TODO Deprecated [breaking] Must replaced by a simple: "getWeight": p.getFuncIntAttribute(label.SuffixWeight, 0) - "getProtocol": p.getFuncStringAttribute(label.SuffixProtocol, label.DefaultProtocol), - "getCircuitBreaker": p.getCircuitBreaker, - "getLoadBalancer": p.getLoadBalancer, - "getMaxConn": p.getMaxConn, - "getHealthCheck": p.getHealthCheck, - "getBuffering": p.getBuffering, - - // Frontend functions - "getFrontendRule": p.getFrontendRule, - "getBasicAuth": p.getFuncSliceAttribute(label.SuffixFrontendAuthBasic), - "getEntryPoints": getEntryPoints, // TODO Deprecated [breaking] - "getFrontEndEntryPoints": p.getFuncSliceAttribute(label.SuffixFrontendEntryPoints), // TODO [breaking] rename to getEntryPoints when getEntryPoints will be removed - "getPriority": p.getFuncIntAttribute(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt), - "getPassHostHeader": p.getFuncBoolAttribute(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), - "getPassTLSCert": p.getFuncBoolAttribute(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), - "getWhiteList": p.getWhiteList, - "getRedirect": p.getRedirect, - "hasErrorPages": p.getFuncHasAttributePrefix(label.BaseFrontendErrorPage), - "getErrorPages": p.getErrorPages, - "hasRateLimit": p.getFuncHasAttributePrefix(label.BaseFrontendRateLimit), - "getRateLimit": p.getRateLimit, - "getHeaders": p.getHeaders, - } - - var allNodes []*api.ServiceEntry - var services []*serviceUpdate - for _, info := range catalog { - if len(info.Nodes) > 0 { - services = append(services, info.Service) - allNodes = append(allNodes, info.Nodes...) - } - } - // Ensure a stable ordering of nodes so that identical configurations may be detected - sort.Sort(nodeSorter(allNodes)) - - templateObjects := struct { - Services []*serviceUpdate - Nodes []*api.ServiceEntry - }{ - Services: services, - Nodes: allNodes, - } - - configuration, err := p.GetConfiguration("templates/consul_catalog.tmpl", FuncMap, templateObjects) - if err != nil { - log.WithError(err).Error("Failed to create config") - } - - return configuration -} - -func (p *Provider) setupFrontEndRuleTemplate() { - var FuncMap = template.FuncMap{ - "getAttribute": p.getAttribute, - "getTag": getTag, - "hasTag": hasTag, - } - tmpl := template.New("consul catalog frontend rule").Funcs(FuncMap) - p.frontEndRuleTemplate = tmpl -} - -// Specific functions - -func (p *Provider) getFrontendRule(service serviceUpdate) string { - customFrontendRule := p.getAttribute(label.SuffixFrontendRule, service.Attributes, "") - if customFrontendRule == "" { - customFrontendRule = p.FrontEndRule - } - - tmpl := p.frontEndRuleTemplate - tmpl, err := tmpl.Parse(customFrontendRule) - if err != nil { - log.Errorf("Failed to parse Consul Catalog custom frontend rule: %v", err) - return "" - } - - templateObjects := struct { - ServiceName string - Domain string - Attributes []string - }{ - ServiceName: service.ServiceName, - Domain: p.Domain, - Attributes: service.Attributes, - } - - var buffer bytes.Buffer - err = tmpl.Execute(&buffer, templateObjects) - if err != nil { - log.Errorf("Failed to execute Consul Catalog custom frontend rule template: %v", err) - return "" - } - - return buffer.String() -} - -// Deprecated -func (p *Provider) hasMaxConnAttributes(attributes []string) bool { - amount := p.getAttribute(label.SuffixBackendMaxConnAmount, attributes, "") - extractorFunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, attributes, "") - return amount != "" && extractorFunc != "" -} - -// Deprecated -func getEntryPoints(list string) []string { - return strings.Split(list, ",") -} - -func getNodeBackendName(node *api.ServiceEntry) string { - return strings.ToLower(node.Service.Service) -} - -func getServiceBackendName(service *serviceUpdate) string { - return strings.ToLower(service.ServiceName) -} - -func getBackendAddress(node *api.ServiceEntry) string { - if node.Service.Address != "" { - return node.Service.Address - } - return node.Node.Address -} - -func getServerName(node *api.ServiceEntry, index int) string { - serviceName := node.Service.Service + node.Service.Address + strconv.Itoa(node.Service.Port) - // TODO sort tags ? - serviceName += strings.Join(node.Service.Tags, "") - - hash := sha1.New() - _, err := hash.Write([]byte(serviceName)) - if err != nil { - // Impossible case - log.Error(err) - } else { - serviceName = base64.URLEncoding.EncodeToString(hash.Sum(nil)) - } - - // unique int at the end - return provider.Normalize(node.Service.Service + "-" + strconv.Itoa(index) + "-" + serviceName) -} - -// TODO: Deprecated -// replaced by Stickiness -// Deprecated -func (p *Provider) getSticky(tags []string) string { - stickyTag := p.getAttribute(label.SuffixBackendLoadBalancerSticky, tags, "") - if len(stickyTag) > 0 { - log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) - } else { - stickyTag = "false" - } - return stickyTag -} - -// Deprecated -func (p *Provider) hasStickinessLabel(tags []string) bool { - stickinessTag := p.getAttribute(label.SuffixBackendLoadBalancerStickiness, tags, "") - return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true") -} - -// Deprecated -func (p *Provider) getStickinessCookieName(tags []string) string { - return p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, "") -} - -// Deprecated -func (p *Provider) getWeight(tags []string) int { - weight := p.getIntAttribute(label.SuffixWeight, tags, label.DefaultWeightInt) - - // Deprecated - deprecatedWeightTag := "backend." + label.SuffixWeight - if p.hasAttribute(deprecatedWeightTag, tags) { - log.Warnf("Deprecated configuration found: %s. Please use %s.", - p.getPrefixedName(deprecatedWeightTag), p.getPrefixedName(label.SuffixWeight)) - - weight = p.getIntAttribute(deprecatedWeightTag, tags, label.DefaultWeightInt) - } - - return weight -} - -func (p *Provider) getCircuitBreaker(tags []string) *types.CircuitBreaker { - circuitBreaker := p.getAttribute(label.SuffixBackendCircuitBreakerExpression, tags, "") - - if p.hasAttribute(label.SuffixBackendCircuitBreaker, tags) { - log.Warnf("Deprecated configuration found: %s. Please use %s.", - p.getPrefixedName(label.SuffixBackendCircuitBreaker), p.getPrefixedName(label.SuffixBackendCircuitBreakerExpression)) - - circuitBreaker = p.getAttribute(label.SuffixBackendCircuitBreaker, tags, "") - } - - if len(circuitBreaker) == 0 { - return nil - } - - return &types.CircuitBreaker{Expression: circuitBreaker} -} - -func (p *Provider) getLoadBalancer(tags []string) *types.LoadBalancer { - rawSticky := p.getSticky(tags) - sticky, err := strconv.ParseBool(rawSticky) - if err != nil { - log.Debugf("Invalid sticky value: %s", rawSticky) - sticky = false - } - - method := p.getAttribute(label.SuffixBackendLoadBalancerMethod, tags, label.DefaultBackendLoadBalancerMethod) - - // Deprecated - deprecatedMethodTag := "backend.loadbalancer" - if p.hasAttribute(deprecatedMethodTag, tags) { - log.Warnf("Deprecated configuration found: %s. Please use %s.", - p.getPrefixedName(deprecatedMethodTag), p.getPrefixedName(label.SuffixWeight)) - - method = p.getAttribute(deprecatedMethodTag, tags, label.SuffixBackendLoadBalancerMethod) - } - - lb := &types.LoadBalancer{ - Method: method, - Sticky: sticky, - } - - if p.getBoolAttribute(label.SuffixBackendLoadBalancerStickiness, tags, false) { - lb.Stickiness = &types.Stickiness{ - CookieName: p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, ""), - } - } - - return lb -} - -func (p *Provider) getMaxConn(tags []string) *types.MaxConn { - amount := p.getInt64Attribute(label.SuffixBackendMaxConnAmount, tags, math.MinInt64) - extractorFunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, tags, label.DefaultBackendMaxconnExtractorFunc) - - if amount == math.MinInt64 || len(extractorFunc) == 0 { - return nil - } - - return &types.MaxConn{ - Amount: amount, - ExtractorFunc: extractorFunc, - } -} - -func (p *Provider) getHealthCheck(tags []string) *types.HealthCheck { - path := p.getAttribute(label.SuffixBackendHealthCheckPath, tags, "") - - if len(path) == 0 { - return nil - } - - port := p.getIntAttribute(label.SuffixBackendHealthCheckPort, tags, label.DefaultBackendHealthCheckPort) - interval := p.getAttribute(label.SuffixBackendHealthCheckInterval, tags, "") - - return &types.HealthCheck{ - Path: path, - Port: port, - Interval: interval, - } -} - -func (p *Provider) getBuffering(tags []string) *types.Buffering { - if !p.hasAttributePrefix(label.SuffixBackendBuffering, tags) { - return nil - } - - return &types.Buffering{ - MaxRequestBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMaxRequestBodyBytes, tags, 0), - MaxResponseBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMaxResponseBodyBytes, tags, 0), - MemRequestBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMemRequestBodyBytes, tags, 0), - MemResponseBodyBytes: p.getInt64Attribute(label.SuffixBackendBufferingMemResponseBodyBytes, tags, 0), - RetryExpression: p.getAttribute(label.SuffixBackendBufferingRetryExpression, tags, ""), - } -} - -func (p *Provider) getWhiteList(tags []string) *types.WhiteList { - ranges := p.getSliceAttribute(label.SuffixFrontendWhiteListSourceRange, tags) - - if len(ranges) > 0 { - return &types.WhiteList{ - SourceRange: ranges, - UseXForwardedFor: p.getBoolAttribute(label.SuffixFrontendWhiteListUseXForwardedFor, tags, false), - } - } - - return nil -} - -func (p *Provider) getRedirect(tags []string) *types.Redirect { - permanent := p.getBoolAttribute(label.SuffixFrontendRedirectPermanent, tags, false) - - if p.hasAttribute(label.SuffixFrontendRedirectEntryPoint, tags) { - return &types.Redirect{ - EntryPoint: p.getAttribute(label.SuffixFrontendRedirectEntryPoint, tags, ""), - Permanent: permanent, - } - } - - if p.hasAttribute(label.SuffixFrontendRedirectRegex, tags) && p.hasAttribute(label.SuffixFrontendRedirectReplacement, tags) { - return &types.Redirect{ - Regex: p.getAttribute(label.SuffixFrontendRedirectRegex, tags, ""), - Replacement: p.getAttribute(label.SuffixFrontendRedirectReplacement, tags, ""), - Permanent: permanent, - } - } - - return nil -} - -func (p *Provider) getErrorPages(tags []string) map[string]*types.ErrorPage { - labels := p.parseTagsToNeutralLabels(tags) - - prefix := label.Prefix + label.BaseFrontendErrorPage - return label.ParseErrorPages(labels, prefix, label.RegexpFrontendErrorPage) -} - -func (p *Provider) getRateLimit(tags []string) *types.RateLimit { - extractorFunc := p.getAttribute(label.SuffixFrontendRateLimitExtractorFunc, tags, "") - if len(extractorFunc) == 0 { - return nil - } - - labels := p.parseTagsToNeutralLabels(tags) - - prefix := label.Prefix + label.BaseFrontendRateLimit - limits := label.ParseRateSets(labels, prefix, label.RegexpFrontendRateLimit) - - return &types.RateLimit{ - ExtractorFunc: extractorFunc, - RateSet: limits, - } -} - -func (p *Provider) getHeaders(tags []string) *types.Headers { - headers := &types.Headers{ - CustomRequestHeaders: p.getMapAttribute(label.SuffixFrontendRequestHeaders, tags), - CustomResponseHeaders: p.getMapAttribute(label.SuffixFrontendResponseHeaders, tags), - SSLProxyHeaders: p.getMapAttribute(label.SuffixFrontendHeadersSSLProxyHeaders, tags), - AllowedHosts: p.getSliceAttribute(label.SuffixFrontendHeadersAllowedHosts, tags), - HostsProxyHeaders: p.getSliceAttribute(label.SuffixFrontendHeadersHostsProxyHeaders, tags), - SSLHost: p.getAttribute(label.SuffixFrontendHeadersSSLHost, tags, ""), - CustomFrameOptionsValue: p.getAttribute(label.SuffixFrontendHeadersCustomFrameOptionsValue, tags, ""), - ContentSecurityPolicy: p.getAttribute(label.SuffixFrontendHeadersContentSecurityPolicy, tags, ""), - PublicKey: p.getAttribute(label.SuffixFrontendHeadersPublicKey, tags, ""), - ReferrerPolicy: p.getAttribute(label.SuffixFrontendHeadersReferrerPolicy, tags, ""), - CustomBrowserXSSValue: p.getAttribute(label.SuffixFrontendHeadersCustomBrowserXSSValue, tags, ""), - STSSeconds: p.getInt64Attribute(label.SuffixFrontendHeadersSTSSeconds, tags, 0), - SSLRedirect: p.getBoolAttribute(label.SuffixFrontendHeadersSSLRedirect, tags, false), - SSLTemporaryRedirect: p.getBoolAttribute(label.SuffixFrontendHeadersSSLTemporaryRedirect, tags, false), - STSIncludeSubdomains: p.getBoolAttribute(label.SuffixFrontendHeadersSTSIncludeSubdomains, tags, false), - STSPreload: p.getBoolAttribute(label.SuffixFrontendHeadersSTSPreload, tags, false), - ForceSTSHeader: p.getBoolAttribute(label.SuffixFrontendHeadersForceSTSHeader, tags, false), - FrameDeny: p.getBoolAttribute(label.SuffixFrontendHeadersFrameDeny, tags, false), - ContentTypeNosniff: p.getBoolAttribute(label.SuffixFrontendHeadersContentTypeNosniff, tags, false), - BrowserXSSFilter: p.getBoolAttribute(label.SuffixFrontendHeadersBrowserXSSFilter, tags, false), - IsDevelopment: p.getBoolAttribute(label.SuffixFrontendHeadersIsDevelopment, tags, false), - } - - if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { - return nil - } - - return headers -} - -// Base functions - -func (p *Provider) parseTagsToNeutralLabels(tags []string) map[string]string { - var labels map[string]string - - for _, tag := range tags { - if strings.HasPrefix(tag, p.Prefix) { - - parts := strings.SplitN(tag, "=", 2) - if len(parts) == 2 { - if labels == nil { - labels = make(map[string]string) - } - - // replace custom prefix by the generic prefix - key := label.Prefix + strings.TrimPrefix(parts[0], p.Prefix+".") - labels[key] = parts[1] - } - } - } - - return labels -} - -func (p *Provider) getFuncStringAttribute(name string, defaultValue string) func(tags []string) string { - return func(tags []string) string { - return p.getAttribute(name, tags, defaultValue) - } -} - -func (p *Provider) getFuncSliceAttribute(name string) func(tags []string) []string { - return func(tags []string) []string { - return p.getSliceAttribute(name, tags) - } -} - -func (p *Provider) getMapAttribute(name string, tags []string) map[string]string { - rawValue := getTag(p.getPrefixedName(name), tags, "") - - if len(rawValue) == 0 { - return nil - } - - return label.ParseMapValue(p.getPrefixedName(name), rawValue) -} - -func (p *Provider) getFuncIntAttribute(name string, defaultValue int) func(tags []string) int { - return func(tags []string) int { - return p.getIntAttribute(name, tags, defaultValue) - } -} - -func (p *Provider) getFuncBoolAttribute(name string, defaultValue bool) func(tags []string) bool { - return func(tags []string) bool { - return p.getBoolAttribute(name, tags, defaultValue) - } -} - -func (p *Provider) getFuncHasAttributePrefix(name string) func(tags []string) bool { - return func(tags []string) bool { - return p.hasAttributePrefix(name, tags) - } -} - -func (p *Provider) getInt64Attribute(name string, tags []string, defaultValue int64) int64 { - rawValue := getTag(p.getPrefixedName(name), tags, "") - - if len(rawValue) == 0 { - return defaultValue - } - - value, err := strconv.ParseInt(rawValue, 10, 64) - if err != nil { - log.Errorf("Invalid value for %s: %s", name, rawValue) - return defaultValue - } - return value -} - -func (p *Provider) getIntAttribute(name string, tags []string, defaultValue int) int { - rawValue := getTag(p.getPrefixedName(name), tags, "") - - if len(rawValue) == 0 { - return defaultValue - } - - value, err := strconv.Atoi(rawValue) - if err != nil { - log.Errorf("Invalid value for %s: %s", name, rawValue) - return defaultValue - } - return value -} - -func (p *Provider) getSliceAttribute(name string, tags []string) []string { - rawValue := getTag(p.getPrefixedName(name), tags, "") - - if len(rawValue) == 0 { - return nil - } - return label.SplitAndTrimString(rawValue, ",") -} - -func (p *Provider) getBoolAttribute(name string, tags []string, defaultValue bool) bool { - rawValue := getTag(p.getPrefixedName(name), tags, "") - - if len(rawValue) == 0 { - return defaultValue - } - - value, err := strconv.ParseBool(rawValue) - if err != nil { - log.Errorf("Invalid value for %s: %s", name, rawValue) - return defaultValue - } - return value -} - -func (p *Provider) hasAttribute(name string, tags []string) bool { - return hasTag(p.getPrefixedName(name), tags) -} - -func (p *Provider) hasAttributePrefix(name string, tags []string) bool { - return hasTagPrefix(p.getPrefixedName(name), tags) -} - -func (p *Provider) getAttribute(name string, tags []string, defaultValue string) string { - return getTag(p.getPrefixedName(name), tags, defaultValue) -} - -func (p *Provider) getPrefixedName(name string) string { - if len(p.Prefix) > 0 && len(name) > 0 { - return p.Prefix + "." + name - } - return name -} - -func hasTag(name string, tags []string) bool { - lowerName := strings.ToLower(name) - - for _, tag := range tags { - lowerTag := strings.ToLower(tag) - - // Given the nature of Consul tags, which could be either singular markers, or key=value pairs - if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName { - return true - } - } - return false -} - -func hasTagPrefix(name string, tags []string) bool { - lowerName := strings.ToLower(name) - - for _, tag := range tags { - lowerTag := strings.ToLower(tag) - - if strings.HasPrefix(lowerTag, lowerName) { - return true - } - } - return false -} - -func getTag(name string, tags []string, defaultValue string) string { - lowerName := strings.ToLower(name) - - for _, tag := range tags { - lowerTag := strings.ToLower(tag) - - // Given the nature of Consul tags, which could be either singular markers, or key=value pairs - if strings.HasPrefix(lowerTag, lowerName+"=") || lowerTag == lowerName { - // In case, where a tag might be a key=value, try to split it by the first '=' - kv := strings.SplitN(tag, "=", 2) - - // If the returned result is a key=value pair, return the 'value' component - if len(kv) == 2 { - return kv[1] - } - // If the returned result is a singular marker, return the 'key' component - return kv[0] - } - - } - return defaultValue -} diff --git a/provider/consulcatalog/consul_catalog_config_test.go b/provider/consulcatalog/consul_catalog_config_test.go deleted file mode 100644 index 8f8b475d0..000000000 --- a/provider/consulcatalog/consul_catalog_config_test.go +++ /dev/null @@ -1,1387 +0,0 @@ -package consulcatalog - -import ( - "testing" - "text/template" - "time" - - "github.com/containous/flaeg" - "github.com/containous/traefik/provider/label" - "github.com/containous/traefik/types" - "github.com/hashicorp/consul/api" - "github.com/stretchr/testify/assert" -) - -func TestProviderBuildConfiguration(t *testing.T) { - provider := &Provider{ - Domain: "localhost", - Prefix: "traefik", - ExposedByDefault: false, - FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", - frontEndRuleTemplate: template.New("consul catalog frontend rule"), - } - - testCases := []struct { - desc string - nodes []catalogUpdate - expectedFrontends map[string]*types.Frontend - expectedBackends map[string]*types.Backend - }{ - { - desc: "Should build config of nothing", - nodes: []catalogUpdate{}, - expectedFrontends: map[string]*types.Frontend{}, - expectedBackends: map[string]*types.Backend{}, - }, - { - desc: "Should build config with no frontend and backend", - nodes: []catalogUpdate{ - { - Service: &serviceUpdate{ - ServiceName: "test", - }, - }, - }, - expectedFrontends: map[string]*types.Frontend{}, - expectedBackends: map[string]*types.Backend{}, - }, - { - desc: "Should build config who contains one frontend and one backend", - nodes: []catalogUpdate{ - { - Service: &serviceUpdate{ - ServiceName: "test", - Attributes: []string{ - "random.foo=bar", - label.Prefix + "backend.loadbalancer=drr", - label.TraefikBackendCircuitBreaker + "=NetworkErrorRatio() > 0.5", - label.TraefikBackendMaxConnAmount + "=1000", - label.TraefikBackendMaxConnExtractorFunc + "=client.ip", - label.TraefikFrontendAuthBasic + "=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - }, - }, - Nodes: []*api.ServiceEntry{ - { - Service: &api.AgentService{ - Service: "test", - Address: "127.0.0.1", - Port: 80, - Tags: []string{ - "random.foo=bar", - label.Prefix + "backend.weight=42", - label.TraefikFrontendPassHostHeader + "=true", - label.TraefikProtocol + "=https", - }, - }, - 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:test.localhost", - }, - }, - EntryPoints: []string{}, - BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-test": { - Servers: map[string]types.Server{ - "test-0-us4-27hAOu2ARV7nNrmv6GoKlcA": { - URL: "https://127.0.0.1:80", - Weight: 42, - }, - }, - CircuitBreaker: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - LoadBalancer: &types.LoadBalancer{ - Method: "drr", - }, - MaxConn: &types.MaxConn{ - Amount: 1000, - ExtractorFunc: "client.ip", - }, - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actualConfig := provider.buildConfiguration(test.nodes) - assert.NotNil(t, actualConfig) - assert.Equal(t, test.expectedBackends, actualConfig.Backends) - assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) - }) - } -} - -func TestGetTag(t *testing.T) { - testCases := []struct { - desc string - tags []string - key string - defaultValue string - expected string - }{ - { - desc: "Should return value of foo.bar key", - tags: []string{ - "foo.bar=random", - "traefik.backend.weight=42", - "management", - }, - key: "foo.bar", - defaultValue: "0", - expected: "random", - }, - { - desc: "Should return default value when nonexistent key", - tags: []string{ - "foo.bar.foo.bar=random", - "traefik.backend.weight=42", - "management", - }, - key: "foo.bar", - defaultValue: "0", - expected: "0", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getTag(test.key, test.tags, test.defaultValue) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestHasTag(t *testing.T) { - testCases := []struct { - desc string - name string - tags []string - expected bool - }{ - { - desc: "tag without value", - name: "foo", - tags: []string{"foo"}, - expected: true, - }, - { - desc: "tag with value", - name: "foo", - tags: []string{"foo=true"}, - expected: true, - }, - { - desc: "missing tag", - name: "foo", - tags: []string{"foobar=true"}, - expected: false, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := hasTag(test.name, test.tags) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestProviderGetPrefixedName(t *testing.T) { - testCases := []struct { - desc string - name string - prefix string - expected string - }{ - { - desc: "empty name with prefix", - name: "", - prefix: "foo", - expected: "", - }, - { - desc: "empty name without prefix", - name: "", - prefix: "", - expected: "", - }, - { - desc: "with prefix", - name: "bar", - prefix: "foo", - expected: "foo.bar", - }, - { - desc: "without prefix", - name: "bar", - prefix: "", - expected: "bar", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - pro := &Provider{Prefix: test.prefix} - - actual := pro.getPrefixedName(test.name) - assert.Equal(t, test.expected, actual) - }) - } - -} - -func TestProviderGetAttribute(t *testing.T) { - testCases := []struct { - desc string - tags []string - key string - defaultValue string - prefix string - expected string - }{ - { - desc: "Should return tag value 42", - prefix: "traefik", - tags: []string{ - "foo.bar=ramdom", - "traefik.backend.weight=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "42", - }, - { - desc: "Should return tag default value 0", - prefix: "traefik", - tags: []string{ - "foo.bar=ramdom", - "traefik.backend.wei=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "0", - }, - { - desc: "Should return tag value 42 when empty prefix", - tags: []string{ - "foo.bar=ramdom", - "backend.weight=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "42", - }, - { - desc: "Should return default value 0 when empty prefix", - tags: []string{ - "foo.bar=ramdom", - "backend.wei=42", - }, - key: "backend.weight", - defaultValue: "0", - expected: "0", - }, - { - desc: "Should return for.bar key value random when empty prefix", - tags: []string{ - "foo.bar=ramdom", - "backend.wei=42", - }, - key: "foo.bar", - defaultValue: "random", - expected: "ramdom", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - p := &Provider{ - Domain: "localhost", - Prefix: test.prefix, - } - - actual := p.getAttribute(test.key, test.tags, test.defaultValue) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestProviderGetIntAttribute(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - name string - tags []string - defaultValue int - expected int - }{ - { - desc: "should return default value when empty name", - name: "", - tags: []string{"traefik.foo=10"}, - defaultValue: 666, - expected: 666, - }, - { - desc: "should return default value when empty tags", - name: "traefik.foo", - tags: nil, - expected: 0, - }, - { - desc: "should return default value when value is not a int", - name: "foo", - tags: []string{"traefik.foo=bar"}, - expected: 0, - }, - { - desc: "should return a value when tag exist", - name: "foo", - tags: []string{"traefik.foo=10"}, - expected: 10, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getIntAttribute(test.name, test.tags, test.defaultValue) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetInt64Attribute(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - name string - tags []string - defaultValue int64 - expected int64 - }{ - { - desc: "should return default value when empty name", - name: "", - tags: []string{"traefik.foo=10"}, - defaultValue: 666, - expected: 666, - }, - { - desc: "should return default value when empty tags", - name: "traefik.foo", - tags: nil, - expected: 0, - }, - { - desc: "should return default value when value is not a int", - name: "foo", - tags: []string{"traefik.foo=bar"}, - expected: 0, - }, - { - desc: "should return a value when tag exist", - name: "foo", - tags: []string{"traefik.foo=10"}, - expected: 10, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getInt64Attribute(test.name, test.tags, test.defaultValue) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetBoolAttribute(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - name string - tags []string - defaultValue bool - expected bool - }{ - { - desc: "should return default value when empty name", - name: "", - tags: []string{"traefik.foo=true"}, - defaultValue: true, - expected: true, - }, - { - desc: "should return default value when empty tags", - name: "traefik.foo", - tags: nil, - expected: false, - }, - { - desc: "should return default value when value is not a bool", - name: "foo", - tags: []string{"traefik.foo=bar"}, - expected: false, - }, - { - desc: "should return a value when tag exist", - name: "foo", - tags: []string{"traefik.foo=true"}, - expected: true, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getBoolAttribute(test.name, test.tags, test.defaultValue) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetSliceAttribute(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - name string - tags []string - expected []string - }{ - { - desc: "should return nil when empty name", - name: "", - tags: []string{"traefik.foo=bar,bor,bir"}, - expected: nil, - }, - { - desc: "should return nil when empty tags", - name: "foo", - tags: nil, - expected: nil, - }, - { - desc: "should return nil when tag doesn't have value", - name: "", - tags: []string{"traefik.foo="}, - expected: nil, - }, - { - desc: "should return a slice when tag contains comma separated values", - name: "foo", - tags: []string{"traefik.foo=bar,bor,bir"}, - expected: []string{"bar", "bor", "bir"}, - }, - { - desc: "should return a slice when tag contains one value", - name: "foo", - tags: []string{"traefik.foo=bar"}, - expected: []string{"bar"}, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getSliceAttribute(test.name, test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetFrontendRule(t *testing.T) { - testCases := []struct { - desc string - service serviceUpdate - expected string - }{ - { - desc: "Should return default host foo.localhost", - service: serviceUpdate{ - ServiceName: "foo", - Attributes: []string{}, - }, - expected: "Host:foo.localhost", - }, - { - desc: "Should return host *.example.com", - service: serviceUpdate{ - ServiceName: "foo", - Attributes: []string{ - "traefik.frontend.rule=Host:*.example.com", - }, - }, - expected: "Host:*.example.com", - }, - { - desc: "Should return host foo.example.com", - service: serviceUpdate{ - ServiceName: "foo", - Attributes: []string{ - "traefik.frontend.rule=Host:{{.ServiceName}}.example.com", - }, - }, - expected: "Host:foo.example.com", - }, - { - desc: "Should return path prefix /bar", - service: serviceUpdate{ - ServiceName: "foo", - Attributes: []string{ - "traefik.frontend.rule=PathPrefix:{{getTag \"contextPath\" .Attributes \"/\"}}", - "contextPath=/bar", - }, - }, - expected: "PathPrefix:/bar", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - p := &Provider{ - Domain: "localhost", - Prefix: "traefik", - FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", - frontEndRuleTemplate: template.New("consul catalog frontend rule"), - } - p.setupFrontEndRuleTemplate() - - actual := p.getFrontendRule(test.service) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetBackendAddress(t *testing.T) { - testCases := []struct { - desc string - node *api.ServiceEntry - expected string - }{ - { - desc: "Should return the address of the service", - node: &api.ServiceEntry{ - Node: &api.Node{ - Address: "10.1.0.1", - }, - Service: &api.AgentService{ - Address: "10.2.0.1", - }, - }, - expected: "10.2.0.1", - }, - { - desc: "Should return the address of the node", - node: &api.ServiceEntry{ - Node: &api.Node{ - Address: "10.1.0.1", - }, - Service: &api.AgentService{ - Address: "", - }, - }, - expected: "10.1.0.1", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getBackendAddress(test.node) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestProviderGetServerName(t *testing.T) { - testCases := []struct { - desc string - node *api.ServiceEntry - expected string - }{ - { - desc: "Should create backend name without tags", - node: &api.ServiceEntry{ - Service: &api.AgentService{ - Service: "api", - Address: "10.0.0.1", - Port: 80, - Tags: []string{}, - }, - }, - expected: "api-0-eUSiqD6uNvvh6zxsY-OeRi8ZbaE", - }, - { - desc: "Should create backend name with multiple tags", - node: &api.ServiceEntry{ - Service: &api.AgentService{ - Service: "api", - Address: "10.0.0.1", - Port: 80, - Tags: []string{"traefik.weight=42", "traefik.enable=true"}, - }, - }, - expected: "api-1-eJ8MR2JxjXyZgs1bhurVa0-9OI8", - }, - { - desc: "Should create backend name with one tag", - node: &api.ServiceEntry{ - Service: &api.AgentService{ - Service: "api", - Address: "10.0.0.1", - Port: 80, - Tags: []string{"a funny looking tag"}, - }, - }, - expected: "api-2-lMCDCsG7sh0SCXOHo4oBOQB-9D4", - }, - } - - for i, test := range testCases { - test := test - i := i - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getServerName(test.node, i) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestHasStickinessLabel(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected bool - }{ - { - desc: "label missing", - tags: []string{}, - expected: false, - }, - { - desc: "stickiness=true", - tags: []string{ - label.TraefikBackendLoadBalancerStickiness + "=true", - }, - expected: true, - }, - { - desc: "stickiness=false", - tags: []string{ - label.TraefikBackendLoadBalancerStickiness + "=false", - }, - expected: false, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := p.hasStickinessLabel(test.tags) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestProviderGetCircuitBreaker(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.CircuitBreaker - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return a struct when has tag", - tags: []string{label.Prefix + label.SuffixBackendCircuitBreaker + "=foo"}, - expected: &types.CircuitBreaker{ - Expression: "foo", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getCircuitBreaker(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetLoadBalancer(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.LoadBalancer - }{ - { - desc: "should return a default struct when no tags", - tags: []string{}, - expected: &types.LoadBalancer{ - Method: "wrr", - }, - }, - { - desc: "should return a struct when has Method tag", - tags: []string{label.Prefix + "backend.loadbalancer" + "=drr"}, - expected: &types.LoadBalancer{ - Method: "drr", - }, - }, - { - desc: "should return a struct when has Sticky tag", - tags: []string{ - label.Prefix + label.SuffixBackendLoadBalancerSticky + "=true", - }, - expected: &types.LoadBalancer{ - Method: "wrr", - Sticky: true, - }, - }, - { - desc: "should skip Sticky when Sticky tag has invalid value", - tags: []string{ - label.Prefix + label.SuffixBackendLoadBalancerSticky + "=goo", - }, - expected: &types.LoadBalancer{ - Method: "wrr", - }, - }, - { - desc: "should return a struct when has Stickiness tag", - tags: []string{ - label.Prefix + label.SuffixBackendLoadBalancerStickiness + "=true", - }, - expected: &types.LoadBalancer{ - Method: "wrr", - Stickiness: &types.Stickiness{}, - }, - }, - { - desc: "should skip Stickiness when Stickiness tag has invalid value", - tags: []string{ - label.Prefix + label.SuffixBackendLoadBalancerStickiness + "=goo", - }, - expected: &types.LoadBalancer{ - Method: "wrr", - }, - }, - { - desc: "should return a struct when has Stickiness tag", - tags: []string{ - label.Prefix + label.SuffixBackendLoadBalancerStickiness + "=true", - label.Prefix + label.SuffixBackendLoadBalancerStickinessCookieName + "=bar", - }, - expected: &types.LoadBalancer{ - Method: "wrr", - Stickiness: &types.Stickiness{ - CookieName: "bar", - }, - }, - }, - { - desc: "should skip Stickiness when Stickiness tag has false as value", - tags: []string{ - label.Prefix + label.SuffixBackendLoadBalancerStickiness + "=false", - label.Prefix + label.SuffixBackendLoadBalancerStickinessCookieName + "=bar", - }, - expected: &types.LoadBalancer{ - Method: "wrr", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getLoadBalancer(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetMaxConn(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.MaxConn - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return a struct when Amount & ExtractorFunc tags", - tags: []string{ - label.Prefix + label.SuffixBackendMaxConnAmount + "=10", - label.Prefix + label.SuffixBackendMaxConnExtractorFunc + "=bar", - }, - expected: &types.MaxConn{ - ExtractorFunc: "bar", - Amount: 10, - }, - }, - { - desc: "should return nil when Amount tags is missing", - tags: []string{ - label.Prefix + label.SuffixBackendMaxConnExtractorFunc + "=bar", - }, - expected: nil, - }, - { - desc: "should return nil when ExtractorFunc tags is empty", - tags: []string{ - label.Prefix + label.SuffixBackendMaxConnAmount + "=10", - label.Prefix + label.SuffixBackendMaxConnExtractorFunc + "=", - }, - expected: nil, - }, - { - desc: "should return a struct when ExtractorFunc tags is missing", - tags: []string{ - label.Prefix + label.SuffixBackendMaxConnAmount + "=10", - }, - expected: &types.MaxConn{ - ExtractorFunc: label.DefaultBackendMaxconnExtractorFunc, - Amount: 10, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getMaxConn(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetHealthCheck(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.HealthCheck - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return nil when Path tag is missing", - tags: []string{ - label.TraefikBackendHealthCheckPort + "=80", - label.TraefikBackendHealthCheckInterval + "=7", - }, - expected: nil, - }, - { - desc: "should return a struct when has tags", - tags: []string{ - label.TraefikBackendHealthCheckPath + "=/health", - label.TraefikBackendHealthCheckPort + "=80", - label.TraefikBackendHealthCheckInterval + "=7", - }, - expected: &types.HealthCheck{ - Path: "/health", - Port: 80, - Interval: "7", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getHealthCheck(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetBuffering(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.Buffering - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return a struct when has proper tags", - tags: []string{ - label.TraefikBackendBufferingMaxResponseBodyBytes + "=10485760", - label.TraefikBackendBufferingMemResponseBodyBytes + "=2097152", - label.TraefikBackendBufferingMaxRequestBodyBytes + "=10485760", - label.TraefikBackendBufferingMemRequestBodyBytes + "=2097152", - label.TraefikBackendBufferingRetryExpression + "=IsNetworkError() && Attempts() <= 2", - }, - expected: &types.Buffering{ - MaxResponseBodyBytes: 10485760, - MemResponseBodyBytes: 2097152, - MaxRequestBodyBytes: 10485760, - MemRequestBodyBytes: 2097152, - RetryExpression: "IsNetworkError() && Attempts() <= 2", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getBuffering(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderWhiteList(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.WhiteList - }{ - { - desc: "should return nil when no white list labels", - expected: nil, - }, - { - desc: "should return a struct when only range", - tags: []string{ - label.TraefikFrontendWhiteListSourceRange + "=10.10.10.10", - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: false, - }, - }, - { - desc: "should return a struct when range and UseXForwardedFor", - tags: []string{ - label.TraefikFrontendWhiteListSourceRange + "=10.10.10.10", - label.TraefikFrontendWhiteListUseXForwardedFor + "=true", - }, - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: true, - }, - }, - { - desc: "should return nil when only UseXForwardedFor", - tags: []string{ - label.TraefikFrontendWhiteListUseXForwardedFor + "=true", - }, - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := p.getWhiteList(test.tags) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestProviderGetRedirect(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.Redirect - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should use only entry point tag when mix regex redirect and entry point redirect", - tags: []string{ - label.TraefikFrontendRedirectEntryPoint + "=https", - label.TraefikFrontendRedirectRegex + "=(.*)", - label.TraefikFrontendRedirectReplacement + "=$1", - }, - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect tag", - tags: []string{ - label.TraefikFrontendRedirectEntryPoint + "=https", - }, - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect tags (permanent)", - tags: []string{ - label.TraefikFrontendRedirectEntryPoint + "=https", - label.TraefikFrontendRedirectPermanent + "=true", - }, - expected: &types.Redirect{ - EntryPoint: "https", - Permanent: true, - }, - }, - { - desc: "should return a struct when regex redirect tags", - tags: []string{ - label.TraefikFrontendRedirectRegex + "=(.*)", - label.TraefikFrontendRedirectReplacement + "=$1", - }, - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - }, - }, - { - desc: "should return a struct when regex redirect tags (permanent)", - tags: []string{ - label.TraefikFrontendRedirectRegex + "=(.*)", - label.TraefikFrontendRedirectReplacement + "=$1", - label.TraefikFrontendRedirectPermanent + "=true", - }, - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - Permanent: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getRedirect(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetErrorPages(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected map[string]*types.ErrorPage - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return a map when tags are present", - tags: []string{ - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus + "=404", - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend + "=foo_backend", - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery + "=foo_query", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus + "=500,600", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend + "=bar_backend", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery + "=bar_query", - }, - expected: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - Query: "foo_query", - Backend: "foo_backend", - }, - "bar": { - Status: []string{"500", "600"}, - Query: "bar_query", - Backend: "bar_backend", - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getErrorPages(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetRateLimit(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.RateLimit - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return a map when tags are present", - tags: []string{ - label.TraefikFrontendRateLimitExtractorFunc + "=client.ip", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod + "=6", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage + "=12", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst + "=18", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod + "=3", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage + "=6", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst + "=9", - }, - expected: &types.RateLimit{ - ExtractorFunc: "client.ip", - RateSet: map[string]*types.Rate{ - "foo": { - Period: flaeg.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - "bar": { - Period: flaeg.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getRateLimit(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestProviderGetHeaders(t *testing.T) { - p := &Provider{ - Prefix: "traefik", - } - - testCases := []struct { - desc string - tags []string - expected *types.Headers - }{ - { - desc: "should return nil when no tags", - tags: []string{}, - expected: nil, - }, - { - desc: "should return a struct when has tags", - tags: []string{ - label.TraefikFrontendRequestHeaders + "=Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendResponseHeaders + "=Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendSSLProxyHeaders + "=Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", - label.TraefikFrontendAllowedHosts + "=foo,bar,bor", - label.TraefikFrontendHostsProxyHeaders + "=foo,bar,bor", - label.TraefikFrontendSSLHost + "=foo", - label.TraefikFrontendCustomFrameOptionsValue + "=foo", - label.TraefikFrontendContentSecurityPolicy + "=foo", - label.TraefikFrontendPublicKey + "=foo", - label.TraefikFrontendReferrerPolicy + "=foo", - label.TraefikFrontendCustomBrowserXSSValue + "=foo", - label.TraefikFrontendSTSSeconds + "=666", - label.TraefikFrontendSSLRedirect + "=true", - label.TraefikFrontendSSLTemporaryRedirect + "=true", - label.TraefikFrontendSTSIncludeSubdomains + "=true", - label.TraefikFrontendSTSPreload + "=true", - label.TraefikFrontendForceSTSHeader + "=true", - label.TraefikFrontendFrameDeny + "=true", - label.TraefikFrontendContentTypeNosniff + "=true", - label.TraefikFrontendBrowserXSSFilter + "=true", - label.TraefikFrontendIsDevelopment + "=true", - }, - expected: &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", - }, - SSLProxyHeaders: 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"}, - SSLHost: "foo", - CustomFrameOptionsValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - CustomBrowserXSSValue: "foo", - STSSeconds: 666, - SSLRedirect: true, - SSLTemporaryRedirect: true, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - ContentTypeNosniff: true, - BrowserXSSFilter: true, - IsDevelopment: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - result := p.getHeaders(test.tags) - - assert.Equal(t, test.expected, result) - }) - } -} diff --git a/provider/consulcatalog/convert_types.go b/provider/consulcatalog/convert_types.go new file mode 100644 index 000000000..edb320087 --- /dev/null +++ b/provider/consulcatalog/convert_types.go @@ -0,0 +1,29 @@ +package consulcatalog + +import ( + "strings" + + "github.com/containous/traefik/provider/label" +) + +func tagsToNeutralLabels(tags []string, prefix string) map[string]string { + var labels map[string]string + + for _, tag := range tags { + if strings.HasPrefix(tag, prefix) { + + parts := strings.SplitN(tag, "=", 2) + if len(parts) == 2 { + if labels == nil { + labels = make(map[string]string) + } + + // replace custom prefix by the generic prefix + key := label.Prefix + strings.TrimPrefix(parts[0], prefix+".") + labels[key] = parts[1] + } + } + } + + return labels +} diff --git a/provider/consulcatalog/convert_types_test.go b/provider/consulcatalog/convert_types_test.go new file mode 100644 index 000000000..b0c24b02d --- /dev/null +++ b/provider/consulcatalog/convert_types_test.go @@ -0,0 +1,64 @@ +package consulcatalog + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTagsToNeutralLabels(t *testing.T) { + testCases := []struct { + desc string + tags []string + prefix string + expected map[string]string + }{ + { + desc: "without tags", + expected: nil, + }, + { + desc: "with a prefix", + prefix: "test", + tags: []string{ + "test.aaa=01", + "test.bbb=02", + "ccc=03", + "test.ddd=04=to", + }, + expected: map[string]string{ + "traefik.aaa": "01", + "traefik.bbb": "02", + "traefik.ddd": "04=to", + }, + }, + + { + desc: "with an empty prefix", + prefix: "", + tags: []string{ + "test.aaa=01", + "test.bbb=02", + "ccc=03", + "test.ddd=04=to", + }, + expected: map[string]string{ + "traefik.test.aaa": "01", + "traefik.test.bbb": "02", + "traefik.ccc": "03", + "traefik.test.ddd": "04=to", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + labels := tagsToNeutralLabels(test.tags, test.prefix) + + assert.Equal(t, test.expected, labels) + }) + } +} diff --git a/provider/consulcatalog/deprecated_config.go b/provider/consulcatalog/deprecated_config.go new file mode 100644 index 000000000..181674dc1 --- /dev/null +++ b/provider/consulcatalog/deprecated_config.go @@ -0,0 +1,250 @@ +package consulcatalog + +import ( + "bytes" + "sort" + "strconv" + "strings" + "text/template" + + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/hashicorp/consul/api" +) + +// Deprecated +func (p *Provider) buildConfigurationV1(catalog []catalogUpdate) *types.Configuration { + var FuncMap = template.FuncMap{ + "getAttribute": p.getAttribute, + "getTag": getTag, + "hasTag": hasTag, + + // Backend functions + "getBackend": getNodeBackendName, + "getServiceBackendName": getServiceBackendName, + "getBackendAddress": getBackendAddress, + "getBackendName": getServerName, + "hasMaxconnAttributes": p.hasMaxConnAttributesV1, + "getSticky": p.getStickyV1, + "hasStickinessLabel": p.hasStickinessLabelV1, + "getStickinessCookieName": p.getStickinessCookieNameV1, + "getWeight": p.getWeight, + "getProtocol": p.getFuncStringAttribute(label.SuffixProtocol, label.DefaultProtocol), + + // Frontend functions + "getFrontendRule": p.getFrontendRuleV1, + "getBasicAuth": p.getFuncSliceAttribute(label.SuffixFrontendAuthBasic), + "getEntryPoints": getEntryPointsV1, + "getPriority": p.getFuncIntAttribute(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": p.getFuncBoolAttribute(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": p.getFuncBoolAttribute(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), + } + + var allNodes []*api.ServiceEntry + var services []*serviceUpdate + for _, info := range catalog { + if len(info.Nodes) > 0 { + services = append(services, info.Service) + allNodes = append(allNodes, info.Nodes...) + } + } + // Ensure a stable ordering of nodes so that identical configurations may be detected + sort.Sort(nodeSorter(allNodes)) + + templateObjects := struct { + Services []*serviceUpdate + Nodes []*api.ServiceEntry + }{ + Services: services, + Nodes: allNodes, + } + + configuration, err := p.GetConfiguration("templates/consul_catalog-v1.tmpl", FuncMap, templateObjects) + if err != nil { + log.WithError(err).Error("Failed to create config") + } + + return configuration +} + +// Specific functions + +// Deprecated +func (p *Provider) getFrontendRuleV1(service serviceUpdate) string { + customFrontendRule := p.getAttribute(label.SuffixFrontendRule, service.Attributes, "") + if customFrontendRule == "" { + customFrontendRule = p.FrontEndRule + } + + tmpl := p.frontEndRuleTemplate + tmpl, err := tmpl.Parse(customFrontendRule) + if err != nil { + log.Errorf("Failed to parse Consul Catalog custom frontend rule: %v", err) + return "" + } + + templateObjects := struct { + ServiceName string + Domain string + Attributes []string + }{ + ServiceName: service.ServiceName, + Domain: p.Domain, + Attributes: service.Attributes, + } + + var buffer bytes.Buffer + err = tmpl.Execute(&buffer, templateObjects) + if err != nil { + log.Errorf("Failed to execute Consul Catalog custom frontend rule template: %v", err) + return "" + } + + return buffer.String() +} + +// Deprecated +func (p *Provider) hasMaxConnAttributesV1(attributes []string) bool { + amount := p.getAttribute(label.SuffixBackendMaxConnAmount, attributes, "") + extractorFunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, attributes, "") + return amount != "" && extractorFunc != "" +} + +// Deprecated +func getEntryPointsV1(list string) []string { + return strings.Split(list, ",") +} + +// TODO: Deprecated +// replaced by Stickiness +// Deprecated +func (p *Provider) getStickyV1(tags []string) string { + stickyTag := p.getAttribute(label.SuffixBackendLoadBalancerSticky, tags, "") + if len(stickyTag) > 0 { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + } else { + stickyTag = "false" + } + return stickyTag +} + +// Deprecated +func (p *Provider) hasStickinessLabelV1(tags []string) bool { + stickinessTag := p.getAttribute(label.SuffixBackendLoadBalancerStickiness, tags, "") + return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true") +} + +// Deprecated +func (p *Provider) getStickinessCookieNameV1(tags []string) string { + return p.getAttribute(label.SuffixBackendLoadBalancerStickinessCookieName, tags, "") +} + +// Base functions + +// Deprecated +func (p *Provider) getFuncStringAttribute(name string, defaultValue string) func(tags []string) string { + return func(tags []string) string { + return p.getAttribute(name, tags, defaultValue) + } +} + +// Deprecated +func (p *Provider) getFuncSliceAttribute(name string) func(tags []string) []string { + return func(tags []string) []string { + return p.getSliceAttribute(name, tags) + } +} + +// Deprecated +func (p *Provider) getMapAttribute(name string, tags []string) map[string]string { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return nil + } + + return label.ParseMapValue(p.getPrefixedName(name), rawValue) +} + +// Deprecated +func (p *Provider) getFuncIntAttribute(name string, defaultValue int) func(tags []string) int { + return func(tags []string) int { + return p.getIntAttribute(name, tags, defaultValue) + } +} + +func (p *Provider) getFuncBoolAttribute(name string, defaultValue bool) func(tags []string) bool { + return func(tags []string) bool { + return p.getBoolAttribute(name, tags, defaultValue) + } +} + +// Deprecated +func (p *Provider) getFuncHasAttributePrefix(name string) func(tags []string) bool { + return func(tags []string) bool { + return p.hasAttributePrefix(name, tags) + } +} + +// Deprecated +func (p *Provider) getInt64Attribute(name string, tags []string, defaultValue int64) int64 { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return defaultValue + } + + value, err := strconv.ParseInt(rawValue, 10, 64) + if err != nil { + log.Errorf("Invalid value for %s: %s", name, rawValue) + return defaultValue + } + return value +} + +// Deprecated +func (p *Provider) getIntAttribute(name string, tags []string, defaultValue int) int { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return defaultValue + } + + value, err := strconv.Atoi(rawValue) + if err != nil { + log.Errorf("Invalid value for %s: %s", name, rawValue) + return defaultValue + } + return value +} + +// Deprecated +func (p *Provider) getSliceAttribute(name string, tags []string) []string { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return nil + } + return label.SplitAndTrimString(rawValue, ",") +} + +// Deprecated +func (p *Provider) getBoolAttribute(name string, tags []string, defaultValue bool) bool { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return defaultValue + } + + value, err := strconv.ParseBool(rawValue) + if err != nil { + log.Errorf("Invalid value for %s: %s", name, rawValue) + return defaultValue + } + return value +} + +func (p *Provider) hasAttributePrefix(name string, tags []string) bool { + return hasTagPrefix(p.getPrefixedName(name), tags) +} diff --git a/provider/consulcatalog/deprecated_config_test.go b/provider/consulcatalog/deprecated_config_test.go new file mode 100644 index 000000000..a5415a425 --- /dev/null +++ b/provider/consulcatalog/deprecated_config_test.go @@ -0,0 +1,444 @@ +package consulcatalog + +import ( + "testing" + "text/template" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/assert" +) + +func TestProviderBuildConfigurationV1(t *testing.T) { + p := &Provider{ + Domain: "localhost", + Prefix: "traefik", + ExposedByDefault: false, + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), + } + + testCases := []struct { + desc string + nodes []catalogUpdate + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "Should build config of nothing", + nodes: []catalogUpdate{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "Should build config with no frontend and backend", + nodes: []catalogUpdate{ + { + Service: &serviceUpdate{ + ServiceName: "test", + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "Should build config who contains one frontend and one backend", + nodes: []catalogUpdate{ + { + Service: &serviceUpdate{ + ServiceName: "test", + Attributes: []string{ + "random.foo=bar", + label.TraefikBackendLoadBalancer + "=drr", + label.TraefikBackendCircuitBreaker + "=NetworkErrorRatio() > 0.5", + label.TraefikBackendMaxConnAmount + "=1000", + label.TraefikBackendMaxConnExtractorFunc + "=client.ip", + label.TraefikFrontendAuthBasic + "=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + }, + Nodes: []*api.ServiceEntry{ + { + Service: &api.AgentService{ + Service: "test", + Address: "127.0.0.1", + Port: 80, + Tags: []string{ + "random.foo=bar", + label.Prefix + "backend.weight=42", + label.TraefikFrontendPassHostHeader + "=true", + label.TraefikProtocol + "=https", + }, + }, + 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:test.localhost", + }, + }, + BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test": { + Servers: map[string]types.Server{ + "test-0-us4-27hAOu2ARV7nNrmv6GoKlcA": { + URL: "https://127.0.0.1:80", + Weight: 42, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + }, + MaxConn: &types.MaxConn{ + Amount: 1000, + ExtractorFunc: "client.ip", + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actualConfig := p.buildConfigurationV1(test.nodes) + assert.NotNil(t, actualConfig) + assert.Equal(t, test.expectedBackends, actualConfig.Backends) + assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + +func TestProviderGetIntAttributeV1(t *testing.T) { + p := &Provider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + name string + tags []string + defaultValue int + expected int + }{ + { + desc: "should return default value when empty name", + name: "", + tags: []string{"traefik.foo=10"}, + defaultValue: 666, + expected: 666, + }, + { + desc: "should return default value when empty tags", + name: "traefik.foo", + tags: nil, + expected: 0, + }, + { + desc: "should return default value when value is not a int", + name: "foo", + tags: []string{"traefik.foo=bar"}, + expected: 0, + }, + { + desc: "should return a value when tag exist", + name: "foo", + tags: []string{"traefik.foo=10"}, + expected: 10, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getIntAttribute(test.name, test.tags, test.defaultValue) + + assert.Equal(t, test.expected, result) + }) + } +} + +func TestProviderGetInt64AttributeV1(t *testing.T) { + p := &Provider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + name string + tags []string + defaultValue int64 + expected int64 + }{ + { + desc: "should return default value when empty name", + name: "", + tags: []string{"traefik.foo=10"}, + defaultValue: 666, + expected: 666, + }, + { + desc: "should return default value when empty tags", + name: "traefik.foo", + tags: nil, + expected: 0, + }, + { + desc: "should return default value when value is not a int", + name: "foo", + tags: []string{"traefik.foo=bar"}, + expected: 0, + }, + { + desc: "should return a value when tag exist", + name: "foo", + tags: []string{"traefik.foo=10"}, + expected: 10, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getInt64Attribute(test.name, test.tags, test.defaultValue) + + assert.Equal(t, test.expected, result) + }) + } +} + +func TestProviderGetBoolAttributeV1(t *testing.T) { + p := &Provider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + name string + tags []string + defaultValue bool + expected bool + }{ + { + desc: "should return default value when empty name", + name: "", + tags: []string{"traefik.foo=true"}, + defaultValue: true, + expected: true, + }, + { + desc: "should return default value when empty tags", + name: "traefik.foo", + tags: nil, + expected: false, + }, + { + desc: "should return default value when value is not a bool", + name: "foo", + tags: []string{"traefik.foo=bar"}, + expected: false, + }, + { + desc: "should return a value when tag exist", + name: "foo", + tags: []string{"traefik.foo=true"}, + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getBoolAttribute(test.name, test.tags, test.defaultValue) + + assert.Equal(t, test.expected, result) + }) + } +} + +func TestProviderGetSliceAttributeV1(t *testing.T) { + p := &Provider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + name string + tags []string + expected []string + }{ + { + desc: "should return nil when empty name", + name: "", + tags: []string{"traefik.foo=bar,bor,bir"}, + expected: nil, + }, + { + desc: "should return nil when empty tags", + name: "foo", + tags: nil, + expected: nil, + }, + { + desc: "should return nil when tag doesn't have value", + name: "", + tags: []string{"traefik.foo="}, + expected: nil, + }, + { + desc: "should return a slice when tag contains comma separated values", + name: "foo", + tags: []string{"traefik.foo=bar,bor,bir"}, + expected: []string{"bar", "bor", "bir"}, + }, + { + desc: "should return a slice when tag contains one value", + name: "foo", + tags: []string{"traefik.foo=bar"}, + expected: []string{"bar"}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getSliceAttribute(test.name, test.tags) + + assert.Equal(t, test.expected, result) + }) + } +} + +func TestProviderGetFrontendRuleV1(t *testing.T) { + testCases := []struct { + desc string + service serviceUpdate + expected string + }{ + { + desc: "Should return default host foo.localhost", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{}, + }, + expected: "Host:foo.localhost", + }, + { + desc: "Should return host *.example.com", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=Host:*.example.com", + }, + }, + expected: "Host:*.example.com", + }, + { + desc: "Should return host foo.example.com", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=Host:{{.ServiceName}}.example.com", + }, + }, + expected: "Host:foo.example.com", + }, + { + desc: "Should return path prefix /bar", + service: serviceUpdate{ + ServiceName: "foo", + Attributes: []string{ + "traefik.frontend.rule=PathPrefix:{{getTag \"contextPath\" .Attributes \"/\"}}", + "contextPath=/bar", + }, + }, + expected: "PathPrefix:/bar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + Domain: "localhost", + Prefix: "traefik", + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), + } + p.setupFrontEndRuleTemplate() + + actual := p.getFrontendRuleV1(test.service) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestHasStickinessLabelV1(t *testing.T) { + p := &Provider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + tags []string + expected bool + }{ + { + desc: "label missing", + tags: []string{}, + expected: false, + }, + { + desc: "stickiness=true", + tags: []string{ + label.TraefikBackendLoadBalancerStickiness + "=true", + }, + expected: true, + }, + { + desc: "stickiness=false", + tags: []string{ + label.TraefikBackendLoadBalancerStickiness + "=false", + }, + expected: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := p.hasStickinessLabelV1(test.tags) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/templates/consul_catalog-v1.tmpl b/templates/consul_catalog-v1.tmpl new file mode 100644 index 000000000..cb99198b4 --- /dev/null +++ b/templates/consul_catalog-v1.tmpl @@ -0,0 +1,56 @@ +[backends] +{{range $index, $node := .Nodes }} + [backends."backend-{{ getBackend $node }}".servers."{{ getBackendName $node $index }}"] + url = "{{ getAttribute "protocol" $node.Service.Tags "http" }}://{{ getBackendAddress $node }}:{{ $node.Service.Port }}" + {{ $weight := getAttribute "backend.weight" $node.Service.Tags "0" }} + {{with $weight }} + weight = {{ $weight }} + {{end}} +{{end}} + +{{range .Services }} + {{ $service := .ServiceName }} + + {{ $circuitBreaker := getAttribute "backend.circuitbreaker" .Attributes "" }} + {{with $circuitBreaker }} + [backends."backend-{{ $service }}".circuitbreaker] + expression = "{{ $circuitBreaker }}" + {{end}} + + [backends."backend-{{ $service }}".loadbalancer] + method = "{{ getAttribute "backend.loadbalancer" .Attributes "wrr" }}" + sticky = {{ getSticky .Attributes }} + {{if hasStickinessLabel .Attributes }} + [backends."backend-{{ $service }}".loadbalancer.stickiness] + cookieName = "{{ getStickinessCookieName .Attributes }}" + {{end}} + + {{if hasMaxconnAttributes .Attributes }} + [backends."backend-{{ $service }}".maxconn] + amount = {{ getAttribute "backend.maxconn.amount" .Attributes "" }} + extractorfunc = "{{ getAttribute "backend.maxconn.extractorfunc" .Attributes "" }}" + {{end}} + +{{end}} + +[frontends] +{{range .Services }} + [frontends."frontend-{{ .ServiceName }}"] + backend = "backend-{{ .ServiceName }}" + passHostHeader = {{ getAttribute "frontend.passHostHeader" .Attributes "true" }} + priority = {{ getAttribute "frontend.priority" .Attributes "0" }} + + {{ $entryPoints := getAttribute "frontend.entrypoints" .Attributes "" }} + {{with $entryPoints }} + entrypoints = [{{range getEntryPoints $entryPoints }} + "{{ . }}", + {{end}}] + {{end}} + + basicAuth = [{{range getBasicAuth .Attributes }} + "{{ . }}", + {{end}}] + + [frontends."frontend-{{ .ServiceName }}".routes."route-host-{{ .ServiceName }}"] + rule = "{{ getFrontendRule . }}" +{{end}} diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index 7638965f7..cea3bd5c6 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -2,13 +2,13 @@ {{range $service := .Services}} {{ $backendName := getServiceBackendName $service }} - {{ $circuitBreaker := getCircuitBreaker $service.Attributes }} + {{ $circuitBreaker := getCircuitBreaker $service.TraefikLabels }} {{if $circuitBreaker }} [backends."backend-{{ $backendName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $service.Attributes }} + {{ $loadBalancer := getLoadBalancer $service.TraefikLabels }} {{if $loadBalancer }} [backends."backend-{{ $backendName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -19,14 +19,14 @@ {{end}} {{end}} - {{ $maxConn := getMaxConn $service.Attributes }} + {{ $maxConn := getMaxConn $service.TraefikLabels }} {{if $maxConn }} [backends."backend-{{ $backendName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $service.Attributes }} + {{ $healthCheck := getHealthCheck $service.TraefikLabels }} {{if $healthCheck }} [backends."backend-{{ $backendName }}".healthCheck] path = "{{ $healthCheck.Path }}" @@ -34,7 +34,7 @@ interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $service.Attributes }} + {{ $buffering := getBuffering $service.TraefikLabels }} {{if $buffering }} [backends."backend-{{ $backendName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -46,10 +46,10 @@ {{end}} {{range $index, $node := .Nodes}} - + {{ $server := getServer $node }} [backends."backend-{{ getNodeBackendName $node }}".servers."{{ getServerName $node $index }}"] - url = "{{ getProtocol $node.Service.Tags }}://{{ getBackendAddress $node }}:{{ $node.Service.Port }}" - weight = {{ getWeight $node.Service.Tags }} + url = "{{ $server.URL }}" + weight = {{ $server.Weight }} {{end}} @@ -58,19 +58,19 @@ [frontends."frontend-{{ $service.ServiceName }}"] backend = "backend-{{ getServiceBackendName $service }}" - priority = {{ getPriority $service.Attributes }} - passHostHeader = {{ getPassHostHeader $service.Attributes }} - passTLSCert = {{ getPassTLSCert $service.Attributes }} + priority = {{ getPriority $service.TraefikLabels }} + passHostHeader = {{ getPassHostHeader $service.TraefikLabels }} + passTLSCert = {{ getPassTLSCert $service.TraefikLabels }} - entryPoints = [{{range getFrontEndEntryPoints $service.Attributes }} + entryPoints = [{{range getFrontEndEntryPoints $service.TraefikLabels }} "{{.}}", {{end}}] - basicAuth = [{{range getBasicAuth $service.Attributes }} + basicAuth = [{{range getBasicAuth $service.TraefikLabels }} "{{.}}", {{end}}] - {{ $whitelist := getWhiteList $service.Attributes }} + {{ $whitelist := getWhiteList $service.TraefikLabels }} {{if $whitelist }} [frontends."frontend-{{ $service.ServiceName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} @@ -79,7 +79,7 @@ useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $service.Attributes }} + {{ $redirect := getRedirect $service.TraefikLabels }} {{if $redirect }} [frontends."frontend-{{ $service.ServiceName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -88,9 +88,10 @@ permanent = {{ $redirect.Permanent }} {{end}} - {{if hasErrorPages $service.Attributes }} + {{ $errorPages := getErrorPages $service.TraefikLabels }} + {{if $errorPages }} [frontends."frontend-{{ $service.ServiceName }}".errors] - {{range $pageName, $page := getErrorPages $service.Attributes }} + {{range $pageName, $page := $errorPages }} [frontends."frontend-{{ $service.ServiceName }}".errors."{{ $pageName }}"] status = [{{range $page.Status }} "{{.}}", @@ -100,22 +101,20 @@ {{end}} {{end}} - {{if hasRateLimit $service.Attributes }} - {{ $rateLimit := getRateLimit $service.Attributes }} + {{ $rateLimit := getRateLimit $service.TraefikLabels }} + {{if $rateLimit }} [frontends."frontend-{{ $service.ServiceName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" - [frontends."frontend-{{ $service.ServiceName }}".rateLimit.rateSet] - {{range $limitName, $limit := $rateLimit.RateSet }} + {{ range $limitName, $limit := $rateLimit.RateSet }} [frontends."frontend-{{ $service.ServiceName }}".rateLimit.rateSet."{{ $limitName }}"] period = "{{ $limit.Period }}" average = {{ $limit.Average }} burst = {{ $limit.Burst }} {{end}} - {{end}} - {{ $headers := getHeaders $service.Attributes }} + {{ $headers := getHeaders $service.TraefikLabels }} {{if $headers }} [frontends."frontend-{{ $service.ServiceName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} From 4b93d040b34e26b372eb09d8faa66239b4102ea8 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 29 Mar 2018 00:01:24 +0200 Subject: [PATCH 16/21] refactor: Mesos labels. --- configuration/configuration.go | 9 + provider/ecs/config_test.go | 4 +- provider/mesos/config.go | 468 +++----------- provider/mesos/config_root.go | 13 + provider/mesos/config_test.go | 751 +---------------------- provider/mesos/deprecated_config.go | 327 ++++++++++ provider/mesos/deprecated_config_test.go | 432 +++++++++++++ provider/mesos/mesos_helper_test.go | 8 + provider/mesos/mesos_test.go | 37 ++ templates/mesos-v1.tmpl | 27 + templates/mesos.tmpl | 30 +- 11 files changed, 969 insertions(+), 1137 deletions(-) create mode 100644 provider/mesos/config_root.go create mode 100644 provider/mesos/deprecated_config.go create mode 100644 provider/mesos/deprecated_config_test.go create mode 100644 provider/mesos/mesos_test.go create mode 100644 templates/mesos-v1.tmpl diff --git a/configuration/configuration.go b/configuration/configuration.go index 10c3557d9..c93ecb1b7 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -230,6 +230,15 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) { } } + if gc.Mesos != nil { + if len(gc.Mesos.Filename) != 0 && gc.Mesos.TemplateVersion != 2 { + log.Warn("Template version 1 is deprecated, please use version 2, see TemplateVersion.") + gc.Mesos.TemplateVersion = 1 + } else { + gc.Mesos.TemplateVersion = 2 + } + } + if gc.Eureka != nil { if gc.Eureka.Delay != 0 { log.Warn("Delay has been deprecated -- please use RefreshSeconds") diff --git a/provider/ecs/config_test.go b/provider/ecs/config_test.go index d9af4de67..95c745c82 100644 --- a/provider/ecs/config_test.go +++ b/provider/ecs/config_test.go @@ -351,11 +351,11 @@ func TestBuildConfiguration(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - provider := &Provider{} + p := &Provider{} services := fakeLoadTraefikLabels(test.services) - got, err := provider.buildConfiguration(services) + got, err := p.buildConfiguration(services) assert.Equal(t, test.err, err) // , err.Error() assert.Equal(t, test.expected, got, test.desc) }) diff --git a/provider/mesos/config.go b/provider/mesos/config.go index 43795edbd..34f484b8d 100644 --- a/provider/mesos/config.go +++ b/provider/mesos/config.go @@ -7,7 +7,6 @@ import ( "strings" "text/template" - "github.com/BurntSushi/ty/fun" "github.com/containous/traefik/log" "github.com/containous/traefik/provider" "github.com/containous/traefik/provider/label" @@ -15,82 +14,63 @@ import ( "github.com/mesosphere/mesos-dns/records/state" ) -func (p *Provider) buildConfiguration(tasks []state.Task) *types.Configuration { +type taskData struct { + state.Task + TraefikLabels map[string]string +} + +func (p *Provider) buildConfigurationV2(tasks []state.Task) *types.Configuration { var mesosFuncMap = template.FuncMap{ - "getDomain": getFuncStringValue(label.TraefikDomain, p.Domain), + "getDomain": label.GetFuncString(label.TraefikDomain, p.Domain), "getID": getID, // Backend functions "getBackendName": getBackendName, - "getCircuitBreaker": getCircuitBreaker, - "getLoadBalancer": getLoadBalancer, - "getMaxConn": getMaxConn, - "getHealthCheck": getHealthCheck, - "getBuffering": getBuffering, + "getCircuitBreaker": label.GetCircuitBreaker, + "getLoadBalancer": label.GetLoadBalancer, + "getMaxConn": label.GetMaxConn, + "getHealthCheck": label.GetHealthCheck, + "getBuffering": label.GetBuffering, "getServers": p.getServers, "getHost": p.getHost, "getServerPort": p.getServerPort, - // TODO Deprecated [breaking] - "getProtocol": getFuncApplicationStringValue(label.TraefikProtocol, label.DefaultProtocol), - // TODO Deprecated [breaking] - "getWeight": getFuncApplicationStringValue(label.TraefikWeight, label.DefaultWeight), - // TODO Deprecated [breaking] replaced by getBackendName - "getBackend": getBackend, - // TODO Deprecated [breaking] - "getPort": p.getPort, - // Frontend functions "getFrontEndName": getFrontendName, - "getEntryPoints": getFuncSliceStringValue(label.TraefikFrontendEntryPoints), - "getBasicAuth": getFuncSliceStringValue(label.TraefikFrontendAuthBasic), - "getPriority": getFuncStringValue(label.TraefikFrontendPriority, label.DefaultFrontendPriority), - "getPassHostHeader": getFuncBoolValue(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), - "getPassTLSCert": getFuncBoolValue(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints), + "getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), + "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), "getFrontendRule": p.getFrontendRule, - "getRedirect": getRedirect, - "getErrorPages": getErrorPages, - "getRateLimit": getRateLimit, - "getHeaders": getHeaders, - "getWhiteList": getWhiteList, - - // TODO Deprecated [breaking] - "getFrontendBackend": getBackendName, + "getRedirect": label.GetRedirect, + "getErrorPages": label.GetErrorPages, + "getRateLimit": label.GetRateLimit, + "getHeaders": label.GetHeaders, + "getWhiteList": label.GetWhiteList, } // filter tasks - filteredTasks := fun.Filter(func(task state.Task) bool { - return taskFilter(task, p.ExposedByDefault) - }, tasks).([]state.Task) - - // Deprecated - var filteredApps []state.Task - uniqueApps := make(map[string]struct{}) - for _, task := range filteredTasks { - if _, ok := uniqueApps[task.DiscoveryInfo.Name]; !ok { - uniqueApps[task.DiscoveryInfo.Name] = struct{}{} - filteredApps = append(filteredApps, task) + appsTasks := make(map[string][]taskData) + for _, task := range tasks { + data := taskData{ + Task: task, + TraefikLabels: extractLabels(task), } - } - - appsTasks := make(map[string][]state.Task) - for _, task := range filteredTasks { - if _, ok := appsTasks[task.DiscoveryInfo.Name]; !ok { - appsTasks[task.DiscoveryInfo.Name] = []state.Task{task} - } else { - appsTasks[task.DiscoveryInfo.Name] = append(appsTasks[task.DiscoveryInfo.Name], task) + if taskFilter(data, p.ExposedByDefault) { + if _, ok := appsTasks[task.DiscoveryInfo.Name]; !ok { + appsTasks[task.DiscoveryInfo.Name] = []taskData{data} + } else { + appsTasks[task.DiscoveryInfo.Name] = append(appsTasks[task.DiscoveryInfo.Name], data) + } } } templateObjects := struct { - ApplicationsTasks map[string][]state.Task - Applications []state.Task // Deprecated - Tasks []state.Task // Deprecated + ApplicationsTasks map[string][]taskData Domain string }{ ApplicationsTasks: appsTasks, - Applications: filteredApps, // Deprecated - Tasks: filteredTasks, // Deprecated Domain: p.Domain, } @@ -98,10 +78,11 @@ func (p *Provider) buildConfiguration(tasks []state.Task) *types.Configuration { if err != nil { log.Error(err) } + return configuration } -func taskFilter(task state.Task, exposedByDefaultFlag bool) bool { +func taskFilter(task taskData, exposedByDefaultFlag bool) bool { if len(task.DiscoveryInfo.Ports.DiscoveryPorts) == 0 { log.Debugf("Filtering Mesos task without port %s", task.Name) return false @@ -113,8 +94,8 @@ func taskFilter(task state.Task, exposedByDefaultFlag bool) bool { } // filter indeterminable task port - portIndexLabel := getStringValue(task, label.TraefikPortIndex, "") - portValueLabel := getStringValue(task, label.TraefikPort, "") + portIndexLabel := label.GetStringValue(task.TraefikLabels, label.TraefikPortIndex, "") + portValueLabel := label.GetStringValue(task.TraefikLabels, label.TraefikPort, "") if portIndexLabel != "" && portValueLabel != "" { log.Debugf("Filtering Mesos task %s specifying both %q' and %q labels", task.Name, label.TraefikPortIndex, label.TraefikPort) return false @@ -156,84 +137,19 @@ func taskFilter(task state.Task, exposedByDefaultFlag bool) bool { return true } -func getID(task state.Task) string { +func getID(task taskData) string { return provider.Normalize(task.ID) } -// Deprecated -func getBackend(task state.Task, apps []state.Task) string { - _, err := getApplication(task, apps) - if err != nil { - log.Error(err) - return "" - } - return getBackendName(task) +func getBackendName(task taskData) string { + return label.GetStringValue(task.TraefikLabels, label.TraefikBackend, provider.Normalize(task.DiscoveryInfo.Name)) } -func getBackendName(task state.Task) string { - if value := getStringValue(task, label.TraefikBackend, ""); len(value) > 0 { - return value - } - return provider.Normalize(task.DiscoveryInfo.Name) -} - -func getFrontendName(task state.Task) string { +func getFrontendName(task taskData) string { // TODO task.ID -> task.Name + task.ID return provider.Normalize(task.ID) } -func (p *Provider) getServerPort(task state.Task) string { - plv := getIntValue(task, label.TraefikPortIndex, math.MinInt32, len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1) - if plv >= 0 { - return strconv.Itoa(task.DiscoveryInfo.Ports.DiscoveryPorts[plv].Number) - } - - if pv := getStringValue(task, label.TraefikPort, ""); len(pv) > 0 { - return pv - } - - for _, port := range task.DiscoveryInfo.Ports.DiscoveryPorts { - return strconv.Itoa(port.Number) - } - return "" -} - -// Deprecated -func (p *Provider) getPort(task state.Task, applications []state.Task) string { - _, err := getApplication(task, applications) - if err != nil { - log.Error(err) - return "" - } - - plv := getIntValue(task, label.TraefikPortIndex, math.MinInt32, len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1) - if plv >= 0 { - return strconv.Itoa(task.DiscoveryInfo.Ports.DiscoveryPorts[plv].Number) - } - - if pv := getStringValue(task, label.TraefikPort, ""); len(pv) > 0 { - return pv - } - - for _, port := range task.DiscoveryInfo.Ports.DiscoveryPorts { - return strconv.Itoa(port.Number) - } - return "" -} - -// getFrontendRule returns the frontend rule for the specified application, using -// it's label. It returns a default one (Host) if the label is not present. -func (p *Provider) getFrontendRule(task state.Task) string { - if v := getStringValue(task, label.TraefikFrontendRule, ""); len(v) > 0 { - return v - } - return "Host:" + strings.ToLower(strings.Replace(p.getSubDomain(task.DiscoveryInfo.Name), "_", "-", -1)) + "." + p.Domain -} - -func (p *Provider) getHost(task state.Task) string { - return task.IP(strings.Split(p.IPSources, ",")...) -} - func (p *Provider) getSubDomain(name string) string { if p.GroupsAsSubDomains { splitedName := strings.Split(strings.TrimPrefix(name, "/"), "/") @@ -244,78 +160,16 @@ func (p *Provider) getSubDomain(name string) string { return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) } -func getCircuitBreaker(task state.Task) *types.CircuitBreaker { - circuitBreaker := getStringValue(task, label.TraefikBackendCircuitBreakerExpression, "") - if len(circuitBreaker) == 0 { - return nil +// getFrontendRule returns the frontend rule for the specified application, using it's label. +// It returns a default one (Host) if the label is not present. +func (p *Provider) getFrontendRule(task taskData) string { + if v := label.GetStringValue(task.TraefikLabels, label.TraefikFrontendRule, ""); len(v) > 0 { + return v } - return &types.CircuitBreaker{Expression: circuitBreaker} + return "Host:" + strings.ToLower(strings.Replace(p.getSubDomain(task.DiscoveryInfo.Name), "_", "-", -1)) + "." + p.Domain } -func getLoadBalancer(task state.Task) *types.LoadBalancer { - if !hasPrefix(task, label.TraefikBackendLoadBalancer) { - return nil - } - - method := getStringValue(task, label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod) - - lb := &types.LoadBalancer{ - Method: method, - } - - if getBoolValue(task, label.TraefikBackendLoadBalancerStickiness, false) { - cookieName := getStringValue(task, label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName) - lb.Stickiness = &types.Stickiness{CookieName: cookieName} - } - - return lb -} - -func getMaxConn(task state.Task) *types.MaxConn { - amount := getInt64Value(task, label.TraefikBackendMaxConnAmount, math.MinInt64) - extractorFunc := getStringValue(task, label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) - - if amount == math.MinInt64 || len(extractorFunc) == 0 { - return nil - } - - return &types.MaxConn{ - Amount: amount, - ExtractorFunc: extractorFunc, - } -} - -func getHealthCheck(task state.Task) *types.HealthCheck { - path := getStringValue(task, label.TraefikBackendHealthCheckPath, "") - if len(path) == 0 { - return nil - } - - port := getIntValue(task, label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort, math.MaxInt32) - interval := getStringValue(task, label.TraefikBackendHealthCheckInterval, "") - - return &types.HealthCheck{ - Path: path, - Port: port, - Interval: interval, - } -} - -func getBuffering(task state.Task) *types.Buffering { - if !hasPrefix(task, label.TraefikBackendBuffering) { - return nil - } - - return &types.Buffering{ - MaxRequestBodyBytes: getInt64Value(task, label.TraefikBackendBufferingMaxRequestBodyBytes, 0), - MaxResponseBodyBytes: getInt64Value(task, label.TraefikBackendBufferingMaxResponseBodyBytes, 0), - MemRequestBodyBytes: getInt64Value(task, label.TraefikBackendBufferingMemRequestBodyBytes, 0), - MemResponseBodyBytes: getInt64Value(task, label.TraefikBackendBufferingMemResponseBodyBytes, 0), - RetryExpression: getStringValue(task, label.TraefikBackendBufferingRetryExpression, ""), - } -} - -func (p *Provider) getServers(tasks []state.Task) map[string]types.Server { +func (p *Provider) getServers(tasks []taskData) map[string]types.Server { var servers map[string]types.Server for _, task := range tasks { @@ -323,236 +177,56 @@ func (p *Provider) getServers(tasks []state.Task) map[string]types.Server { servers = make(map[string]types.Server) } - protocol := getStringValue(task, label.TraefikProtocol, label.DefaultProtocol) + protocol := label.GetStringValue(task.TraefikLabels, label.TraefikProtocol, label.DefaultProtocol) host := p.getHost(task) port := p.getServerPort(task) serverName := "server-" + getID(task) servers[serverName] = types.Server{ URL: fmt.Sprintf("%s://%s:%s", protocol, host, port), - Weight: getIntValue(task, label.TraefikWeight, label.DefaultWeightInt, math.MaxInt32), + Weight: getIntValue(task.TraefikLabels, label.TraefikWeight, label.DefaultWeightInt, math.MaxInt32), } } return servers } -func getWhiteList(task state.Task) *types.WhiteList { - ranges := getSliceStringValue(task, label.TraefikFrontendWhiteListSourceRange) - if len(ranges) > 0 { - return &types.WhiteList{ - SourceRange: ranges, - UseXForwardedFor: getBoolValue(task, label.TraefikFrontendWhiteListUseXForwardedFor, false), - } - } - - return nil +func (p *Provider) getHost(task taskData) string { + return task.IP(strings.Split(p.IPSources, ",")...) } -func getRedirect(task state.Task) *types.Redirect { - permanent := getBoolValue(task, label.TraefikFrontendRedirectPermanent, false) - - if hasLabel(task, label.TraefikFrontendRedirectEntryPoint) { - return &types.Redirect{ - EntryPoint: getStringValue(task, label.TraefikFrontendRedirectEntryPoint, ""), - Permanent: permanent, - } +func (p *Provider) getServerPort(task taskData) string { + plv := getIntValue(task.TraefikLabels, label.TraefikPortIndex, math.MinInt32, len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1) + if plv >= 0 { + return strconv.Itoa(task.DiscoveryInfo.Ports.DiscoveryPorts[plv].Number) } - if hasLabel(task, label.TraefikFrontendRedirectRegex) && - hasLabel(task, label.TraefikFrontendRedirectReplacement) { - return &types.Redirect{ - Regex: getStringValue(task, label.TraefikFrontendRedirectRegex, ""), - Replacement: getStringValue(task, label.TraefikFrontendRedirectReplacement, ""), - Permanent: permanent, - } + if pv := label.GetStringValue(task.TraefikLabels, label.TraefikPort, ""); len(pv) > 0 { + return pv } - return nil + for _, port := range task.DiscoveryInfo.Ports.DiscoveryPorts { + return strconv.Itoa(port.Number) + } + return "" } -func getErrorPages(task state.Task) map[string]*types.ErrorPage { - prefix := label.Prefix + label.BaseFrontendErrorPage - labels := taskLabelsToMap(task) - return label.ParseErrorPages(labels, prefix, label.RegexpFrontendErrorPage) -} - -func getRateLimit(task state.Task) *types.RateLimit { - extractorFunc := getStringValue(task, label.TraefikFrontendRateLimitExtractorFunc, "") - if len(extractorFunc) == 0 { - return nil - } - - labels := taskLabelsToMap(task) - prefix := label.Prefix + label.BaseFrontendRateLimit - limits := label.ParseRateSets(labels, prefix, label.RegexpFrontendRateLimit) - - return &types.RateLimit{ - ExtractorFunc: extractorFunc, - RateSet: limits, - } -} - -func getHeaders(task state.Task) *types.Headers { - labels := taskLabelsToMap(task) - - headers := &types.Headers{ - CustomRequestHeaders: label.GetMapValue(labels, label.TraefikFrontendRequestHeaders), - CustomResponseHeaders: label.GetMapValue(labels, label.TraefikFrontendResponseHeaders), - SSLProxyHeaders: label.GetMapValue(labels, label.TraefikFrontendSSLProxyHeaders), - AllowedHosts: label.GetSliceStringValue(labels, label.TraefikFrontendAllowedHosts), - HostsProxyHeaders: label.GetSliceStringValue(labels, label.TraefikFrontendHostsProxyHeaders), - STSSeconds: label.GetInt64Value(labels, label.TraefikFrontendSTSSeconds, 0), - SSLRedirect: label.GetBoolValue(labels, label.TraefikFrontendSSLRedirect, false), - SSLTemporaryRedirect: label.GetBoolValue(labels, label.TraefikFrontendSSLTemporaryRedirect, false), - STSIncludeSubdomains: label.GetBoolValue(labels, label.TraefikFrontendSTSIncludeSubdomains, false), - STSPreload: label.GetBoolValue(labels, label.TraefikFrontendSTSPreload, false), - ForceSTSHeader: label.GetBoolValue(labels, label.TraefikFrontendForceSTSHeader, false), - FrameDeny: label.GetBoolValue(labels, label.TraefikFrontendFrameDeny, false), - ContentTypeNosniff: label.GetBoolValue(labels, label.TraefikFrontendContentTypeNosniff, false), - BrowserXSSFilter: label.GetBoolValue(labels, label.TraefikFrontendBrowserXSSFilter, false), - IsDevelopment: label.GetBoolValue(labels, label.TraefikFrontendIsDevelopment, false), - SSLHost: label.GetStringValue(labels, label.TraefikFrontendSSLHost, ""), - CustomFrameOptionsValue: label.GetStringValue(labels, label.TraefikFrontendCustomFrameOptionsValue, ""), - ContentSecurityPolicy: label.GetStringValue(labels, label.TraefikFrontendContentSecurityPolicy, ""), - PublicKey: label.GetStringValue(labels, label.TraefikFrontendPublicKey, ""), - ReferrerPolicy: label.GetStringValue(labels, label.TraefikFrontendReferrerPolicy, ""), - CustomBrowserXSSValue: label.GetStringValue(labels, label.TraefikFrontendCustomBrowserXSSValue, ""), - } - - if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { - return nil - } - - return headers -} - -func isEnabled(task state.Task, exposedByDefault bool) bool { - return getBoolValue(task, label.TraefikEnable, exposedByDefault) +func isEnabled(task taskData, exposedByDefault bool) bool { + return label.GetBoolValue(task.TraefikLabels, label.TraefikEnable, exposedByDefault) } // Label functions -// Deprecated -func getFuncApplicationStringValue(labelName string, defaultValue string) func(task state.Task, applications []state.Task) string { - return func(task state.Task, applications []state.Task) string { - _, err := getApplication(task, applications) - if err != nil { - log.Error(err) - return defaultValue - } - - return getStringValue(task, labelName, defaultValue) - } -} - -func getFuncStringValue(labelName string, defaultValue string) func(task state.Task) string { - return func(task state.Task) string { - return getStringValue(task, labelName, defaultValue) - } -} - -func getFuncBoolValue(labelName string, defaultValue bool) func(task state.Task) bool { - return func(task state.Task) bool { - return getBoolValue(task, labelName, defaultValue) - } -} - -func getFuncSliceStringValue(labelName string) func(task state.Task) []string { - return func(task state.Task) []string { - return getSliceStringValue(task, labelName) - } -} - -func getStringValue(task state.Task, labelName string, defaultValue string) string { - for _, lbl := range task.Labels { - if lbl.Key == labelName && len(lbl.Value) > 0 { - return lbl.Value - } +func getIntValue(labels map[string]string, labelName string, defaultValue int, maxValue int) int { + value := label.GetIntValue(labels, labelName, defaultValue) + if value <= maxValue { + return value } + log.Warnf("The value %q for %q exceed the max authorized value %q, falling back to %v.", value, labelName, maxValue, defaultValue) return defaultValue } -func getBoolValue(task state.Task, labelName string, defaultValue bool) bool { - for _, lbl := range task.Labels { - if lbl.Key == labelName { - v, err := strconv.ParseBool(lbl.Value) - if err == nil { - return v - } - } - } - return defaultValue -} - -func getIntValue(task state.Task, labelName string, defaultValue int, maxValue int) int { - for _, lbl := range task.Labels { - if lbl.Key == labelName { - value, err := strconv.Atoi(lbl.Value) - if err == nil { - if value <= maxValue { - return value - } - log.Warnf("The value %q for %q exceed the max authorized value %q, falling back to %v.", lbl.Value, labelName, maxValue, defaultValue) - } else { - log.Warnf("Unable to parse %q: %q, falling back to %v. %v", labelName, lbl.Value, defaultValue, err) - } - } - } - return defaultValue -} - -func getSliceStringValue(task state.Task, labelName string) []string { - for _, lbl := range task.Labels { - if lbl.Key == labelName { - return label.SplitAndTrimString(lbl.Value, ",") - } - } - return nil -} - -// Deprecated -func getApplication(task state.Task, apps []state.Task) (state.Task, error) { - for _, app := range apps { - if app.DiscoveryInfo.Name == task.DiscoveryInfo.Name { - return app, nil - } - } - return state.Task{}, fmt.Errorf("unable to get Mesos application from task %s", task.DiscoveryInfo.Name) -} - -func hasPrefix(task state.Task, prefix string) bool { - for _, lbl := range task.Labels { - if strings.HasPrefix(lbl.Key, prefix) { - return true - } - } - return false -} - -func getInt64Value(task state.Task, labelName string, defaultValue int64) int64 { - for _, lbl := range task.Labels { - if lbl.Key == labelName { - value, err := strconv.ParseInt(lbl.Value, 10, 64) - if err != nil { - log.Warnf("Unable to parse %q: %q, falling back to %v. %v", labelName, lbl.Value, defaultValue, err) - } - return value - } - } - return defaultValue -} - -func hasLabel(task state.Task, label string) bool { - for _, lbl := range task.Labels { - if lbl.Key == label { - return true - } - } - return false -} - -func taskLabelsToMap(task state.Task) map[string]string { +func extractLabels(task state.Task) map[string]string { labels := make(map[string]string) for _, lbl := range task.Labels { labels[lbl.Key] = lbl.Value diff --git a/provider/mesos/config_root.go b/provider/mesos/config_root.go new file mode 100644 index 000000000..487c366ea --- /dev/null +++ b/provider/mesos/config_root.go @@ -0,0 +1,13 @@ +package mesos + +import ( + "github.com/containous/traefik/types" + "github.com/mesosphere/mesos-dns/records/state" +) + +func (p *Provider) buildConfiguration(tasks []state.Task) *types.Configuration { + if p.TemplateVersion == 1 { + return p.buildConfigurationV1(tasks) + } + return p.buildConfigurationV2(tasks) +} diff --git a/provider/mesos/config_test.go b/provider/mesos/config_test.go index 730f336b3..335858723 100644 --- a/provider/mesos/config_test.go +++ b/provider/mesos/config_test.go @@ -7,7 +7,6 @@ import ( "github.com/containous/flaeg" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" - "github.com/mesos/mesos-go/upid" "github.com/mesosphere/mesos-dns/records/state" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -15,7 +14,7 @@ import ( func TestBuildConfiguration(t *testing.T) { p := &Provider{ - Domain: "docker.localhost", + Domain: "mesos.localhost", ExposedByDefault: true, IPSources: "host", } @@ -70,7 +69,7 @@ func TestBuildConfiguration(t *testing.T) { PassHostHeader: true, Routes: map[string]types.Route{ "route-host-ID1": { - Rule: "Host:name1.docker.localhost", + Rule: "Host:name1.mesos.localhost", }, }, }, @@ -81,7 +80,7 @@ func TestBuildConfiguration(t *testing.T) { PassHostHeader: true, Routes: map[string]types.Route{ "route-host-ID3": { - Rule: "Host:name2.docker.localhost", + Rule: "Host:name2.mesos.localhost", }, }, }, @@ -333,8 +332,9 @@ func TestBuildConfiguration(t *testing.T) { for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() - actualConfig := p.buildConfiguration(test.tasks) + actualConfig := p.buildConfigurationV2(test.tasks) require.NotNil(t, actualConfig) assert.Equal(t, test.expectedBackends, actualConfig.Backends) @@ -346,25 +346,25 @@ func TestBuildConfiguration(t *testing.T) { func TestTaskFilter(t *testing.T) { testCases := []struct { desc string - mesosTask state.Task + mesosTask taskData exposedByDefault bool expected bool }{ { desc: "no task", - mesosTask: state.Task{}, + mesosTask: taskData{}, exposedByDefault: true, expected: false, }, { desc: "task not healthy", - mesosTask: aTask("test", withStatus(withState("TASK_RUNNING"))), + mesosTask: aTaskData("test", withStatus(withState("TASK_RUNNING"))), exposedByDefault: true, expected: false, }, { desc: "exposedByDefault false and traefik.enable false", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "false"), withInfo("test", withPorts(withPortTCP(80, "WEB"))), @@ -374,7 +374,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.enable = true", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withInfo("test", withPorts(withPortTCP(80, "WEB"))), @@ -384,7 +384,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "exposedByDefault true and traefik.enable true", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withInfo("test", withPorts(withPortTCP(80, "WEB"))), @@ -394,7 +394,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "exposedByDefault true and traefik.enable false", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "false"), withInfo("test", withPorts(withPortTCP(80, "WEB"))), @@ -404,7 +404,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.portIndex and traefik.port both set", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPortIndex, "1"), @@ -416,7 +416,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "valid traefik.portIndex", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPortIndex, "1"), @@ -430,7 +430,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "default to first port index", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withInfo("test", withPorts( @@ -443,7 +443,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.portIndex and discoveryPorts don't correspond", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPortIndex, "1"), @@ -454,7 +454,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.portIndex and discoveryPorts correspond", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPortIndex, "0"), @@ -465,7 +465,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.port is not an integer", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPort, "TRAEFIK"), @@ -476,7 +476,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.port is not the same as discovery.port", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPort, "443"), @@ -487,7 +487,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "traefik.port is the same as discovery.port", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withDefaultStatus(), withLabel(label.TraefikEnable, "true"), withLabel(label.TraefikPort, "80"), @@ -498,7 +498,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "healthy nil", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withStatus( withState("TASK_RUNNING"), ), @@ -511,7 +511,7 @@ func TestTaskFilter(t *testing.T) { }, { desc: "healthy false", - mesosTask: aTask("test", + mesosTask: aTaskData("test", withStatus( withState("TASK_RUNNING"), withHealthy(false), @@ -542,35 +542,6 @@ func TestTaskFilter(t *testing.T) { } } -func TestTaskRecords(t *testing.T) { - var task = state.Task{ - SlaveID: "s_id", - State: "TASK_RUNNING", - } - var framework = state.Framework{ - Tasks: []state.Task{task}, - } - var slave = state.Slave{ - ID: "s_id", - Hostname: "127.0.0.1", - } - slave.PID.UPID = &upid.UPID{} - slave.PID.Host = slave.Hostname - - var taskState = state.State{ - Slaves: []state.Slave{slave}, - Frameworks: []state.Framework{framework}, - } - - var p = taskRecords(taskState) - if len(p) == 0 { - t.Fatal("No task") - } - if p[0].SlaveIP != slave.Hostname { - t.Fatalf("The SlaveIP (%s) should be set with the slave hostname (%s)", p[0].SlaveID, slave.Hostname) - } -} - func TestGetSubDomain(t *testing.T) { providerGroups := &Provider{GroupsAsSubDomains: true} providerNoGroups := &Provider{GroupsAsSubDomains: false} @@ -604,296 +575,23 @@ func TestGetSubDomain(t *testing.T) { } } -func TestGetCircuitBreaker(t *testing.T) { - testCases := []struct { - desc string - task state.Task - expected *types.CircuitBreaker - }{ - { - desc: "should return nil when no CB labels", - task: aTask("ID1", - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - { - desc: "should return a struct CB when CB labels are set", - task: aTask("ID1", - withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getCircuitBreaker(test.task) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetLoadBalancer(t *testing.T) { - testCases := []struct { - desc string - task state.Task - expected *types.LoadBalancer - }{ - { - desc: "should return nil when no LB labels", - task: aTask("ID1", - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - { - desc: "should return a struct when labels are set", - task: aTask("ID1", - withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), - withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), - withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "foo"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.LoadBalancer{ - Method: "drr", - Stickiness: &types.Stickiness{ - CookieName: "foo", - }, - }, - }, - { - desc: "should return a nil Stickiness when Stickiness is not set", - task: aTask("ID1", - withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), - withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "foo"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.LoadBalancer{ - Method: "drr", - Stickiness: nil, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getLoadBalancer(test.task) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetMaxConn(t *testing.T) { - testCases := []struct { - desc string - task state.Task - expected *types.MaxConn - }{ - { - desc: "should return nil when no max conn labels", - task: aTask("ID1", - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - { - desc: "should return nil when no amount label", - task: aTask("ID1", - withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - { - desc: "should return default when empty extractorFunc label", - task: aTask("ID1", - withLabel(label.TraefikBackendMaxConnExtractorFunc, ""), - withLabel(label.TraefikBackendMaxConnAmount, "666"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.MaxConn{ - ExtractorFunc: "request.host", - Amount: 666, - }, - }, - { - desc: "should return a struct when max conn labels are set", - task: aTask("ID1", - withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - withLabel(label.TraefikBackendMaxConnAmount, "666"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.MaxConn{ - ExtractorFunc: "client.ip", - Amount: 666, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getMaxConn(test.task) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetHealthCheck(t *testing.T) { - testCases := []struct { - desc string - task state.Task - expected *types.HealthCheck - }{ - { - desc: "should return nil when no health check labels", - task: aTask("ID1", - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - { - desc: "should return nil when no health check Path label", - task: aTask("ID1", - withLabel(label.TraefikBackendHealthCheckPort, "80"), - withLabel(label.TraefikBackendHealthCheckInterval, "6"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - { - desc: "should return a struct when health check labels are set", - task: aTask("ID1", - withLabel(label.TraefikBackendHealthCheckPath, "/health"), - withLabel(label.TraefikBackendHealthCheckPort, "80"), - withLabel(label.TraefikBackendHealthCheckInterval, "6"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.HealthCheck{ - Path: "/health", - Port: 80, - Interval: "6", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getHealthCheck(test.task) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetBuffering(t *testing.T) { - testCases := []struct { - desc string - task state.Task - expected *types.Buffering - }{ - { - desc: "should return nil when no buffering labels", - task: aTask("ID1", - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - { - desc: "should return a struct when health check labels are set", - task: aTask("ID1", - withLabel(label.TraefikBackendBufferingMaxResponseBodyBytes, "10485760"), - withLabel(label.TraefikBackendBufferingMemResponseBodyBytes, "2097152"), - withLabel(label.TraefikBackendBufferingMaxRequestBodyBytes, "10485760"), - withLabel(label.TraefikBackendBufferingMemRequestBodyBytes, "2097152"), - withLabel(label.TraefikBackendBufferingRetryExpression, "IsNetworkError() && Attempts() <= 2"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.Buffering{ - MaxResponseBodyBytes: 10485760, - MemResponseBodyBytes: 2097152, - MaxRequestBodyBytes: 10485760, - MemRequestBodyBytes: 2097152, - RetryExpression: "IsNetworkError() && Attempts() <= 2", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getBuffering(test.task) - - assert.Equal(t, test.expected, actual) - }) - } -} - func TestGetServers(t *testing.T) { testCases := []struct { desc string - tasks []state.Task + tasks []taskData expected map[string]types.Server }{ { desc: "", - tasks: []state.Task{ + tasks: []taskData{ // App 1 - aTask("ID1", + aTaskData("ID1", withIP("10.10.10.10"), withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), withStatus(withHealthy(true), withState("TASK_RUNNING")), ), - aTask("ID2", + aTaskData("ID2", withIP("10.10.10.11"), withLabel(label.TraefikWeight, "18"), withInfo("name1", @@ -901,14 +599,14 @@ func TestGetServers(t *testing.T) { withStatus(withHealthy(true), withState("TASK_RUNNING")), ), // App 2 - aTask("ID3", + aTaskData("ID3", withLabel(label.TraefikWeight, "12"), withIP("20.10.10.10"), withInfo("name2", withPorts(withPort("TCP", 80, "WEB"))), withStatus(withHealthy(true), withState("TASK_RUNNING")), ), - aTask("ID4", + aTaskData("ID4", withLabel(label.TraefikWeight, "6"), withIP("20.10.10.11"), withInfo("name2", @@ -954,396 +652,3 @@ func TestGetServers(t *testing.T) { }) } } - -func TestWhiteList(t *testing.T) { - testCases := []struct { - desc string - task state.Task - expected *types.WhiteList - }{ - { - desc: "should return nil when no white list labels", - task: aTask("ID1", - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - { - desc: "should return a struct when only range", - task: aTask("ID1", - withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: false, - }, - }, - { - desc: "should return a struct when range and UseXForwardedFor", - task: aTask("ID1", - withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"), - withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.WhiteList{ - SourceRange: []string{ - "10.10.10.10", - }, - UseXForwardedFor: true, - }, - }, - { - desc: "should return nil when only UseXForwardedFor", - task: aTask("ID1", - withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getWhiteList(test.task) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetRedirect(t *testing.T) { - testCases := []struct { - desc string - task state.Task - expected *types.Redirect - }{ - - { - desc: "should return nil when no redirect labels", - task: aTask("ID1", - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - { - desc: "should use only entry point tag when mix regex redirect and entry point redirect", - task: aTask("ID1", - withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), - withLabel(label.TraefikFrontendRedirectRegex, "(.*)"), - withLabel(label.TraefikFrontendRedirectReplacement, "$1"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label", - task: aTask("ID1", - withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.Redirect{ - EntryPoint: "https", - }, - }, - { - desc: "should return a struct when entry point redirect label (permanent)", - task: aTask("ID1", - withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), - withLabel(label.TraefikFrontendRedirectPermanent, "true"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.Redirect{ - EntryPoint: "https", - Permanent: true, - }, - }, - { - desc: "should return a struct when regex redirect labels", - task: aTask("ID1", - withLabel(label.TraefikFrontendRedirectRegex, "(.*)"), - withLabel(label.TraefikFrontendRedirectReplacement, "$1"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - }, - }, - { - desc: "should return a struct when regex redirect labels (permanent)", - task: aTask("ID1", - withLabel(label.TraefikFrontendRedirectRegex, "(.*)"), - withLabel(label.TraefikFrontendRedirectReplacement, "$1"), - withLabel(label.TraefikFrontendRedirectPermanent, "true"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.Redirect{ - Regex: "(.*)", - Replacement: "$1", - Permanent: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getRedirect(test.task) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetErrorPages(t *testing.T) { - testCases := []struct { - desc string - task state.Task - expected map[string]*types.ErrorPage - }{ - { - desc: "2 errors pages", - task: aTask("ID1", - withIP("10.10.10.10"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageBackend, "foo_backend"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageQuery, "foo_query"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageStatus, "500,600"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageBackend, "bar_backend"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageQuery, "bar_query"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - Query: "foo_query", - Backend: "foo_backend", - }, - "bar": { - Status: []string{"500", "600"}, - Query: "bar_query", - Backend: "bar_backend", - }, - }, - }, - { - desc: "only status field", - task: aTask("ID1", - withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getErrorPages(test.task) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetRateLimit(t *testing.T) { - testCases := []struct { - desc string - task state.Task - expected *types.RateLimit - }{ - { - desc: "should return nil when no rate limit labels", - task: aTask("ID1", - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - { - desc: "should return a struct when rate limit labels are defined", - task: aTask("ID1", - withLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &types.RateLimit{ - ExtractorFunc: "client.ip", - RateSet: map[string]*types.Rate{ - "foo": { - Period: flaeg.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - "bar": { - Period: flaeg.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - }, - }, - }, - { - desc: "should return nil when ExtractorFunc is missing", - task: aTask("ID1", - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getRateLimit(test.task) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetHeaders(t *testing.T) { - testCases := []struct { - desc string - task state.Task - expected *types.Headers - }{ - { - desc: "should return nil when no custom headers options are set", - task: aTask("ID1", - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: nil, - }, - { - desc: "should return a struct when all custom headers options are set", - task: aTask("ID1", - withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor"), - withLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor"), - withLabel(label.TraefikFrontendSSLHost, "foo"), - withLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo"), - withLabel(label.TraefikFrontendContentSecurityPolicy, "foo"), - withLabel(label.TraefikFrontendPublicKey, "foo"), - withLabel(label.TraefikFrontendReferrerPolicy, "foo"), - withLabel(label.TraefikFrontendCustomBrowserXSSValue, "foo"), - withLabel(label.TraefikFrontendSTSSeconds, "666"), - withLabel(label.TraefikFrontendSSLRedirect, "true"), - withLabel(label.TraefikFrontendSSLTemporaryRedirect, "true"), - withLabel(label.TraefikFrontendSTSIncludeSubdomains, "true"), - withLabel(label.TraefikFrontendSTSPreload, "true"), - withLabel(label.TraefikFrontendForceSTSHeader, "true"), - withLabel(label.TraefikFrontendFrameDeny, "true"), - withLabel(label.TraefikFrontendContentTypeNosniff, "true"), - withLabel(label.TraefikFrontendBrowserXSSFilter, "true"), - withLabel(label.TraefikFrontendIsDevelopment, "true"), - withIP("10.10.10.10"), - withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))), - withDefaultStatus(), - ), - expected: &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", - }, - SSLProxyHeaders: 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"}, - SSLHost: "foo", - CustomFrameOptionsValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - CustomBrowserXSSValue: "foo", - STSSeconds: 666, - SSLRedirect: true, - SSLTemporaryRedirect: true, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - ContentTypeNosniff: true, - BrowserXSSFilter: true, - IsDevelopment: true, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getHeaders(test.task) - - assert.Equal(t, test.expected, actual) - }) - } -} diff --git a/provider/mesos/deprecated_config.go b/provider/mesos/deprecated_config.go new file mode 100644 index 000000000..f4469496f --- /dev/null +++ b/provider/mesos/deprecated_config.go @@ -0,0 +1,327 @@ +package mesos + +import ( + "fmt" + "math" + "strconv" + "strings" + "text/template" + + "github.com/BurntSushi/ty/fun" + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/mesosphere/mesos-dns/records/state" +) + +func (p *Provider) buildConfigurationV1(tasks []state.Task) *types.Configuration { + var mesosFuncMap = template.FuncMap{ + "getDomain": getFuncStringValueV1(label.TraefikDomain, p.Domain), + "getID": getIDV1, + + // Backend functions + "getBackendName": getBackendNameV1, + "getHost": p.getHostV1, + "getProtocol": getFuncApplicationStringValueV1(label.TraefikProtocol, label.DefaultProtocol), + "getWeight": getFuncApplicationIntValueV1(label.TraefikWeight, label.DefaultWeightInt), + "getBackend": getBackendV1, + "getPort": p.getPort, + + // Frontend functions + "getFrontendBackend": getBackendNameV1, + "getFrontEndName": getFrontendNameV1, + "getEntryPoints": getFuncSliceStringValueV1(label.TraefikFrontendEntryPoints), + "getBasicAuth": getFuncSliceStringValueV1(label.TraefikFrontendAuthBasic), + "getPriority": getFuncIntValueV1(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": getFuncBoolValueV1(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getFrontendRule": p.getFrontendRuleV1, + } + + // filter tasks + filteredTasks := fun.Filter(func(task state.Task) bool { + return taskFilterV1(task, p.ExposedByDefault) + }, tasks).([]state.Task) + + // Deprecated + var filteredApps []state.Task + uniqueApps := make(map[string]struct{}) + for _, task := range filteredTasks { + if _, ok := uniqueApps[task.DiscoveryInfo.Name]; !ok { + uniqueApps[task.DiscoveryInfo.Name] = struct{}{} + filteredApps = append(filteredApps, task) + } + } + + appsTasks := make(map[string][]state.Task) + for _, task := range filteredTasks { + if _, ok := appsTasks[task.DiscoveryInfo.Name]; !ok { + appsTasks[task.DiscoveryInfo.Name] = []state.Task{task} + } else { + appsTasks[task.DiscoveryInfo.Name] = append(appsTasks[task.DiscoveryInfo.Name], task) + } + } + + templateObjects := struct { + ApplicationsTasks map[string][]state.Task + Applications []state.Task // Deprecated + Tasks []state.Task // Deprecated + Domain string + }{ + ApplicationsTasks: appsTasks, + Applications: filteredApps, // Deprecated + Tasks: filteredTasks, // Deprecated + Domain: p.Domain, + } + + configuration, err := p.GetConfiguration("templates/mesos-v1.tmpl", mesosFuncMap, templateObjects) + if err != nil { + log.Error(err) + } + return configuration +} + +// Deprecated +func taskFilterV1(task state.Task, exposedByDefaultFlag bool) bool { + if len(task.DiscoveryInfo.Ports.DiscoveryPorts) == 0 { + log.Debugf("Filtering Mesos task without port %s", task.Name) + return false + } + + if !isEnabledV1(task, exposedByDefaultFlag) { + log.Debugf("Filtering disabled Mesos task %s", task.DiscoveryInfo.Name) + return false + } + + // filter indeterminable task port + portIndexLabel := getStringValueV1(task, label.TraefikPortIndex, "") + portValueLabel := getStringValueV1(task, label.TraefikPort, "") + if portIndexLabel != "" && portValueLabel != "" { + log.Debugf("Filtering Mesos task %s specifying both %q' and %q labels", task.Name, label.TraefikPortIndex, label.TraefikPort) + return false + } + if portIndexLabel != "" { + index, err := strconv.Atoi(portIndexLabel) + if err != nil || index < 0 || index > len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1 { + log.Debugf("Filtering Mesos task %s with unexpected value for %q label", task.Name, label.TraefikPortIndex) + return false + } + } + if portValueLabel != "" { + port, err := strconv.Atoi(portValueLabel) + if err != nil { + log.Debugf("Filtering Mesos task %s with unexpected value for %q label", task.Name, label.TraefikPort) + return false + } + + var foundPort bool + for _, exposedPort := range task.DiscoveryInfo.Ports.DiscoveryPorts { + if port == exposedPort.Number { + foundPort = true + break + } + } + + if !foundPort { + log.Debugf("Filtering Mesos task %s without a matching port for %q label", task.Name, label.TraefikPort) + return false + } + } + + // filter healthChecks + if task.Statuses != nil && len(task.Statuses) > 0 && task.Statuses[0].Healthy != nil && !*task.Statuses[0].Healthy { + log.Debugf("Filtering Mesos task %s with bad healthCheck", task.DiscoveryInfo.Name) + return false + + } + return true +} + +// Deprecated +func getIDV1(task state.Task) string { + return provider.Normalize(task.ID) +} + +// Deprecated +func getBackendV1(task state.Task, apps []state.Task) string { + _, err := getApplicationV1(task, apps) + if err != nil { + log.Error(err) + return "" + } + return getBackendNameV1(task) +} + +// Deprecated +func getBackendNameV1(task state.Task) string { + if value := getStringValueV1(task, label.TraefikBackend, ""); len(value) > 0 { + return value + } + return provider.Normalize(task.DiscoveryInfo.Name) +} + +// Deprecated +func getFrontendNameV1(task state.Task) string { + // TODO task.ID -> task.Name + task.ID + return provider.Normalize(task.ID) +} + +// Deprecated +func (p *Provider) getPort(task state.Task, applications []state.Task) string { + _, err := getApplicationV1(task, applications) + if err != nil { + log.Error(err) + return "" + } + + plv := getIntValueV1(task, label.TraefikPortIndex, math.MinInt32, len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1) + if plv >= 0 { + return strconv.Itoa(task.DiscoveryInfo.Ports.DiscoveryPorts[plv].Number) + } + + if pv := getStringValueV1(task, label.TraefikPort, ""); len(pv) > 0 { + return pv + } + + for _, port := range task.DiscoveryInfo.Ports.DiscoveryPorts { + return strconv.Itoa(port.Number) + } + return "" +} + +// getFrontendRuleV1 returns the frontend rule for the specified application, using +// it's label. It returns a default one (Host) if the label is not present. +// Deprecated +func (p *Provider) getFrontendRuleV1(task state.Task) string { + if v := getStringValueV1(task, label.TraefikFrontendRule, ""); len(v) > 0 { + return v + } + return "Host:" + strings.ToLower(strings.Replace(p.getSubDomain(task.DiscoveryInfo.Name), "_", "-", -1)) + "." + p.Domain +} + +// Deprecated +func (p *Provider) getHostV1(task state.Task) string { + return task.IP(strings.Split(p.IPSources, ",")...) +} + +// Deprecated +func isEnabledV1(task state.Task, exposedByDefault bool) bool { + return getBoolValueV1(task, label.TraefikEnable, exposedByDefault) +} + +// Label functions + +// Deprecated +func getFuncApplicationStringValueV1(labelName string, defaultValue string) func(task state.Task, applications []state.Task) string { + return func(task state.Task, applications []state.Task) string { + _, err := getApplicationV1(task, applications) + if err != nil { + log.Error(err) + return defaultValue + } + + return getStringValueV1(task, labelName, defaultValue) + } +} + +// Deprecated +func getFuncApplicationIntValueV1(labelName string, defaultValue int) func(task state.Task, applications []state.Task) int { + return func(task state.Task, applications []state.Task) int { + _, err := getApplicationV1(task, applications) + if err != nil { + log.Error(err) + return defaultValue + } + + return getIntValueV1(task, labelName, defaultValue, math.MaxInt64) + } +} + +// Deprecated +func getFuncStringValueV1(labelName string, defaultValue string) func(task state.Task) string { + return func(task state.Task) string { + return getStringValueV1(task, labelName, defaultValue) + } +} + +// Deprecated +func getFuncBoolValueV1(labelName string, defaultValue bool) func(task state.Task) bool { + return func(task state.Task) bool { + return getBoolValueV1(task, labelName, defaultValue) + } +} + +// Deprecated +func getFuncIntValueV1(labelName string, defaultValue int) func(task state.Task) int { + return func(task state.Task) int { + return getIntValueV1(task, labelName, defaultValue, math.MaxInt64) + } +} + +// Deprecated +func getFuncSliceStringValueV1(labelName string) func(task state.Task) []string { + return func(task state.Task) []string { + return getSliceStringValueV1(task, labelName) + } +} + +// Deprecated +func getStringValueV1(task state.Task, labelName string, defaultValue string) string { + for _, lbl := range task.Labels { + if lbl.Key == labelName && len(lbl.Value) > 0 { + return lbl.Value + } + } + return defaultValue +} + +// Deprecated +func getBoolValueV1(task state.Task, labelName string, defaultValue bool) bool { + for _, lbl := range task.Labels { + if lbl.Key == labelName { + v, err := strconv.ParseBool(lbl.Value) + if err == nil { + return v + } + } + } + return defaultValue +} + +// Deprecated +func getIntValueV1(task state.Task, labelName string, defaultValue int, maxValue int) int { + for _, lbl := range task.Labels { + if lbl.Key == labelName { + value, err := strconv.Atoi(lbl.Value) + if err == nil { + if value <= maxValue { + return value + } + log.Warnf("The value %q for %q exceed the max authorized value %q, falling back to %v.", lbl.Value, labelName, maxValue, defaultValue) + } else { + log.Warnf("Unable to parse %q: %q, falling back to %v. %v", labelName, lbl.Value, defaultValue, err) + } + } + } + return defaultValue +} + +// Deprecated +func getSliceStringValueV1(task state.Task, labelName string) []string { + for _, lbl := range task.Labels { + if lbl.Key == labelName { + return label.SplitAndTrimString(lbl.Value, ",") + } + } + return nil +} + +// Deprecated +func getApplicationV1(task state.Task, apps []state.Task) (state.Task, error) { + for _, app := range apps { + if app.DiscoveryInfo.Name == task.DiscoveryInfo.Name { + return app, nil + } + } + return state.Task{}, fmt.Errorf("unable to get Mesos application from task %s", task.DiscoveryInfo.Name) +} diff --git a/provider/mesos/deprecated_config_test.go b/provider/mesos/deprecated_config_test.go new file mode 100644 index 000000000..1366b8587 --- /dev/null +++ b/provider/mesos/deprecated_config_test.go @@ -0,0 +1,432 @@ +package mesos + +import ( + "testing" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/mesosphere/mesos-dns/records/state" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuildConfigurationV1(t *testing.T) { + p := &Provider{ + Domain: "mesos.localhost", + ExposedByDefault: true, + IPSources: "host", + } + + testCases := []struct { + desc string + tasks []state.Task + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "when no tasks", + tasks: []state.Task{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "2 applications with 2 tasks", + tasks: []state.Task{ + // App 1 + aTask("ID1", + withIP("10.10.10.10"), + withInfo("name1", + withPorts(withPort("TCP", 80, "WEB"))), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + ), + aTask("ID2", + withIP("10.10.10.11"), + withInfo("name1", + withPorts(withPort("TCP", 81, "WEB"))), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + ), + // App 2 + aTask("ID3", + withIP("20.10.10.10"), + withInfo("name2", + withPorts(withPort("TCP", 80, "WEB"))), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + ), + aTask("ID4", + withIP("20.10.10.11"), + withInfo("name2", + withPorts(withPort("TCP", 81, "WEB"))), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-ID1": { + Backend: "backend-name1", + EntryPoints: []string{}, + PassHostHeader: true, + Routes: map[string]types.Route{ + "route-host-ID1": { + Rule: "Host:name1.mesos.localhost", + }, + }, + }, + "frontend-ID3": { + Backend: "backend-name2", + EntryPoints: []string{}, + PassHostHeader: true, + Routes: map[string]types.Route{ + "route-host-ID3": { + Rule: "Host:name2.mesos.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-name1": { + Servers: map[string]types.Server{ + "server-ID1": { + URL: "http://10.10.10.10:80", + Weight: 0, + }, + "server-ID2": { + URL: "http://10.10.10.11:81", + Weight: 0, + }, + }, + }, + "backend-name2": { + Servers: map[string]types.Server{ + "server-ID3": { + URL: "http://20.10.10.10:80", + Weight: 0, + }, + "server-ID4": { + URL: "http://20.10.10.11:81", + Weight: 0, + }, + }, + }, + }, + }, + { + desc: "with all labels", + tasks: []state.Task{ + aTask("ID1", + withLabel(label.TraefikPort, "666"), + withLabel(label.TraefikProtocol, "https"), + withLabel(label.TraefikWeight, "12"), + + withLabel(label.TraefikBackend, "foobar"), + + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), + withLabel(label.TraefikBackendHealthCheckPath, "/health"), + withLabel(label.TraefikBackendHealthCheckPort, "880"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + withLabel(label.TraefikBackendBufferingMaxResponseBodyBytes, "10485760"), + withLabel(label.TraefikBackendBufferingMemResponseBodyBytes, "2097152"), + withLabel(label.TraefikBackendBufferingMaxRequestBodyBytes, "10485760"), + withLabel(label.TraefikBackendBufferingMemRequestBodyBytes, "2097152"), + withLabel(label.TraefikBackendBufferingRetryExpression, "IsNetworkError() && Attempts() <= 2"), + + withLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + withLabel(label.TraefikFrontendEntryPoints, "http,https"), + withLabel(label.TraefikFrontendPassHostHeader, "true"), + withLabel(label.TraefikFrontendPassTLSCert, "true"), + withLabel(label.TraefikFrontendPriority, "666"), + withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), + withLabel(label.TraefikFrontendRedirectRegex, "nope"), + withLabel(label.TraefikFrontendRedirectReplacement, "nope"), + withLabel(label.TraefikFrontendRedirectPermanent, "true"), + withLabel(label.TraefikFrontendRule, "Host:traefik.io"), + withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"), + withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"), + + withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type:application/json; charset=utf-8"), + withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type:application/json; charset=utf-8"), + withLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type:application/json; charset=utf-8"), + withLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor"), + withLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor"), + withLabel(label.TraefikFrontendSSLHost, "foo"), + withLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo"), + withLabel(label.TraefikFrontendContentSecurityPolicy, "foo"), + withLabel(label.TraefikFrontendPublicKey, "foo"), + withLabel(label.TraefikFrontendReferrerPolicy, "foo"), + withLabel(label.TraefikFrontendCustomBrowserXSSValue, "foo"), + withLabel(label.TraefikFrontendSTSSeconds, "666"), + withLabel(label.TraefikFrontendSSLRedirect, "true"), + withLabel(label.TraefikFrontendSSLTemporaryRedirect, "true"), + withLabel(label.TraefikFrontendSTSIncludeSubdomains, "true"), + withLabel(label.TraefikFrontendSTSPreload, "true"), + withLabel(label.TraefikFrontendForceSTSHeader, "true"), + withLabel(label.TraefikFrontendFrameDeny, "true"), + withLabel(label.TraefikFrontendContentTypeNosniff, "true"), + withLabel(label.TraefikFrontendBrowserXSSFilter, "true"), + withLabel(label.TraefikFrontendIsDevelopment, "true"), + + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageQuery, "foo_query"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageStatus, "500,600"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageQuery, "bar_query"), + + withLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), + withIP("10.10.10.10"), + withInfo("name1", withPorts( + withPortTCP(80, "n"), + withPortTCP(666, "n"))), + withStatus(withHealthy(true), withState("TASK_RUNNING")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-ID1": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backend-foobar", + Routes: map[string]types.Route{ + "route-host-ID1": { + Rule: "Host:traefik.io", + }, + }, + PassHostHeader: true, + Priority: 666, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "server-ID1": { + URL: "https://10.10.10.10:666", + Weight: 12, + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actualConfig := p.buildConfigurationV1(test.tasks) + + require.NotNil(t, actualConfig) + assert.Equal(t, test.expectedBackends, actualConfig.Backends) + assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} + +func TestTaskFilterV1(t *testing.T) { + testCases := []struct { + desc string + mesosTask state.Task + exposedByDefault bool + expected bool + }{ + { + desc: "no task", + mesosTask: state.Task{}, + exposedByDefault: true, + expected: false, + }, + { + desc: "task not healthy", + mesosTask: aTask("test", withStatus(withState("TASK_RUNNING"))), + exposedByDefault: true, + expected: false, + }, + { + desc: "exposedByDefault false and traefik.enable false", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "false"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: false, + expected: false, + }, + { + desc: "traefik.enable = true", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: false, + expected: true, + }, + { + desc: "exposedByDefault true and traefik.enable true", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: true, + expected: true, + }, + { + desc: "exposedByDefault true and traefik.enable false", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "false"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: true, + expected: false, + }, + { + desc: "traefik.portIndex and traefik.port both set", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withLabel(label.TraefikPortIndex, "1"), + withLabel(label.TraefikEnable, "80"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: true, + expected: false, + }, + { + desc: "valid traefik.portIndex", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withLabel(label.TraefikPortIndex, "1"), + withInfo("test", withPorts( + withPortTCP(80, "WEB"), + withPortTCP(443, "WEB HTTPS"), + )), + ), + exposedByDefault: true, + expected: true, + }, + { + desc: "default to first port index", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withInfo("test", withPorts( + withPortTCP(80, "WEB"), + withPortTCP(443, "WEB HTTPS"), + )), + ), + exposedByDefault: true, + expected: true, + }, + { + desc: "traefik.portIndex and discoveryPorts don't correspond", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withLabel(label.TraefikPortIndex, "1"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: true, + expected: false, + }, + { + desc: "traefik.portIndex and discoveryPorts correspond", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withLabel(label.TraefikPortIndex, "0"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: true, + expected: true, + }, + { + desc: "traefik.port is not an integer", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withLabel(label.TraefikPort, "TRAEFIK"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: true, + expected: false, + }, + { + desc: "traefik.port is not the same as discovery.port", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withLabel(label.TraefikPort, "443"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: true, + expected: false, + }, + { + desc: "traefik.port is the same as discovery.port", + mesosTask: aTask("test", + withDefaultStatus(), + withLabel(label.TraefikEnable, "true"), + withLabel(label.TraefikPort, "80"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: true, + expected: true, + }, + { + desc: "healthy nil", + mesosTask: aTask("test", + withStatus( + withState("TASK_RUNNING"), + ), + withLabel(label.TraefikEnable, "true"), + withLabel(label.TraefikPort, "80"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: true, + expected: true, + }, + { + desc: "healthy false", + mesosTask: aTask("test", + withStatus( + withState("TASK_RUNNING"), + withHealthy(false), + ), + withLabel(label.TraefikEnable, "true"), + withLabel(label.TraefikPort, "80"), + withInfo("test", withPorts(withPortTCP(80, "WEB"))), + ), + exposedByDefault: true, + expected: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := taskFilterV1(test.mesosTask, test.exposedByDefault) + ok := assert.Equal(t, test.expected, actual) + if !ok { + t.Logf("Statuses : %v", test.mesosTask.Statuses) + t.Logf("Label : %v", test.mesosTask.Labels) + t.Logf("DiscoveryInfo : %v", test.mesosTask.DiscoveryInfo) + t.Fatalf("Expected %v, got %v", test.expected, actual) + } + }) + } +} diff --git a/provider/mesos/mesos_helper_test.go b/provider/mesos/mesos_helper_test.go index f7ac6353a..0f30ab073 100644 --- a/provider/mesos/mesos_helper_test.go +++ b/provider/mesos/mesos_helper_test.go @@ -48,6 +48,14 @@ func TestBuilder(t *testing.T) { assert.Equal(t, expected, result) } +func aTaskData(id string, ops ...func(*state.Task)) taskData { + ts := &state.Task{ID: id} + for _, op := range ops { + op(ts) + } + return taskData{Task: *ts, TraefikLabels: extractLabels(*ts)} +} + func aTask(id string, ops ...func(*state.Task)) state.Task { ts := &state.Task{ID: id} for _, op := range ops { diff --git a/provider/mesos/mesos_test.go b/provider/mesos/mesos_test.go new file mode 100644 index 000000000..1459c27e3 --- /dev/null +++ b/provider/mesos/mesos_test.go @@ -0,0 +1,37 @@ +package mesos + +import ( + "testing" + + "github.com/mesos/mesos-go/upid" + "github.com/mesosphere/mesos-dns/records/state" +) + +func TestTaskRecords(t *testing.T) { + var task = state.Task{ + SlaveID: "s_id", + State: "TASK_RUNNING", + } + var framework = state.Framework{ + Tasks: []state.Task{task}, + } + var slave = state.Slave{ + ID: "s_id", + Hostname: "127.0.0.1", + } + slave.PID.UPID = &upid.UPID{} + slave.PID.Host = slave.Hostname + + var taskState = state.State{ + Slaves: []state.Slave{slave}, + Frameworks: []state.Framework{framework}, + } + + var p = taskRecords(taskState) + if len(p) == 0 { + t.Fatal("No task") + } + if p[0].SlaveIP != slave.Hostname { + t.Fatalf("The SlaveIP (%s) should be set with the slave hostname (%s)", p[0].SlaveID, slave.Hostname) + } +} diff --git a/templates/mesos-v1.tmpl b/templates/mesos-v1.tmpl new file mode 100644 index 000000000..3ec2990d9 --- /dev/null +++ b/templates/mesos-v1.tmpl @@ -0,0 +1,27 @@ +{{$apps := .Applications}} + +[backends] +{{range .Tasks}} + + [backends."backend-{{ getBackend . $apps }}".servers."server-{{ getID . }}"] + url = "{{ getProtocol . $apps }}://{{ getHost . }}:{{ getPort . $apps }}" + weight = {{ getWeight . $apps }} + +{{end}} + +[frontends] +{{range .Applications}} + + [frontends."frontend-{{getFrontEndName . }}"] + backend = "backend-{{ getFrontendBackend . }}" + passHostHeader = {{ getPassHostHeader . }} + priority = {{ getPriority . }} + + entryPoints = [{{range getEntryPoints . }} + "{{.}}", + {{end}}] + + [frontends."frontend-{{ getFrontEndName . }}".routes."route-host-{{ getFrontEndName . }}"] + rule = "{{ getFrontendRule . }}" + +{{end}} diff --git a/templates/mesos.tmpl b/templates/mesos.tmpl index e71cc238e..9ea840d7c 100644 --- a/templates/mesos.tmpl +++ b/templates/mesos.tmpl @@ -5,13 +5,13 @@ [backends."backend-{{ $backendName }}"] - {{ $circuitBreaker := getCircuitBreaker $app }} + {{ $circuitBreaker := getCircuitBreaker $app.TraefikLabels }} {{if $circuitBreaker }} [backends."backend-{{ $backendName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $app }} + {{ $loadBalancer := getLoadBalancer $app.TraefikLabels }} {{if $loadBalancer }} [backends."backend-{{ $backendName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -22,14 +22,14 @@ {{end}} {{end}} - {{ $maxConn := getMaxConn $app }} + {{ $maxConn := getMaxConn $app.TraefikLabels }} {{if $maxConn }} [backends."backend-{{ $backendName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $app }} + {{ $healthCheck := getHealthCheck $app.TraefikLabels }} {{if $healthCheck }} [backends."backend-{{ $backendName }}".healthCheck] path = "{{ $healthCheck.Path }}" @@ -37,7 +37,7 @@ interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $app }} + {{ $buffering := getBuffering $app.TraefikLabels }} {{if $buffering }} [backends."backend-{{ $backendName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -61,19 +61,19 @@ [frontends."frontend-{{ $frontendName }}"] backend = "backend-{{ getBackendName $app }}" - priority = {{ getPriority $app }} - passHostHeader = {{ getPassHostHeader $app }} - passTLSCert = {{ getPassTLSCert $app }} + priority = {{ getPriority $app.TraefikLabels }} + passHostHeader = {{ getPassHostHeader $app.TraefikLabels }} + passTLSCert = {{ getPassTLSCert $app.TraefikLabels }} - entryPoints = [{{range getEntryPoints $app }} + entryPoints = [{{range getEntryPoints $app.TraefikLabels }} "{{.}}", {{end}}] - basicAuth = [{{range getBasicAuth $app }} + basicAuth = [{{range getBasicAuth $app.TraefikLabels }} "{{.}}", {{end}}] - {{ $whitelist := getWhiteList $app }} + {{ $whitelist := getWhiteList $app.TraefikLabels }} {{if $whitelist }} [frontends."frontend-{{ $frontendName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} @@ -82,7 +82,7 @@ useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $app }} + {{ $redirect := getRedirect $app.TraefikLabels }} {{if $redirect }} [frontends."frontend-{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -91,7 +91,7 @@ permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $app }} + {{ $errorPages := getErrorPages $app.TraefikLabels }} {{if $errorPages }} [frontends."frontend-{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} @@ -104,7 +104,7 @@ {{end}} {{end}} - {{ $rateLimit := getRateLimit $app }} + {{ $rateLimit := getRateLimit $app.TraefikLabels }} {{if $rateLimit }} [frontends."frontend-{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" @@ -117,7 +117,7 @@ {{end}} {{end}} - {{ $headers := getHeaders $app }} + {{ $headers := getHeaders $app.TraefikLabels }} {{if $headers }} [frontends."frontend-{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} From 5c0b18efe459717cc7750a0720a03ee82cf26dfa Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 29 Mar 2018 00:01:35 +0200 Subject: [PATCH 17/21] chore: autogen. --- autogen/gentemplates/gen.go | 345 +++++++++++++++++++++++++++--------- 1 file changed, 265 insertions(+), 80 deletions(-) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index f64d7b52d..fc64ee49d 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -1,14 +1,17 @@ // Code generated by go-bindata. // sources: +// templates/consul_catalog-v1.tmpl // templates/consul_catalog.tmpl // templates/docker-v1.tmpl // templates/docker.tmpl +// templates/ecs-v1.tmpl // templates/ecs.tmpl // templates/eureka.tmpl // templates/kubernetes.tmpl // templates/kv.tmpl // templates/marathon-v1.tmpl // templates/marathon.tmpl +// templates/mesos-v1.tmpl // templates/mesos.tmpl // templates/notFound.tmpl // templates/rancher-v1.tmpl @@ -57,17 +60,90 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } +var _templatesConsul_catalogV1Tmpl = []byte(`[backends] +{{range $index, $node := .Nodes }} + [backends."backend-{{ getBackend $node }}".servers."{{ getBackendName $node $index }}"] + url = "{{ getAttribute "protocol" $node.Service.Tags "http" }}://{{ getBackendAddress $node }}:{{ $node.Service.Port }}" + {{ $weight := getAttribute "backend.weight" $node.Service.Tags "0" }} + {{with $weight }} + weight = {{ $weight }} + {{end}} +{{end}} + +{{range .Services }} + {{ $service := .ServiceName }} + + {{ $circuitBreaker := getAttribute "backend.circuitbreaker" .Attributes "" }} + {{with $circuitBreaker }} + [backends."backend-{{ $service }}".circuitbreaker] + expression = "{{ $circuitBreaker }}" + {{end}} + + [backends."backend-{{ $service }}".loadbalancer] + method = "{{ getAttribute "backend.loadbalancer" .Attributes "wrr" }}" + sticky = {{ getSticky .Attributes }} + {{if hasStickinessLabel .Attributes }} + [backends."backend-{{ $service }}".loadbalancer.stickiness] + cookieName = "{{ getStickinessCookieName .Attributes }}" + {{end}} + + {{if hasMaxconnAttributes .Attributes }} + [backends."backend-{{ $service }}".maxconn] + amount = {{ getAttribute "backend.maxconn.amount" .Attributes "" }} + extractorfunc = "{{ getAttribute "backend.maxconn.extractorfunc" .Attributes "" }}" + {{end}} + +{{end}} + +[frontends] +{{range .Services }} + [frontends."frontend-{{ .ServiceName }}"] + backend = "backend-{{ .ServiceName }}" + passHostHeader = {{ getAttribute "frontend.passHostHeader" .Attributes "true" }} + priority = {{ getAttribute "frontend.priority" .Attributes "0" }} + + {{ $entryPoints := getAttribute "frontend.entrypoints" .Attributes "" }} + {{with $entryPoints }} + entrypoints = [{{range getEntryPoints $entryPoints }} + "{{ . }}", + {{end}}] + {{end}} + + basicAuth = [{{range getBasicAuth .Attributes }} + "{{ . }}", + {{end}}] + + [frontends."frontend-{{ .ServiceName }}".routes."route-host-{{ .ServiceName }}"] + rule = "{{ getFrontendRule . }}" +{{end}} +`) + +func templatesConsul_catalogV1TmplBytes() ([]byte, error) { + return _templatesConsul_catalogV1Tmpl, nil +} + +func templatesConsul_catalogV1Tmpl() (*asset, error) { + bytes, err := templatesConsul_catalogV1TmplBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "templates/consul_catalog-v1.tmpl", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _templatesConsul_catalogTmpl = []byte(`[backends] {{range $service := .Services}} {{ $backendName := getServiceBackendName $service }} - {{ $circuitBreaker := getCircuitBreaker $service.Attributes }} + {{ $circuitBreaker := getCircuitBreaker $service.TraefikLabels }} {{if $circuitBreaker }} [backends."backend-{{ $backendName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $service.Attributes }} + {{ $loadBalancer := getLoadBalancer $service.TraefikLabels }} {{if $loadBalancer }} [backends."backend-{{ $backendName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -78,14 +154,14 @@ var _templatesConsul_catalogTmpl = []byte(`[backends] {{end}} {{end}} - {{ $maxConn := getMaxConn $service.Attributes }} + {{ $maxConn := getMaxConn $service.TraefikLabels }} {{if $maxConn }} [backends."backend-{{ $backendName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $service.Attributes }} + {{ $healthCheck := getHealthCheck $service.TraefikLabels }} {{if $healthCheck }} [backends."backend-{{ $backendName }}".healthCheck] path = "{{ $healthCheck.Path }}" @@ -93,7 +169,7 @@ var _templatesConsul_catalogTmpl = []byte(`[backends] interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $service.Attributes }} + {{ $buffering := getBuffering $service.TraefikLabels }} {{if $buffering }} [backends."backend-{{ $backendName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -105,10 +181,10 @@ var _templatesConsul_catalogTmpl = []byte(`[backends] {{end}} {{range $index, $node := .Nodes}} - + {{ $server := getServer $node }} [backends."backend-{{ getNodeBackendName $node }}".servers."{{ getServerName $node $index }}"] - url = "{{ getProtocol $node.Service.Tags }}://{{ getBackendAddress $node }}:{{ $node.Service.Port }}" - weight = {{ getWeight $node.Service.Tags }} + url = "{{ $server.URL }}" + weight = {{ $server.Weight }} {{end}} @@ -117,19 +193,19 @@ var _templatesConsul_catalogTmpl = []byte(`[backends] [frontends."frontend-{{ $service.ServiceName }}"] backend = "backend-{{ getServiceBackendName $service }}" - priority = {{ getPriority $service.Attributes }} - passHostHeader = {{ getPassHostHeader $service.Attributes }} - passTLSCert = {{ getPassTLSCert $service.Attributes }} + priority = {{ getPriority $service.TraefikLabels }} + passHostHeader = {{ getPassHostHeader $service.TraefikLabels }} + passTLSCert = {{ getPassTLSCert $service.TraefikLabels }} - entryPoints = [{{range getFrontEndEntryPoints $service.Attributes }} + entryPoints = [{{range getFrontEndEntryPoints $service.TraefikLabels }} "{{.}}", {{end}}] - basicAuth = [{{range getBasicAuth $service.Attributes }} + basicAuth = [{{range getBasicAuth $service.TraefikLabels }} "{{.}}", {{end}}] - {{ $whitelist := getWhiteList $service.Attributes }} + {{ $whitelist := getWhiteList $service.TraefikLabels }} {{if $whitelist }} [frontends."frontend-{{ $service.ServiceName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} @@ -138,7 +214,7 @@ var _templatesConsul_catalogTmpl = []byte(`[backends] useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $service.Attributes }} + {{ $redirect := getRedirect $service.TraefikLabels }} {{if $redirect }} [frontends."frontend-{{ $service.ServiceName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -147,9 +223,10 @@ var _templatesConsul_catalogTmpl = []byte(`[backends] permanent = {{ $redirect.Permanent }} {{end}} - {{if hasErrorPages $service.Attributes }} + {{ $errorPages := getErrorPages $service.TraefikLabels }} + {{if $errorPages }} [frontends."frontend-{{ $service.ServiceName }}".errors] - {{range $pageName, $page := getErrorPages $service.Attributes }} + {{range $pageName, $page := $errorPages }} [frontends."frontend-{{ $service.ServiceName }}".errors."{{ $pageName }}"] status = [{{range $page.Status }} "{{.}}", @@ -159,22 +236,20 @@ var _templatesConsul_catalogTmpl = []byte(`[backends] {{end}} {{end}} - {{if hasRateLimit $service.Attributes }} - {{ $rateLimit := getRateLimit $service.Attributes }} + {{ $rateLimit := getRateLimit $service.TraefikLabels }} + {{if $rateLimit }} [frontends."frontend-{{ $service.ServiceName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" - [frontends."frontend-{{ $service.ServiceName }}".rateLimit.rateSet] - {{range $limitName, $limit := $rateLimit.RateSet }} + {{ range $limitName, $limit := $rateLimit.RateSet }} [frontends."frontend-{{ $service.ServiceName }}".rateLimit.rateSet."{{ $limitName }}"] period = "{{ $limit.Period }}" average = {{ $limit.Average }} burst = {{ $limit.Burst }} {{end}} - {{end}} - {{ $headers := getHeaders $service.Attributes }} + {{ $headers := getHeaders $service.TraefikLabels }} {{if $headers }} [frontends."frontend-{{ $service.ServiceName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} @@ -651,17 +726,77 @@ func templatesDockerTmpl() (*asset, error) { return a, nil } +var _templatesEcsV1Tmpl = []byte(`[backends] +{{range $serviceName, $instances := .Services }} + [backends."backend-{{ $serviceName }}".loadBalancer] + method = "{{ getLoadBalancerMethod $instances }}" + sticky = {{ getLoadBalancerSticky $instances }} + + {{if hasStickinessLabel $instances }} + [backends."backend-{{ $serviceName }}".loadBalancer.stickiness] + cookieName = "{{ getStickinessCookieName $instances }}" + {{end}} + + {{ if hasHealthCheckLabels $instances }} + [backends."backend-{{ $serviceName }}".healthCheck] + path = "{{ getHealthCheckPath $instances }}" + interval = "{{ getHealthCheckInterval $instances }}" + {{end}} + + {{range $index, $i := $instances }} + [backends."backend-{{ $i.Name }}".servers."server-{{ $i.Name }}{{ $i.ID }}"] + url = "{{ getProtocol $i }}://{{ getHost $i }}:{{ getPort $i }}" + weight = {{ getWeight $i }} + {{end}} +{{end}} + +[frontends] +{{range $serviceName, $instances := .Services}} +{{range filterFrontends $instances }} + [frontends."frontend-{{ $serviceName }}"] + backend = "backend-{{ $serviceName }}" + passHostHeader = {{ getPassHostHeader . }} + priority = {{ getPriority . }} + + entryPoints = [{{range getEntryPoints . }} + "{{.}}", + {{end}}] + + basicAuth = [{{range getBasicAuth . }} + "{{.}}", + {{end}}] + + [frontends."frontend-{{ $serviceName }}".routes."route-frontend-{{ $serviceName }}"] + rule = "{{getFrontendRule .}}" +{{end}} +{{end}}`) + +func templatesEcsV1TmplBytes() ([]byte, error) { + return _templatesEcsV1Tmpl, nil +} + +func templatesEcsV1Tmpl() (*asset, error) { + bytes, err := templatesEcsV1TmplBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "templates/ecs-v1.tmpl", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _templatesEcsTmpl = []byte(`[backends] {{range $serviceName, $instances := .Services }} {{ $firstInstance := index $instances 0 }} - {{ $circuitBreaker := getCircuitBreaker $firstInstance }} + {{ $circuitBreaker := getCircuitBreaker $firstInstance.TraefikLabels }} {{if $circuitBreaker }} [backends."backend-{{ $serviceName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $firstInstance }} + {{ $loadBalancer := getLoadBalancer $firstInstance.TraefikLabels }} {{if $loadBalancer }} [backends."backend-{{ $serviceName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -672,14 +807,14 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $maxConn := getMaxConn $firstInstance }} + {{ $maxConn := getMaxConn $firstInstance.TraefikLabels }} {{if $maxConn }} [backends."backend-{{ $serviceName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $firstInstance }} + {{ $healthCheck := getHealthCheck $firstInstance.TraefikLabels }} {{if $healthCheck }} [backends."backend-{{ $serviceName }}".healthCheck] path = "{{ $healthCheck.Path }}" @@ -687,7 +822,7 @@ var _templatesEcsTmpl = []byte(`[backends] interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $firstInstance }} + {{ $buffering := getBuffering $firstInstance.TraefikLabels }} {{if $buffering }} [backends."backend-{{ $serviceName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -711,19 +846,19 @@ var _templatesEcsTmpl = []byte(`[backends] [frontends."frontend-{{ $serviceName }}"] backend = "backend-{{ $serviceName }}" - priority = {{ getPriority $instance }} - passHostHeader = {{ getPassHostHeader $instance }} - passTLSCert = {{ getPassTLSCert $instance }} + priority = {{ getPriority $instance.TraefikLabels }} + passHostHeader = {{ getPassHostHeader $instance.TraefikLabels }} + passTLSCert = {{ getPassTLSCert $instance.TraefikLabels }} - entryPoints = [{{range getEntryPoints $instance }} + entryPoints = [{{range getEntryPoints $instance.TraefikLabels }} "{{.}}", {{end}}] - basicAuth = [{{range getBasicAuth $instance }} + basicAuth = [{{range getBasicAuth $instance.TraefikLabels }} "{{.}}", {{end}}] - {{ $whitelist := getWhiteList $instance }} + {{ $whitelist := getWhiteList $instance.TraefikLabels }} {{if $whitelist }} [frontends."frontend-{{ $serviceName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} @@ -732,7 +867,7 @@ var _templatesEcsTmpl = []byte(`[backends] useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $instance }} + {{ $redirect := getRedirect $instance.TraefikLabels }} {{if $redirect }} [frontends."frontend-{{ $serviceName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -741,7 +876,7 @@ var _templatesEcsTmpl = []byte(`[backends] permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $instance }} + {{ $errorPages := getErrorPages $instance.TraefikLabels }} {{if $errorPages }} [frontends."frontend-{{ $serviceName }}".errors] {{range $pageName, $page := $errorPages }} @@ -754,7 +889,7 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $rateLimit := getRateLimit $instance }} + {{ $rateLimit := getRateLimit $instance.TraefikLabels }} {{if $rateLimit }} [frontends."frontend-{{ $serviceName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" @@ -767,7 +902,7 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $headers := getHeaders $instance }} + {{ $headers := getHeaders $instance.TraefikLabels }} {{if $headers }} [frontends."frontend-{{ $serviceName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} @@ -822,7 +957,7 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} [frontends."frontend-{{ $serviceName }}".routes."route-frontend-{{ $serviceName }}"] - rule = "{{getFrontendRule $instance}}" + rule = "{{ getFrontendRule $instance }}" {{end}} {{end}}`) @@ -1547,6 +1682,50 @@ func templatesMarathonTmpl() (*asset, error) { return a, nil } +var _templatesMesosV1Tmpl = []byte(`{{$apps := .Applications}} + +[backends] +{{range .Tasks}} + + [backends."backend-{{ getBackend . $apps }}".servers."server-{{ getID . }}"] + url = "{{ getProtocol . $apps }}://{{ getHost . }}:{{ getPort . $apps }}" + weight = {{ getWeight . $apps }} + +{{end}} + +[frontends] +{{range .Applications}} + + [frontends."frontend-{{getFrontEndName . }}"] + backend = "backend-{{ getFrontendBackend . }}" + passHostHeader = {{ getPassHostHeader . }} + priority = {{ getPriority . }} + + entryPoints = [{{range getEntryPoints . }} + "{{.}}", + {{end}}] + + [frontends."frontend-{{ getFrontEndName . }}".routes."route-host-{{ getFrontEndName . }}"] + rule = "{{ getFrontendRule . }}" + +{{end}} +`) + +func templatesMesosV1TmplBytes() ([]byte, error) { + return _templatesMesosV1Tmpl, nil +} + +func templatesMesosV1Tmpl() (*asset, error) { + bytes, err := templatesMesosV1TmplBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "templates/mesos-v1.tmpl", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _templatesMesosTmpl = []byte(`[backends] {{range $applicationName, $tasks := .ApplicationsTasks }} {{ $app := index $tasks 0 }} @@ -1554,13 +1733,13 @@ var _templatesMesosTmpl = []byte(`[backends] [backends."backend-{{ $backendName }}"] - {{ $circuitBreaker := getCircuitBreaker $app }} + {{ $circuitBreaker := getCircuitBreaker $app.TraefikLabels }} {{if $circuitBreaker }} [backends."backend-{{ $backendName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $app }} + {{ $loadBalancer := getLoadBalancer $app.TraefikLabels }} {{if $loadBalancer }} [backends."backend-{{ $backendName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -1571,14 +1750,14 @@ var _templatesMesosTmpl = []byte(`[backends] {{end}} {{end}} - {{ $maxConn := getMaxConn $app }} + {{ $maxConn := getMaxConn $app.TraefikLabels }} {{if $maxConn }} [backends."backend-{{ $backendName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $app }} + {{ $healthCheck := getHealthCheck $app.TraefikLabels }} {{if $healthCheck }} [backends."backend-{{ $backendName }}".healthCheck] path = "{{ $healthCheck.Path }}" @@ -1586,7 +1765,7 @@ var _templatesMesosTmpl = []byte(`[backends] interval = "{{ $healthCheck.Interval }}" {{end}} - {{ $buffering := getBuffering $app }} + {{ $buffering := getBuffering $app.TraefikLabels }} {{if $buffering }} [backends."backend-{{ $backendName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -1610,19 +1789,19 @@ var _templatesMesosTmpl = []byte(`[backends] [frontends."frontend-{{ $frontendName }}"] backend = "backend-{{ getBackendName $app }}" - priority = {{ getPriority $app }} - passHostHeader = {{ getPassHostHeader $app }} - passTLSCert = {{ getPassTLSCert $app }} + priority = {{ getPriority $app.TraefikLabels }} + passHostHeader = {{ getPassHostHeader $app.TraefikLabels }} + passTLSCert = {{ getPassTLSCert $app.TraefikLabels }} - entryPoints = [{{range getEntryPoints $app }} + entryPoints = [{{range getEntryPoints $app.TraefikLabels }} "{{.}}", {{end}}] - basicAuth = [{{range getBasicAuth $app }} + basicAuth = [{{range getBasicAuth $app.TraefikLabels }} "{{.}}", {{end}}] - {{ $whitelist := getWhiteList $app }} + {{ $whitelist := getWhiteList $app.TraefikLabels }} {{if $whitelist }} [frontends."frontend-{{ $frontendName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} @@ -1631,7 +1810,7 @@ var _templatesMesosTmpl = []byte(`[backends] useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $app }} + {{ $redirect := getRedirect $app.TraefikLabels }} {{if $redirect }} [frontends."frontend-{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" @@ -1640,7 +1819,7 @@ var _templatesMesosTmpl = []byte(`[backends] permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $app }} + {{ $errorPages := getErrorPages $app.TraefikLabels }} {{if $errorPages }} [frontends."frontend-{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} @@ -1653,7 +1832,7 @@ var _templatesMesosTmpl = []byte(`[backends] {{end}} {{end}} - {{ $rateLimit := getRateLimit $app }} + {{ $rateLimit := getRateLimit $app.TraefikLabels }} {{if $rateLimit }} [frontends."frontend-{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" @@ -1666,7 +1845,7 @@ var _templatesMesosTmpl = []byte(`[backends] {{end}} {{end}} - {{ $headers := getHeaders $app }} + {{ $headers := getHeaders $app.TraefikLabels }} {{if $headers }} [frontends."frontend-{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} @@ -2084,19 +2263,22 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ - "templates/consul_catalog.tmpl": templatesConsul_catalogTmpl, - "templates/docker-v1.tmpl": templatesDockerV1Tmpl, - "templates/docker.tmpl": templatesDockerTmpl, - "templates/ecs.tmpl": templatesEcsTmpl, - "templates/eureka.tmpl": templatesEurekaTmpl, - "templates/kubernetes.tmpl": templatesKubernetesTmpl, - "templates/kv.tmpl": templatesKvTmpl, - "templates/marathon-v1.tmpl": templatesMarathonV1Tmpl, - "templates/marathon.tmpl": templatesMarathonTmpl, - "templates/mesos.tmpl": templatesMesosTmpl, - "templates/notFound.tmpl": templatesNotfoundTmpl, - "templates/rancher-v1.tmpl": templatesRancherV1Tmpl, - "templates/rancher.tmpl": templatesRancherTmpl, + "templates/consul_catalog-v1.tmpl": templatesConsul_catalogV1Tmpl, + "templates/consul_catalog.tmpl": templatesConsul_catalogTmpl, + "templates/docker-v1.tmpl": templatesDockerV1Tmpl, + "templates/docker.tmpl": templatesDockerTmpl, + "templates/ecs-v1.tmpl": templatesEcsV1Tmpl, + "templates/ecs.tmpl": templatesEcsTmpl, + "templates/eureka.tmpl": templatesEurekaTmpl, + "templates/kubernetes.tmpl": templatesKubernetesTmpl, + "templates/kv.tmpl": templatesKvTmpl, + "templates/marathon-v1.tmpl": templatesMarathonV1Tmpl, + "templates/marathon.tmpl": templatesMarathonTmpl, + "templates/mesos-v1.tmpl": templatesMesosV1Tmpl, + "templates/mesos.tmpl": templatesMesosTmpl, + "templates/notFound.tmpl": templatesNotfoundTmpl, + "templates/rancher-v1.tmpl": templatesRancherV1Tmpl, + "templates/rancher.tmpl": templatesRancherTmpl, } // AssetDir returns the file names below a certain @@ -2141,19 +2323,22 @@ type bintree struct { var _bintree = &bintree{nil, map[string]*bintree{ "templates": {nil, map[string]*bintree{ - "consul_catalog.tmpl": {templatesConsul_catalogTmpl, map[string]*bintree{}}, - "docker-v1.tmpl": {templatesDockerV1Tmpl, map[string]*bintree{}}, - "docker.tmpl": {templatesDockerTmpl, map[string]*bintree{}}, - "ecs.tmpl": {templatesEcsTmpl, map[string]*bintree{}}, - "eureka.tmpl": {templatesEurekaTmpl, map[string]*bintree{}}, - "kubernetes.tmpl": {templatesKubernetesTmpl, map[string]*bintree{}}, - "kv.tmpl": {templatesKvTmpl, map[string]*bintree{}}, - "marathon-v1.tmpl": {templatesMarathonV1Tmpl, map[string]*bintree{}}, - "marathon.tmpl": {templatesMarathonTmpl, map[string]*bintree{}}, - "mesos.tmpl": {templatesMesosTmpl, map[string]*bintree{}}, - "notFound.tmpl": {templatesNotfoundTmpl, map[string]*bintree{}}, - "rancher-v1.tmpl": {templatesRancherV1Tmpl, map[string]*bintree{}}, - "rancher.tmpl": {templatesRancherTmpl, map[string]*bintree{}}, + "consul_catalog-v1.tmpl": {templatesConsul_catalogV1Tmpl, map[string]*bintree{}}, + "consul_catalog.tmpl": {templatesConsul_catalogTmpl, map[string]*bintree{}}, + "docker-v1.tmpl": {templatesDockerV1Tmpl, map[string]*bintree{}}, + "docker.tmpl": {templatesDockerTmpl, map[string]*bintree{}}, + "ecs-v1.tmpl": {templatesEcsV1Tmpl, map[string]*bintree{}}, + "ecs.tmpl": {templatesEcsTmpl, map[string]*bintree{}}, + "eureka.tmpl": {templatesEurekaTmpl, map[string]*bintree{}}, + "kubernetes.tmpl": {templatesKubernetesTmpl, map[string]*bintree{}}, + "kv.tmpl": {templatesKvTmpl, map[string]*bintree{}}, + "marathon-v1.tmpl": {templatesMarathonV1Tmpl, map[string]*bintree{}}, + "marathon.tmpl": {templatesMarathonTmpl, map[string]*bintree{}}, + "mesos-v1.tmpl": {templatesMesosV1Tmpl, map[string]*bintree{}}, + "mesos.tmpl": {templatesMesosTmpl, map[string]*bintree{}}, + "notFound.tmpl": {templatesNotfoundTmpl, map[string]*bintree{}}, + "rancher-v1.tmpl": {templatesRancherV1Tmpl, map[string]*bintree{}}, + "rancher.tmpl": {templatesRancherTmpl, map[string]*bintree{}}, }}, }} From 6845068b8276b8dd0f39f5f2ed4a1f193bc5e090 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 29 Mar 2018 00:18:50 +0200 Subject: [PATCH 18/21] doc: template version for ECS, Consul Catalog and Mesos. --- docs/configuration/backends/consulcatalog.md | 16 ++++++++++++++++ docs/configuration/backends/ecs.md | 9 +++++++++ docs/configuration/backends/mesos.md | 20 +++++++++++++++----- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/docs/configuration/backends/consulcatalog.md b/docs/configuration/backends/consulcatalog.md index 94b56450a..de6a01259 100644 --- a/docs/configuration/backends/consulcatalog.md +++ b/docs/configuration/backends/consulcatalog.md @@ -58,6 +58,22 @@ prefix = "traefik" # cert = "/etc/ssl/consul.crt" # key = "/etc/ssl/consul.key" # insecureskipverify = true + +# Override default configuration template. +# For advanced users :) +# +# Optional +# +# filename = "consulcatalog.tmpl" + +# Override template version +# For advanced users :) +# +# Optional +# - "1": previous template version (must be used only with older custom templates, see "filename") +# - "2": current template version (must be used to force template version when "filename" is used) +# +# templateVersion = "2" ``` This backend will create routes matching on hostname based on the service name used in Consul. diff --git a/docs/configuration/backends/ecs.md b/docs/configuration/backends/ecs.md index ef2278f29..e08b8f254 100644 --- a/docs/configuration/backends/ecs.md +++ b/docs/configuration/backends/ecs.md @@ -84,6 +84,15 @@ secretAccessKey = "123" # Optional # # filename = "ecs.tmpl" + +# Override template version +# For advanced users :) +# +# Optional +# - "1": previous template version (must be used only with older custom templates, see "filename") +# - "2": current template version (must be used to force template version when "filename" is used) +# +# templateVersion = "2" ``` If `AccessKeyID`/`SecretAccessKey` is not given credentials will be resolved in the following order: diff --git a/docs/configuration/backends/mesos.md b/docs/configuration/backends/mesos.md index 229418729..9fd465fd8 100644 --- a/docs/configuration/backends/mesos.md +++ b/docs/configuration/backends/mesos.md @@ -34,6 +34,13 @@ watch = true # domain = "mesos.localhost" +# Expose Mesos apps by default in Traefik. +# +# Optional +# Default: true +# +# exposedByDefault = false + # Override default configuration template. # For advanced users :) # @@ -41,12 +48,14 @@ domain = "mesos.localhost" # # filename = "mesos.tmpl" -# Expose Mesos apps by default in Traefik. +# Override template version +# For advanced users :) # # Optional -# Default: true +# - "1": previous template version (must be used only with older custom templates, see "filename") +# - "2": current template version (must be used to force template version when "filename" is used) # -# ExposedByDefault = false +# templateVersion = "2" # TLS client configuration. https://golang.org/pkg/crypto/tls/#Config # @@ -90,11 +99,12 @@ domain = "mesos.localhost" # Default: false # # groupsAsSubDomains = true + ``` -## Labels: overriding default behaviour +## Labels: overriding default behavior -The following labels can be defined on Mesos tasks. They adjust the behaviour for the entire application. +The following labels can be defined on Mesos tasks. They adjust the behavior for the entire application. | Label | Description | |------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| From 88b71d23db163f905a8a8182cc04f72a74c52171 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 4 Apr 2018 12:28:03 +0200 Subject: [PATCH 19/21] Several apps with same backend name in Marathon. --- autogen/gentemplates/gen.go | 7 +- provider/marathon/builder_test.go | 21 +- provider/marathon/config.go | 79 ++- provider/marathon/config_test.go | 501 +++++++++++--------- provider/marathon/deprecated_config_test.go | 30 +- templates/marathon.tmpl | 7 +- 6 files changed, 367 insertions(+), 278 deletions(-) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index fc64ee49d..7d129c2a7 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -1490,8 +1490,7 @@ func templatesMarathonV1Tmpl() (*asset, error) { var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} [backends] -{{range $app := $apps }} - {{ $backendName := getBackendName $app }} +{{range $backendName, $app := $apps }} [backends."{{ $backendName }}"] @@ -1546,11 +1545,11 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} {{end}} [frontends] -{{range $app := $apps }} +{{range $backendName, $app := $apps }} {{ $frontendName := getFrontendName $app }} [frontends."{{ $frontendName }}"] - backend = "{{ getBackendName $app }}" + backend = "{{ $backendName }}" priority = {{ getPriority $app.SegmentLabels }} passHostHeader = {{ getPassHostHeader $app.SegmentLabels }} passTLSCert = {{ getPassTLSCert $app.SegmentLabels }} diff --git a/provider/marathon/builder_test.go b/provider/marathon/builder_test.go index 0644c6848..45f6e5dbd 100644 --- a/provider/marathon/builder_test.go +++ b/provider/marathon/builder_test.go @@ -54,14 +54,14 @@ func constraint(value string) func(*marathon.Application) { } } -func withServiceLabel(key, value string, serviceName string) func(*marathon.Application) { - if len(serviceName) == 0 { - panic("serviceName can not be empty") +func withSegmentLabel(key, value string, segmentName string) func(*marathon.Application) { + if len(segmentName) == 0 { + panic("segmentName can not be empty") } property := strings.TrimPrefix(key, label.Prefix) return func(app *marathon.Application) { - app.AddLabel(label.Prefix+serviceName+"."+property, value) + app.AddLabel(label.Prefix+segmentName+"."+property, value) } } @@ -152,6 +152,7 @@ func localhostTask(ops ...func(*marathon.Task)) marathon.Task { t := task( host("localhost"), ipAddresses("127.0.0.1"), + taskState(taskStateRunning), ) for _, op := range ops { @@ -167,6 +168,12 @@ func taskPorts(ports ...int) func(*marathon.Task) { } } +func taskState(state TaskState) func(*marathon.Task) { + return func(t *marathon.Task) { + t.State = string(state) + } +} + func host(h string) func(*marathon.Task) { return func(t *marathon.Task) { t.Host = h @@ -184,12 +191,6 @@ func ipAddresses(addresses ...string) func(*marathon.Task) { } } -func state(s TaskState) func(*marathon.Task) { - return func(t *marathon.Task) { - t.State = string(s) - } -} - func startedAt(timestamp string) func(*marathon.Task) { return func(t *marathon.Task) { t.StartedAt = timestamp diff --git a/provider/marathon/config.go b/provider/marathon/config.go index 4842d1e5d..b9c1c9259 100644 --- a/provider/marathon/config.go +++ b/provider/marathon/config.go @@ -21,6 +21,7 @@ type appData struct { marathon.Application SegmentLabels map[string]string SegmentName string + LinkedApps []*appData } func (p *Provider) buildConfigurationV2(applications *marathon.Applications) *types.Configuration { @@ -54,7 +55,7 @@ func (p *Provider) buildConfigurationV2(applications *marathon.Applications) *ty "getWhiteList": label.GetWhiteList, } - var apps []*appData + apps := make(map[string]*appData) for _, app := range applications.Apps { if p.applicationFilter(app) { // Tasks @@ -66,10 +67,6 @@ func (p *Provider) buildConfigurationV2(applications *marathon.Applications) *ty } } - if len(filteredTasks) == 0 { - log.Warnf("No valid tasks for application %s", app.ID) - continue - } app.Tasks = filteredTasks // segments @@ -80,13 +77,19 @@ func (p *Provider) buildConfigurationV2(applications *marathon.Applications) *ty SegmentLabels: labels, SegmentName: segmentName, } - apps = append(apps, data) + + backendName := p.getBackendName(*data) + if baseApp, ok := apps[backendName]; ok { + baseApp.LinkedApps = append(baseApp.LinkedApps, data) + } else { + apps[backendName] = data + } } } } templateObjects := struct { - Applications []*appData + Applications map[string]*appData Domain string }{ Applications: apps, @@ -181,11 +184,11 @@ func (p *Provider) getSubDomain(name string) string { } func (p *Provider) getBackendName(app appData) string { - value := label.GetStringValue(app.SegmentLabels, label.TraefikBackend, "") if len(value) > 0 { return provider.Normalize("backend" + value) } + return provider.Normalize("backend" + app.ID + getSegmentNameSuffix(app.SegmentName)) } @@ -291,8 +294,9 @@ func (p *Provider) getServers(app appData) map[string]types.Server { var servers map[string]types.Server for _, task := range app.Tasks { - host := p.getBackendServer(*task, app) - if len(host) == 0 { + name, server, err := p.getServer(app, *task) + if err != nil { + log.Error(err) continue } @@ -300,45 +304,68 @@ func (p *Provider) getServers(app appData) map[string]types.Server { servers = make(map[string]types.Server) } - port := getPort(*task, app) - protocol := label.GetStringValue(app.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol) + servers[name] = *server + } - serverName := provider.Normalize("server-" + task.ID + getSegmentNameSuffix(app.SegmentName)) - servers[serverName] = types.Server{ - URL: fmt.Sprintf("%s://%s:%v", protocol, host, port), - Weight: label.GetIntValue(app.SegmentLabels, label.TraefikWeight, label.DefaultWeightInt), + for _, linkedApp := range app.LinkedApps { + for _, task := range linkedApp.Tasks { + name, server, err := p.getServer(*linkedApp, *task) + if err != nil { + log.Error(err) + continue + } + + if servers == nil { + servers = make(map[string]types.Server) + } + + servers[name] = *server } } return servers } -func (p *Provider) getBackendServer(task marathon.Task, app appData) string { +func (p *Provider) getServer(app appData, task marathon.Task) (string, *types.Server, error) { + host, err := p.getServerHost(task, app) + if len(host) == 0 { + return "", nil, err + } + + port := getPort(task, app) + protocol := label.GetStringValue(app.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol) + + serverName := provider.Normalize("server-" + app.ID + "-" + task.ID + getSegmentNameSuffix(app.SegmentName)) + + return serverName, &types.Server{ + URL: fmt.Sprintf("%s://%s:%v", protocol, host, port), + Weight: label.GetIntValue(app.SegmentLabels, label.TraefikWeight, label.DefaultWeightInt), + }, nil +} + +func (p *Provider) getServerHost(task marathon.Task, app appData) (string, error) { if app.IPAddressPerTask == nil || p.ForceTaskHostname { - return task.Host + return task.Host, nil } numTaskIPAddresses := len(task.IPAddresses) switch numTaskIPAddresses { case 0: - log.Errorf("Missing IP address for Marathon application %s on task %s", app.ID, task.ID) - return "" + return "", fmt.Errorf("missing IP address for Marathon application %s on task %s", app.ID, task.ID) case 1: - return task.IPAddresses[0].IPAddress + return task.IPAddresses[0].IPAddress, nil default: ipAddressIdx := label.GetIntValue(stringValueMap(app.Labels), labelIPAddressIdx, math.MinInt32) if ipAddressIdx == math.MinInt32 { - log.Errorf("Found %d task IP addresses but missing IP address index for Marathon application %s on task %s", + return "", fmt.Errorf("found %d task IP addresses but missing IP address index for Marathon application %s on task %s", numTaskIPAddresses, app.ID, task.ID) - return "" } if ipAddressIdx < 0 || ipAddressIdx > numTaskIPAddresses { - log.Errorf("Cannot use IP address index to select from %d task IP addresses for Marathon application %s on task %s", + return "", fmt.Errorf("cannot use IP address index to select from %d task IP addresses for Marathon application %s on task %s", numTaskIPAddresses, app.ID, task.ID) - return "" } - return task.IPAddresses[ipAddressIdx].IPAddress + return task.IPAddresses[ipAddressIdx].IPAddress, nil } } diff --git a/provider/marathon/config_test.go b/provider/marathon/config_test.go index ef92573d9..a6f5ac6d1 100644 --- a/provider/marathon/config_test.go +++ b/provider/marathon/config_test.go @@ -30,22 +30,24 @@ func TestGetConfigurationAPIErrors(t *testing.T) { func TestBuildConfiguration(t *testing.T) { testCases := []struct { desc string - application marathon.Application + applications *marathon.Applications expectedFrontends map[string]*types.Frontend expectedBackends map[string]*types.Backend }{ { desc: "simple application", - application: application( - appPorts(80), - withTasks(localhostTask(taskPorts(80))), - ), + applications: withApplications( + application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80))), + )), expectedFrontends: map[string]*types.Frontend{ "frontend-app": { Backend: "backend-app", Routes: map[string]types.Route{ "route-host-app": { - Rule: "Host:app.docker.localhost", + Rule: "Host:app.marathon.localhost", }, }, PassHostHeader: true, @@ -56,7 +58,7 @@ func TestBuildConfiguration(t *testing.T) { expectedBackends: map[string]*types.Backend{ "backend-app": { Servers: map[string]types.Server{ - "server-task": { + "server-app-taskID": { URL: "http://localhost:80", Weight: 0, }, @@ -67,27 +69,44 @@ func TestBuildConfiguration(t *testing.T) { }, { desc: "filtered task", - application: application( - appPorts(80), - withTasks(localhostTask(taskPorts(80), state(taskStateStaging))), - ), - expectedFrontends: map[string]*types.Frontend{}, - expectedBackends: map[string]*types.Backend{}, - }, - { - desc: "max connection extractor function label only", - application: application( - appPorts(80), - withTasks(localhostTask(taskPorts(80))), - - withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - ), + applications: withApplications( + application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80), taskState(taskStateStaging))), + )), expectedFrontends: map[string]*types.Frontend{ "frontend-app": { Backend: "backend-app", Routes: map[string]types.Route{ "route-host-app": { - Rule: "Host:app.docker.localhost", + Rule: "Host:app.marathon.localhost", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + EntryPoints: []string{}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-app": {}, + }, + }, + { + desc: "max connection extractor function label only", + applications: withApplications(application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80))), + + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + )), + expectedFrontends: map[string]*types.Frontend{ + "frontend-app": { + Backend: "backend-app", + Routes: map[string]types.Route{ + "route-host-app": { + Rule: "Host:app.marathon.localhost", }, }, PassHostHeader: true, @@ -98,7 +117,7 @@ func TestBuildConfiguration(t *testing.T) { expectedBackends: map[string]*types.Backend{ "backend-app": { Servers: map[string]types.Server{ - "server-task": { + "server-app-taskID": { URL: "http://localhost:80", Weight: 0, }, @@ -109,16 +128,18 @@ func TestBuildConfiguration(t *testing.T) { }, { desc: "multiple ports", - application: application( - appPorts(80, 81), - withTasks(localhostTask(taskPorts(80, 81))), - ), + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + )), expectedFrontends: map[string]*types.Frontend{ "frontend-app": { Backend: "backend-app", Routes: map[string]types.Route{ "route-host-app": { - Rule: "Host:app.docker.localhost", + Rule: "Host:app.marathon.localhost", }, }, PassHostHeader: true, @@ -129,7 +150,7 @@ func TestBuildConfiguration(t *testing.T) { expectedBackends: map[string]*types.Backend{ "backend-app": { Servers: map[string]types.Server{ - "server-task": { + "server-app-taskID": { URL: "http://localhost:80", Weight: 0, }, @@ -139,82 +160,84 @@ func TestBuildConfiguration(t *testing.T) { }, { desc: "with all labels", - application: application( - appPorts(80), - withTasks(task(host("127.0.0.1"), taskPorts(80))), + applications: withApplications( + application( + appID("/app"), + appPorts(80), + withTasks(task(host("127.0.0.1"), taskPorts(80), taskState(taskStateRunning))), - withLabel(label.TraefikPort, "666"), - withLabel(label.TraefikProtocol, "https"), - withLabel(label.TraefikWeight, "12"), + withLabel(label.TraefikPort, "666"), + withLabel(label.TraefikProtocol, "https"), + withLabel(label.TraefikWeight, "12"), - withLabel(label.TraefikBackend, "foobar"), + withLabel(label.TraefikBackend, "foobar"), - withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), - withLabel(label.TraefikBackendHealthCheckPath, "/health"), - withLabel(label.TraefikBackendHealthCheckPort, "880"), - withLabel(label.TraefikBackendHealthCheckInterval, "6"), - withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), - withLabel(label.TraefikBackendLoadBalancerSticky, "true"), - withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), - withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"), - withLabel(label.TraefikBackendMaxConnAmount, "666"), - withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - withLabel(label.TraefikBackendBufferingMaxResponseBodyBytes, "10485760"), - withLabel(label.TraefikBackendBufferingMemResponseBodyBytes, "2097152"), - withLabel(label.TraefikBackendBufferingMaxRequestBodyBytes, "10485760"), - withLabel(label.TraefikBackendBufferingMemRequestBodyBytes, "2097152"), - withLabel(label.TraefikBackendBufferingRetryExpression, "IsNetworkError() && Attempts() <= 2"), + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), + withLabel(label.TraefikBackendHealthCheckPath, "/health"), + withLabel(label.TraefikBackendHealthCheckPort, "880"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerSticky, "true"), + withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + withLabel(label.TraefikBackendBufferingMaxResponseBodyBytes, "10485760"), + withLabel(label.TraefikBackendBufferingMemResponseBodyBytes, "2097152"), + withLabel(label.TraefikBackendBufferingMaxRequestBodyBytes, "10485760"), + withLabel(label.TraefikBackendBufferingMemRequestBodyBytes, "2097152"), + withLabel(label.TraefikBackendBufferingRetryExpression, "IsNetworkError() && Attempts() <= 2"), - withLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - withLabel(label.TraefikFrontendEntryPoints, "http,https"), - withLabel(label.TraefikFrontendPassHostHeader, "true"), - withLabel(label.TraefikFrontendPassTLSCert, "true"), - withLabel(label.TraefikFrontendPriority, "666"), - withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), - withLabel(label.TraefikFrontendRedirectRegex, "nope"), - withLabel(label.TraefikFrontendRedirectReplacement, "nope"), - withLabel(label.TraefikFrontendRedirectPermanent, "true"), - withLabel(label.TraefikFrontendRule, "Host:traefik.io"), - withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"), - withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"), + withLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + withLabel(label.TraefikFrontendEntryPoints, "http,https"), + withLabel(label.TraefikFrontendPassHostHeader, "true"), + withLabel(label.TraefikFrontendPassTLSCert, "true"), + withLabel(label.TraefikFrontendPriority, "666"), + withLabel(label.TraefikFrontendRedirectEntryPoint, "https"), + withLabel(label.TraefikFrontendRedirectRegex, "nope"), + withLabel(label.TraefikFrontendRedirectReplacement, "nope"), + withLabel(label.TraefikFrontendRedirectPermanent, "true"), + withLabel(label.TraefikFrontendRule, "Host:traefik.io"), + withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"), + withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"), - withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - withLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor"), - withLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor"), - withLabel(label.TraefikFrontendSSLHost, "foo"), - withLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo"), - withLabel(label.TraefikFrontendContentSecurityPolicy, "foo"), - withLabel(label.TraefikFrontendPublicKey, "foo"), - withLabel(label.TraefikFrontendReferrerPolicy, "foo"), - withLabel(label.TraefikFrontendCustomBrowserXSSValue, "foo"), - withLabel(label.TraefikFrontendSTSSeconds, "666"), - withLabel(label.TraefikFrontendSSLRedirect, "true"), - withLabel(label.TraefikFrontendSSLTemporaryRedirect, "true"), - withLabel(label.TraefikFrontendSTSIncludeSubdomains, "true"), - withLabel(label.TraefikFrontendSTSPreload, "true"), - withLabel(label.TraefikFrontendForceSTSHeader, "true"), - withLabel(label.TraefikFrontendFrameDeny, "true"), - withLabel(label.TraefikFrontendContentTypeNosniff, "true"), - withLabel(label.TraefikFrontendBrowserXSSFilter, "true"), - withLabel(label.TraefikFrontendIsDevelopment, "true"), + withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + withLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor"), + withLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor"), + withLabel(label.TraefikFrontendSSLHost, "foo"), + withLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo"), + withLabel(label.TraefikFrontendContentSecurityPolicy, "foo"), + withLabel(label.TraefikFrontendPublicKey, "foo"), + withLabel(label.TraefikFrontendReferrerPolicy, "foo"), + withLabel(label.TraefikFrontendCustomBrowserXSSValue, "foo"), + withLabel(label.TraefikFrontendSTSSeconds, "666"), + withLabel(label.TraefikFrontendSSLRedirect, "true"), + withLabel(label.TraefikFrontendSSLTemporaryRedirect, "true"), + withLabel(label.TraefikFrontendSTSIncludeSubdomains, "true"), + withLabel(label.TraefikFrontendSTSPreload, "true"), + withLabel(label.TraefikFrontendForceSTSHeader, "true"), + withLabel(label.TraefikFrontendFrameDeny, "true"), + withLabel(label.TraefikFrontendContentTypeNosniff, "true"), + withLabel(label.TraefikFrontendBrowserXSSFilter, "true"), + withLabel(label.TraefikFrontendIsDevelopment, "true"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageBackend, "foobar"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageQuery, "foo_query"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageStatus, "500,600"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageBackend, "foobar"), - withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageQuery, "bar_query"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageQuery, "foo_query"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageStatus, "500,600"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageQuery, "bar_query"), - withLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), - withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), - ), + withLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), + withLabel(label.Prefix+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), + )), expectedFrontends: map[string]*types.Frontend{ "frontend-app": { EntryPoints: []string{ @@ -319,7 +342,7 @@ func TestBuildConfiguration(t *testing.T) { expectedBackends: map[string]*types.Backend{ "backendfoobar": { Servers: map[string]types.Server{ - "server-task": { + "server-app-taskID": { URL: "https://127.0.0.1:666", Weight: 12, }, @@ -353,6 +376,60 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + { + desc: "2 applications with the same backend name", + applications: withApplications( + application( + appID("/foo-v000"), + withTasks(localhostTask(taskPorts(8080))), + + withLabel("traefik.main.backend", "test.foo"), + withLabel("traefik.main.protocol", "http"), + withLabel("traefik.protocol", "http"), + withLabel("traefik.main.portIndex", "0"), + withLabel("traefik.enable", "true"), + withLabel("traefik.main.frontend.rule", "Host:app.marathon.localhost"), + ), + application( + appID("/foo-v001"), + withTasks(localhostTask(taskPorts(8081))), + + withLabel("traefik.main.backend", "test.foo"), + withLabel("traefik.main.protocol", "http"), + withLabel("traefik.protocol", "http"), + withLabel("traefik.main.portIndex", "0"), + withLabel("traefik.enable", "true"), + withLabel("traefik.main.frontend.rule", "Host:app.marathon.localhost"), + ), + ), + expectedFrontends: map[string]*types.Frontend{ + "frontend-foo-v000-service-main": { + EntryPoints: []string{}, + Backend: "backendtest-foo", + Routes: map[string]types.Route{ + "route-host-foo-v000-service-main": { + Rule: "Host:app.marathon.localhost", + }, + }, + PassHostHeader: true, + BasicAuth: []string{}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backendtest-foo": { + Servers: map[string]types.Server{ + "server-foo-v000-taskID-service-main": { + URL: "http://localhost:8080", + Weight: 0, + }, + "server-foo-v001-taskID-service-main": { + URL: "http://localhost:8081", + Weight: 0, + }, + }, + }, + }, + }, } for _, test := range testCases { @@ -360,21 +437,12 @@ func TestBuildConfiguration(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - test.application.ID = "/app" - - for _, task := range test.application.Tasks { - task.ID = "task" - if task.State == "" { - task.State = "TASK_RUNNING" - } - } - p := &Provider{ - Domain: "docker.localhost", + Domain: "marathon.localhost", ExposedByDefault: true, } - actualConfig := p.buildConfigurationV2(withApplications(test.application)) + actualConfig := p.buildConfigurationV2(test.applications) assert.NotNil(t, actualConfig) assert.Equal(t, test.expectedBackends, actualConfig.Backends) @@ -383,33 +451,35 @@ func TestBuildConfiguration(t *testing.T) { } } -func TestBuildConfigurationServices(t *testing.T) { +func TestBuildConfigurationSegments(t *testing.T) { testCases := []struct { desc string - application marathon.Application + applications *marathon.Applications expectedFrontends map[string]*types.Frontend expectedBackends map[string]*types.Backend }{ { - desc: "multiple ports with services", - application: application( - appPorts(80, 81), - withTasks(localhostTask(taskPorts(80, 81))), + desc: "multiple ports with segments", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), - withLabel(label.TraefikBackendMaxConnAmount, "1000"), - withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - withServiceLabel(label.TraefikPort, "80", "web"), - withServiceLabel(label.TraefikPort, "81", "admin"), - withLabel("traefik..port", "82"), // This should be ignored, as it fails to match the segmentPropertiesRegexp regex. - withServiceLabel(label.TraefikFrontendRule, "Host:web.app.docker.localhost", "web"), - withServiceLabel(label.TraefikFrontendRule, "Host:admin.app.docker.localhost", "admin"), - ), + withLabel(label.TraefikBackendMaxConnAmount, "1000"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + withSegmentLabel(label.TraefikPort, "80", "web"), + withSegmentLabel(label.TraefikPort, "81", "admin"), + withLabel("traefik..port", "82"), // This should be ignored, as it fails to match the segmentPropertiesRegexp regex. + withSegmentLabel(label.TraefikFrontendRule, "Host:web.app.marathon.localhost", "web"), + withSegmentLabel(label.TraefikFrontendRule, "Host:admin.app.marathon.localhost", "admin"), + )), expectedFrontends: map[string]*types.Frontend{ "frontend-app-service-web": { Backend: "backend-app-service-web", Routes: map[string]types.Route{ `route-host-app-service-web`: { - Rule: "Host:web.app.docker.localhost", + Rule: "Host:web.app.marathon.localhost", }, }, PassHostHeader: true, @@ -420,7 +490,7 @@ func TestBuildConfigurationServices(t *testing.T) { Backend: "backend-app-service-admin", Routes: map[string]types.Route{ `route-host-app-service-admin`: { - Rule: "Host:admin.app.docker.localhost", + Rule: "Host:admin.app.marathon.localhost", }, }, PassHostHeader: true, @@ -431,7 +501,7 @@ func TestBuildConfigurationServices(t *testing.T) { expectedBackends: map[string]*types.Backend{ "backend-app-service-web": { Servers: map[string]types.Server{ - "server-task-service-web": { + "server-app-taskID-service-web": { URL: "http://localhost:80", Weight: 0, }, @@ -443,7 +513,7 @@ func TestBuildConfigurationServices(t *testing.T) { }, "backend-app-service-admin": { Servers: map[string]types.Server{ - "server-task-service-admin": { + "server-app-taskID-service-admin": { URL: "http://localhost:81", Weight: 0, }, @@ -457,82 +527,84 @@ func TestBuildConfigurationServices(t *testing.T) { }, { desc: "when all labels are set", - application: application( - appPorts(80, 81), - withTasks(localhostTask(taskPorts(80, 81))), + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), - // withLabel(label.TraefikBackend, "foobar"), + // withLabel(label.TraefikBackend, "foobar"), - withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), - withLabel(label.TraefikBackendHealthCheckPath, "/health"), - withLabel(label.TraefikBackendHealthCheckPort, "880"), - withLabel(label.TraefikBackendHealthCheckInterval, "6"), - withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), - withLabel(label.TraefikBackendLoadBalancerSticky, "true"), - withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), - withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"), - withLabel(label.TraefikBackendMaxConnAmount, "666"), - withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - withLabel(label.TraefikBackendBufferingMaxResponseBodyBytes, "10485760"), - withLabel(label.TraefikBackendBufferingMemResponseBodyBytes, "2097152"), - withLabel(label.TraefikBackendBufferingMaxRequestBodyBytes, "10485760"), - withLabel(label.TraefikBackendBufferingMemRequestBodyBytes, "2097152"), - withLabel(label.TraefikBackendBufferingRetryExpression, "IsNetworkError() && Attempts() <= 2"), + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), + withLabel(label.TraefikBackendHealthCheckPath, "/health"), + withLabel(label.TraefikBackendHealthCheckPort, "880"), + withLabel(label.TraefikBackendHealthCheckInterval, "6"), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendLoadBalancerSticky, "true"), + withLabel(label.TraefikBackendLoadBalancerStickiness, "true"), + withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"), + withLabel(label.TraefikBackendMaxConnAmount, "666"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + withLabel(label.TraefikBackendBufferingMaxResponseBodyBytes, "10485760"), + withLabel(label.TraefikBackendBufferingMemResponseBodyBytes, "2097152"), + withLabel(label.TraefikBackendBufferingMaxRequestBodyBytes, "10485760"), + withLabel(label.TraefikBackendBufferingMemRequestBodyBytes, "2097152"), + withLabel(label.TraefikBackendBufferingRetryExpression, "IsNetworkError() && Attempts() <= 2"), - withServiceLabel(label.TraefikPort, "80", "containous"), - withServiceLabel(label.TraefikProtocol, "https", "containous"), - withServiceLabel(label.TraefikWeight, "12", "containous"), + withSegmentLabel(label.TraefikPort, "80", "containous"), + withSegmentLabel(label.TraefikProtocol, "https", "containous"), + withSegmentLabel(label.TraefikWeight, "12", "containous"), - withServiceLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "containous"), - withServiceLabel(label.TraefikFrontendEntryPoints, "http,https", "containous"), - withServiceLabel(label.TraefikFrontendPassHostHeader, "true", "containous"), - withServiceLabel(label.TraefikFrontendPassTLSCert, "true", "containous"), - withServiceLabel(label.TraefikFrontendPriority, "666", "containous"), - withServiceLabel(label.TraefikFrontendRedirectEntryPoint, "https", "containous"), - withServiceLabel(label.TraefikFrontendRedirectRegex, "nope", "containous"), - withServiceLabel(label.TraefikFrontendRedirectReplacement, "nope", "containous"), - withServiceLabel(label.TraefikFrontendRedirectPermanent, "true", "containous"), - withServiceLabel(label.TraefikFrontendRule, "Host:traefik.io", "containous"), - withServiceLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10", "containous"), - withServiceLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true", "containous"), + withSegmentLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "containous"), + withSegmentLabel(label.TraefikFrontendEntryPoints, "http,https", "containous"), + withSegmentLabel(label.TraefikFrontendPassHostHeader, "true", "containous"), + withSegmentLabel(label.TraefikFrontendPassTLSCert, "true", "containous"), + withSegmentLabel(label.TraefikFrontendPriority, "666", "containous"), + withSegmentLabel(label.TraefikFrontendRedirectEntryPoint, "https", "containous"), + withSegmentLabel(label.TraefikFrontendRedirectRegex, "nope", "containous"), + withSegmentLabel(label.TraefikFrontendRedirectReplacement, "nope", "containous"), + withSegmentLabel(label.TraefikFrontendRedirectPermanent, "true", "containous"), + withSegmentLabel(label.TraefikFrontendRule, "Host:traefik.io", "containous"), + withSegmentLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10", "containous"), + withSegmentLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true", "containous"), - withServiceLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), - withServiceLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), - withServiceLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), - withServiceLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor", "containous"), - withServiceLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor", "containous"), - withServiceLabel(label.TraefikFrontendSSLHost, "foo", "containous"), - withServiceLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo", "containous"), - withServiceLabel(label.TraefikFrontendContentSecurityPolicy, "foo", "containous"), - withServiceLabel(label.TraefikFrontendPublicKey, "foo", "containous"), - withServiceLabel(label.TraefikFrontendReferrerPolicy, "foo", "containous"), - withServiceLabel(label.TraefikFrontendCustomBrowserXSSValue, "foo", "containous"), - withServiceLabel(label.TraefikFrontendSTSSeconds, "666", "containous"), - withServiceLabel(label.TraefikFrontendSSLRedirect, "true", "containous"), - withServiceLabel(label.TraefikFrontendSSLTemporaryRedirect, "true", "containous"), - withServiceLabel(label.TraefikFrontendSTSIncludeSubdomains, "true", "containous"), - withServiceLabel(label.TraefikFrontendSTSPreload, "true", "containous"), - withServiceLabel(label.TraefikFrontendForceSTSHeader, "true", "containous"), - withServiceLabel(label.TraefikFrontendFrameDeny, "true", "containous"), - withServiceLabel(label.TraefikFrontendContentTypeNosniff, "true", "containous"), - withServiceLabel(label.TraefikFrontendBrowserXSSFilter, "true", "containous"), - withServiceLabel(label.TraefikFrontendIsDevelopment, "true", "containous"), + withSegmentLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), + withSegmentLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), + withSegmentLabel(label.TraefikFrontendSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), + withSegmentLabel(label.TraefikFrontendAllowedHosts, "foo,bar,bor", "containous"), + withSegmentLabel(label.TraefikFrontendHostsProxyHeaders, "foo,bar,bor", "containous"), + withSegmentLabel(label.TraefikFrontendSSLHost, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendCustomFrameOptionsValue, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendContentSecurityPolicy, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendPublicKey, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendReferrerPolicy, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendCustomBrowserXSSValue, "foo", "containous"), + withSegmentLabel(label.TraefikFrontendSTSSeconds, "666", "containous"), + withSegmentLabel(label.TraefikFrontendSSLRedirect, "true", "containous"), + withSegmentLabel(label.TraefikFrontendSSLTemporaryRedirect, "true", "containous"), + withSegmentLabel(label.TraefikFrontendSTSIncludeSubdomains, "true", "containous"), + withSegmentLabel(label.TraefikFrontendSTSPreload, "true", "containous"), + withSegmentLabel(label.TraefikFrontendForceSTSHeader, "true", "containous"), + withSegmentLabel(label.TraefikFrontendFrameDeny, "true", "containous"), + withSegmentLabel(label.TraefikFrontendContentTypeNosniff, "true", "containous"), + withSegmentLabel(label.TraefikFrontendBrowserXSSFilter, "true", "containous"), + withSegmentLabel(label.TraefikFrontendIsDevelopment, "true", "containous"), - withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), - withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageBackend, "foobar"), - withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageQuery, "foo_query"), - withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageStatus, "500,600"), - withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageBackend, "foobar"), - withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageQuery, "bar_query"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageStatus, "404"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"foo."+label.SuffixErrorPageQuery, "foo_query"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageStatus, "500,600"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageBackend, "foobar"), + withLabel(label.Prefix+"containous."+label.BaseFrontendErrorPage+"bar."+label.SuffixErrorPageQuery, "bar_query"), - withServiceLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip", "containous"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), - withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), - ), + withSegmentLabel(label.TraefikFrontendRateLimitExtractorFunc, "client.ip", "containous"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitPeriod, "6"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitAverage, "12"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"foo."+label.SuffixRateLimitBurst, "18"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitPeriod, "3"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitAverage, "6"), + withLabel(label.Prefix+"containous."+label.BaseFrontendRateLimit+"bar."+label.SuffixRateLimitBurst, "9"), + )), expectedFrontends: map[string]*types.Frontend{ "frontend-app-service-containous": { EntryPoints: []string{ @@ -637,7 +709,7 @@ func TestBuildConfigurationServices(t *testing.T) { expectedBackends: map[string]*types.Backend{ "backend-app-service-containous": { Servers: map[string]types.Server{ - "server-task-service-containous": { + "server-app-taskID-service-containous": { URL: "https://localhost:80", Weight: 12, }, @@ -678,21 +750,12 @@ func TestBuildConfigurationServices(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - test.application.ID = "/app" - - for _, task := range test.application.Tasks { - task.ID = "task" - if task.State == "" { - task.State = "TASK_RUNNING" - } - } - p := &Provider{ - Domain: "docker.localhost", + Domain: "marathon.localhost", ExposedByDefault: true, } - actualConfig := p.buildConfigurationV2(withApplications(test.application)) + actualConfig := p.buildConfigurationV2(test.applications) assert.NotNil(t, actualConfig) assert.Equal(t, test.expectedBackends, actualConfig.Backends) @@ -858,7 +921,7 @@ func TestTaskFilter(t *testing.T) { desc: "task not running", task: task( taskPorts(80), - state(taskStateStaging), + taskState(taskStateStaging), ), application: application(appPorts(80)), expected: false, @@ -884,8 +947,8 @@ func TestTaskFilter(t *testing.T) { task: task(taskPorts(80, 81)), application: application( appPorts(80, 81), - withServiceLabel(label.TraefikPort, "80", "web"), - withServiceLabel(label.TraefikPort, "illegal", "admin"), + withSegmentLabel(label.TraefikPort, "80", "web"), + withSegmentLabel(label.TraefikPort, "illegal", "admin"), ), expected: true, }, @@ -894,7 +957,7 @@ func TestTaskFilter(t *testing.T) { task: task(taskPorts(80, 81)), application: application( appPorts(80, 81), - withServiceLabel(label.TraefikPort, "81", "admin"), + withSegmentLabel(label.TraefikPort, "81", "admin"), ), expected: true, }, @@ -1090,7 +1153,7 @@ func TestGetFrontendRule(t *testing.T) { desc: "label missing", application: application(appID("test")), marathonLBCompatibility: true, - expected: "Host:test.docker.localhost", + expected: "Host:test.marathon.localhost", }, { desc: "HAProxy vhost available and LB compat disabled", @@ -1099,7 +1162,7 @@ func TestGetFrontendRule(t *testing.T) { withLabel("HAPROXY_0_VHOST", "foo.bar"), ), marathonLBCompatibility: false, - expected: "Host:test.docker.localhost", + expected: "Host:test.marathon.localhost", }, { desc: "HAProxy vhost available and LB compat enabled", @@ -1119,7 +1182,7 @@ func TestGetFrontendRule(t *testing.T) { }, { desc: "service label existing", - application: application(withServiceLabel(label.TraefikFrontendRule, "Host:foo.bar", "app")), + application: application(withSegmentLabel(label.TraefikFrontendRule, "Host:foo.bar", "app")), serviceName: "app", marathonLBCompatibility: true, expected: "Host:foo.bar", @@ -1131,7 +1194,7 @@ func TestGetFrontendRule(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() p := &Provider{ - Domain: "docker.localhost", + Domain: "marathon.localhost", MarathonLBCompatibility: test.marathonLBCompatibility, } @@ -1161,7 +1224,7 @@ func TestGetBackendName(t *testing.T) { }, { desc: "service label existing", - application: application(withServiceLabel(label.TraefikBackend, "bar", "app")), + application: application(withSegmentLabel(label.TraefikBackend, "bar", "app")), serviceName: "app", expected: "backendbar", }, diff --git a/provider/marathon/deprecated_config_test.go b/provider/marathon/deprecated_config_test.go index 575b9ba9f..b03aa8f49 100644 --- a/provider/marathon/deprecated_config_test.go +++ b/provider/marathon/deprecated_config_test.go @@ -67,7 +67,7 @@ func TestBuildConfigurationV1(t *testing.T) { desc: "filtered task", application: application( appPorts(80), - withTasks(localhostTask(taskPorts(80), state(taskStateStaging))), + withTasks(localhostTask(taskPorts(80), taskState(taskStateStaging))), ), expectedFrontends: map[string]*types.Frontend{ "frontend-app": { @@ -271,11 +271,11 @@ func TestBuildConfigurationServicesV1(t *testing.T) { withLabel(label.TraefikBackendMaxConnAmount, "1000"), withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - withServiceLabel(label.TraefikPort, "80", "web"), - withServiceLabel(label.TraefikPort, "81", "admin"), + withSegmentLabel(label.TraefikPort, "80", "web"), + withSegmentLabel(label.TraefikPort, "81", "admin"), withLabel("traefik..port", "82"), // This should be ignored, as it fails to match the servicesPropertiesRegexp regex. - withServiceLabel(label.TraefikFrontendRule, "Host:web.app.marathon.localhost", "web"), - withServiceLabel(label.TraefikFrontendRule, "Host:admin.app.marathon.localhost", "admin"), + withSegmentLabel(label.TraefikFrontendRule, "Host:web.app.marathon.localhost", "web"), + withSegmentLabel(label.TraefikFrontendRule, "Host:admin.app.marathon.localhost", "admin"), ), expectedFrontends: map[string]*types.Frontend{ "frontend-app-service-web": { @@ -344,15 +344,15 @@ func TestBuildConfigurationServicesV1(t *testing.T) { withLabel(label.TraefikBackendMaxConnAmount, "666"), withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), - withServiceLabel(label.TraefikPort, "80", "containous"), - withServiceLabel(label.TraefikProtocol, "https", "containous"), - withServiceLabel(label.TraefikWeight, "12", "containous"), + withSegmentLabel(label.TraefikPort, "80", "containous"), + withSegmentLabel(label.TraefikProtocol, "https", "containous"), + withSegmentLabel(label.TraefikWeight, "12", "containous"), - withServiceLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "containous"), - withServiceLabel(label.TraefikFrontendEntryPoints, "http,https", "containous"), - withServiceLabel(label.TraefikFrontendPassHostHeader, "true", "containous"), - withServiceLabel(label.TraefikFrontendPriority, "666", "containous"), - withServiceLabel(label.TraefikFrontendRule, "Host:traefik.io", "containous"), + withSegmentLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "containous"), + withSegmentLabel(label.TraefikFrontendEntryPoints, "http,https", "containous"), + withSegmentLabel(label.TraefikFrontendPassHostHeader, "true", "containous"), + withSegmentLabel(label.TraefikFrontendPriority, "666", "containous"), + withSegmentLabel(label.TraefikFrontendRule, "Host:traefik.io", "containous"), ), expectedFrontends: map[string]*types.Frontend{ "frontend-app-service-containous": { @@ -594,7 +594,7 @@ func TestGetFrontendRuleV1(t *testing.T) { }, { desc: "service label existing", - application: application(withServiceLabel(label.TraefikFrontendRule, "Host:foo.bar", "app")), + application: application(withSegmentLabel(label.TraefikFrontendRule, "Host:foo.bar", "app")), serviceName: "app", marathonLBCompatibility: true, expected: "Host:foo.bar", @@ -637,7 +637,7 @@ func TestGetBackendV1(t *testing.T) { }, { desc: "service label existing", - application: application(withServiceLabel(label.TraefikBackend, "bar", "app")), + application: application(withSegmentLabel(label.TraefikBackend, "bar", "app")), serviceName: "app", expected: "backendbar", }, diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index 2bc3dfb58..83e04a9be 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -1,8 +1,7 @@ {{ $apps := .Applications }} [backends] -{{range $app := $apps }} - {{ $backendName := getBackendName $app }} +{{range $backendName, $app := $apps }} [backends."{{ $backendName }}"] @@ -57,11 +56,11 @@ {{end}} [frontends] -{{range $app := $apps }} +{{range $backendName, $app := $apps }} {{ $frontendName := getFrontendName $app }} [frontends."{{ $frontendName }}"] - backend = "{{ getBackendName $app }}" + backend = "{{ $backendName }}" priority = {{ getPriority $app.SegmentLabels }} passHostHeader = {{ getPassHostHeader $app.SegmentLabels }} passTLSCert = {{ getPassTLSCert $app.SegmentLabels }} From f7fd1f2a636b33fe26d4b19e1869f9be94c2f985 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 4 Apr 2018 14:12:03 +0200 Subject: [PATCH 20/21] Prepare release v1.6.0-rc4 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b87f997e3..c004d0567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log +## [v1.6.0-rc4](https://github.com/containous/traefik/tree/v1.6.0-rc4) (2018-04-04) +[All Commits](https://github.com/containous/traefik/compare/v1.6.0-rc3...v1.6.0-rc4) + +**Enhancements:** +- **[consulcatalog,ecs,mesos]** Factorize labels managements. ([#3099](https://github.com/containous/traefik/pull/3099) by [ldez](https://github.com/ldez)) +- **[middleware]** Add tests on IPWhiteLister. ([#3106](https://github.com/containous/traefik/pull/3106) by [ldez](https://github.com/ldez)) + +**Bug fixes:** +- **[api,cluster]** Moved /api/cluster/leadership handler under public routes (requires no authentication) ([#3101](https://github.com/containous/traefik/pull/3101) by [aantono](https://github.com/aantono)) +- **[k8s]** Fixes prefixed annotations support. ([#3110](https://github.com/containous/traefik/pull/3110) by [ldez](https://github.com/ldez)) +- **[marathon]** Several apps with same backend name in Marathon. ([#3109](https://github.com/containous/traefik/pull/3109) by [ldez](https://github.com/ldez)) + +**Documentation:** +- **[k8s]** Update kubernetes.md ([#3093](https://github.com/containous/traefik/pull/3093) by [rdrgporto](https://github.com/rdrgporto)) +- Fixed documentation urls on README.md ([#3102](https://github.com/containous/traefik/pull/3102) by [emir](https://github.com/emir)) + ## [v1.6.0-rc3](https://github.com/containous/traefik/tree/v1.6.0-rc3) (2018-03-28) [All Commits](https://github.com/containous/traefik/compare/v1.6.0-rc2...v1.6.0-rc3) From a19b93c966306bc8f44306afca3f0f83d0a4aa3d Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 4 Apr 2018 15:04:04 +0200 Subject: [PATCH 21/21] fix: overflow on 32 bits arch. --- provider/mesos/deprecated_config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/provider/mesos/deprecated_config.go b/provider/mesos/deprecated_config.go index f4469496f..c7f22ee0d 100644 --- a/provider/mesos/deprecated_config.go +++ b/provider/mesos/deprecated_config.go @@ -233,7 +233,7 @@ func getFuncApplicationIntValueV1(labelName string, defaultValue int) func(task return defaultValue } - return getIntValueV1(task, labelName, defaultValue, math.MaxInt64) + return getIntValueV1(task, labelName, defaultValue, math.MaxInt32) } } @@ -254,7 +254,7 @@ func getFuncBoolValueV1(labelName string, defaultValue bool) func(task state.Tas // Deprecated func getFuncIntValueV1(labelName string, defaultValue int) func(task state.Task) int { return func(task state.Task) int { - return getIntValueV1(task, labelName, defaultValue, math.MaxInt64) + return getIntValueV1(task, labelName, defaultValue, math.MaxInt32) } }