diff --git a/docs/toml.md b/docs/toml.md index 86c6e2c4d..e0561657c 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -711,6 +711,10 @@ domain = "marathon.localhost" Labels can be used on containers to override default behaviour: - `traefik.backend=foo`: assign the application to `foo` backend +- `traefik.backend.maxconn.amount=10`: set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. +- `traefik.backend.maxconn.extractorfunc=client.ip`: set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. +- `traefik.backend.loadbalancer.method=drr`: override the default `wrr` load balancer algorithm +- `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5`: create a [circuit breaker](/basics/#backends) to be used against the backend - `traefik.portIndex=1`: register port by index in the application's ports array. Useful when the application exposes multiple ports. - `traefik.port=80`: register the explicit application port value. Cannot be used alongside `traefik.portIndex`. - `traefik.protocol=https`: override the default `http` protocol diff --git a/provider/marathon.go b/provider/marathon.go index bef3d7633..27982b5ce 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -8,14 +8,16 @@ import ( "strings" "text/template" + "math" + "net/http" + "time" + "github.com/BurntSushi/ty/fun" log "github.com/Sirupsen/logrus" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/emilevauge/backoff" "github.com/gambol99/go-marathon" - "net/http" - "time" ) // Marathon holds configuration of the Marathon provider. @@ -117,17 +119,24 @@ func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage, func (provider *Marathon) loadMarathonConfig() *types.Configuration { var MarathonFuncMap = template.FuncMap{ - "getBackend": provider.getBackend, - "getPort": provider.getPort, - "getWeight": provider.getWeight, - "getDomain": provider.getDomain, - "getProtocol": provider.getProtocol, - "getPassHostHeader": provider.getPassHostHeader, - "getPriority": provider.getPriority, - "getEntryPoints": provider.getEntryPoints, - "getFrontendRule": provider.getFrontendRule, - "getFrontendBackend": provider.getFrontendBackend, - "replace": replace, + "getBackend": provider.getBackend, + "getPort": provider.getPort, + "getWeight": provider.getWeight, + "getDomain": provider.getDomain, + "getProtocol": provider.getProtocol, + "getPassHostHeader": provider.getPassHostHeader, + "getPriority": provider.getPriority, + "getEntryPoints": provider.getEntryPoints, + "getFrontendRule": provider.getFrontendRule, + "getFrontendBackend": provider.getFrontendBackend, + "replace": replace, + "hasCircuitBreakerLabels": provider.hasCircuitBreakerLabels, + "hasLoadBalancerLabels": provider.hasLoadBalancerLabels, + "hasMaxConnLabels": provider.hasMaxConnLabels, + "getMaxConnExtractorFunc": provider.getMaxConnExtractorFunc, + "getMaxConnAmount": provider.getMaxConnAmount, + "getLoadBalancerMethod": provider.getLoadBalancerMethod, + "getCircuitBreakerExpression": provider.getCircuitBreakerExpression, } applications, err := provider.marathonClient.Applications(nil) @@ -375,3 +384,60 @@ func (provider *Marathon) getSubDomain(name string) string { } return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) } + +func (provider *Marathon) hasCircuitBreakerLabels(application marathon.Application) bool { + if _, err := provider.getLabel(application, "traefik.backend.circuitbreaker.expression"); err != nil { + return false + } + return true +} + +func (provider *Marathon) hasLoadBalancerLabels(application marathon.Application) bool { + if _, err := provider.getLabel(application, "traefik.backend.loadbalancer.method"); err != nil { + return false + } + return true +} + +func (provider *Marathon) hasMaxConnLabels(application marathon.Application) bool { + if _, err := provider.getLabel(application, "traefik.backend.maxconn.amount"); err != nil { + return false + } + if _, err := provider.getLabel(application, "traefik.backend.maxconn.extractorfunc"); err != nil { + return false + } + return true +} + +func (provider *Marathon) getMaxConnAmount(application marathon.Application) int64 { + if label, err := provider.getLabel(application, "traefik.backend.maxconn.amount"); err == nil { + i, errConv := strconv.ParseInt(label, 10, 64) + if errConv != nil { + log.Errorf("Unable to parse traefik.backend.maxconn.amount %s", label) + return math.MaxInt64 + } + return i + } + return math.MaxInt64 +} + +func (provider *Marathon) getMaxConnExtractorFunc(application marathon.Application) string { + if label, err := provider.getLabel(application, "traefik.backend.maxconn.extractorfunc"); err == nil { + return label + } + return "request.host" +} + +func (provider *Marathon) getLoadBalancerMethod(application marathon.Application) string { + if label, err := provider.getLabel(application, "traefik.backend.loadbalancer.method"); err == nil { + return label + } + return "wrr" +} + +func (provider *Marathon) getCircuitBreakerExpression(application marathon.Application) string { + if label, err := provider.getLabel(application, "traefik.backend.circuitbreaker.expression"); err == nil { + return label + } + return "NetworkErrorRatio() > 1" +} diff --git a/provider/marathon_test.go b/provider/marathon_test.go index af47a23bf..94e91701e 100644 --- a/provider/marathon_test.go +++ b/provider/marathon_test.go @@ -111,6 +111,200 @@ func TestMarathonLoadConfig(t *testing.T) { }, }, }, + { + applications: &marathon.Applications{ + Apps: []marathon.Application{ + { + ID: "/testLoadBalancerAndCircuitBreaker", + Ports: []int{80}, + Labels: &map[string]string{ + "traefik.backend.loadbalancer.method": "drr", + "traefik.backend.circuitbreaker.expression": "NetworkErrorRatio() > 0.5", + }, + }, + }, + }, + tasks: &marathon.Tasks{ + Tasks: []marathon.Task{ + { + ID: "testLoadBalancerAndCircuitBreaker", + AppID: "/testLoadBalancerAndCircuitBreaker", + Host: "127.0.0.1", + Ports: []int{80}, + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + `frontend-testLoadBalancerAndCircuitBreaker`: { + Backend: "backend-testLoadBalancerAndCircuitBreaker", + PassHostHeader: true, + EntryPoints: []string{}, + Routes: map[string]types.Route{ + `route-host-testLoadBalancerAndCircuitBreaker`: { + Rule: "Host:testLoadBalancerAndCircuitBreaker.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-testLoadBalancerAndCircuitBreaker": { + Servers: map[string]types.Server{ + "server-testLoadBalancerAndCircuitBreaker": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + }, + }, + }, + }, + { + applications: &marathon.Applications{ + Apps: []marathon.Application{ + { + ID: "/testMaxConn", + Ports: []int{80}, + Labels: &map[string]string{ + "traefik.backend.maxconn.amount": "1000", + "traefik.backend.maxconn.extractorfunc": "client.ip", + }, + }, + }, + }, + tasks: &marathon.Tasks{ + Tasks: []marathon.Task{ + { + ID: "testMaxConn", + AppID: "/testMaxConn", + Host: "127.0.0.1", + Ports: []int{80}, + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + `frontend-testMaxConn`: { + Backend: "backend-testMaxConn", + PassHostHeader: true, + EntryPoints: []string{}, + Routes: map[string]types.Route{ + `route-host-testMaxConn`: { + Rule: "Host:testMaxConn.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-testMaxConn": { + Servers: map[string]types.Server{ + "server-testMaxConn": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + MaxConn: &types.MaxConn{ + Amount: 1000, + ExtractorFunc: "client.ip", + }, + }, + }, + }, + { + applications: &marathon.Applications{ + Apps: []marathon.Application{ + { + ID: "/testMaxConnOnlySpecifyAmount", + Ports: []int{80}, + Labels: &map[string]string{ + "traefik.backend.maxconn.amount": "1000", + }, + }, + }, + }, + tasks: &marathon.Tasks{ + Tasks: []marathon.Task{ + { + ID: "testMaxConnOnlySpecifyAmount", + AppID: "/testMaxConnOnlySpecifyAmount", + Host: "127.0.0.1", + Ports: []int{80}, + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + `frontend-testMaxConnOnlySpecifyAmount`: { + Backend: "backend-testMaxConnOnlySpecifyAmount", + PassHostHeader: true, + EntryPoints: []string{}, + Routes: map[string]types.Route{ + `route-host-testMaxConnOnlySpecifyAmount`: { + Rule: "Host:testMaxConnOnlySpecifyAmount.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-testMaxConnOnlySpecifyAmount": { + Servers: map[string]types.Server{ + "server-testMaxConnOnlySpecifyAmount": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + MaxConn: nil, + }, + }, + }, + { + applications: &marathon.Applications{ + Apps: []marathon.Application{ + { + ID: "/testMaxConnOnlyExtractorFunc", + Ports: []int{80}, + Labels: &map[string]string{ + "traefik.backend.maxconn.extractorfunc": "client.ip", + }, + }, + }, + }, + tasks: &marathon.Tasks{ + Tasks: []marathon.Task{ + { + ID: "testMaxConnOnlyExtractorFunc", + AppID: "/testMaxConnOnlyExtractorFunc", + Host: "127.0.0.1", + Ports: []int{80}, + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + `frontend-testMaxConnOnlyExtractorFunc`: { + Backend: "backend-testMaxConnOnlyExtractorFunc", + PassHostHeader: true, + EntryPoints: []string{}, + Routes: map[string]types.Route{ + `route-host-testMaxConnOnlyExtractorFunc`: { + Rule: "Host:testMaxConnOnlyExtractorFunc.docker.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-testMaxConnOnlyExtractorFunc": { + Servers: map[string]types.Server{ + "server-testMaxConnOnlyExtractorFunc": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + MaxConn: nil, + }, + }, + }, } for _, c := range cases { diff --git a/provider/provider.go b/provider/provider.go index 90287fe40..441861025 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -10,12 +10,13 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "os" + "github.com/BurntSushi/toml" log "github.com/Sirupsen/logrus" "github.com/containous/traefik/autogen" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" - "os" ) // Provider defines methods of a provider. @@ -80,7 +81,9 @@ func (p *BaseProvider) getConfiguration(defaultTemplateFile string, funcMap temp return nil, err } - if _, err := toml.Decode(buffer.String(), configuration); err != nil { + var renderedTemplate = buffer.String() + // log.Debugf("Rendering results of %s:\n%s", defaultTemplateFile, renderedTemplate) + if _, err := toml.Decode(renderedTemplate, configuration); err != nil { return nil, err } return configuration, nil diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index 26194fab7..fbd2bf801 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -5,6 +5,22 @@ weight = {{getWeight . $apps}} {{end}} +{{range .Applications}} +{{ if hasMaxConnLabels . }} + [backends.backend{{getFrontendBackend . }}.maxconn] + amount = {{getMaxConnAmount . }} + extractorfunc = "{{getMaxConnExtractorFunc . }}" +{{end}} +{{ if hasLoadBalancerLabels . }} + [backends.backend{{getFrontendBackend . }}.loadbalancer] + method = "{{getLoadBalancerMethod . }}" +{{end}} +{{ if hasCircuitBreakerLabels . }} + [backends.backend{{getFrontendBackend . }}.circuitbreaker] + expression = "{{getCircuitBreakerExpression . }}" +{{end}} +{{end}} + [frontends]{{range .Applications}} [frontends.frontend{{.ID | replace "/" "-"}}] backend = "backend{{getFrontendBackend .}}"