From cee022b935bb844c30cdcc415dfb015c57549a92 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sat, 2 Dec 2017 19:27:47 +0100 Subject: [PATCH] refactor(marathon): rewrite configuration system. --- autogen/gentemplates/gen.go | 16 +- integration/marathon_test.go | 6 +- provider/marathon/builder_test.go | 10 +- provider/marathon/config.go | 416 ++++++++ .../{marathon_test.go => config_test.go} | 939 ++++-------------- provider/marathon/marathon.go | 557 +---------- provider/marathon/readiness.go | 6 +- provider/marathon/readiness_test.go | 2 +- templates/marathon.tmpl | 16 +- 9 files changed, 649 insertions(+), 1319 deletions(-) create mode 100644 provider/marathon/config.go rename provider/marathon/{marathon_test.go => config_test.go} (57%) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index d0bac7c6c..21ecd6d49 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -589,7 +589,7 @@ var _templatesMarathonTmpl = []byte(`{{$apps := .Applications}} {{range $app := $apps}} {{range $task := $app.Tasks}} {{range $serviceIndex, $serviceName := getServiceNames $app}} - [backends."backend{{getBackend $app $serviceName}}".servers."server-{{$task.ID | replace "." "-"}}{{getServiceNameSuffix $serviceName }}"] + [backends."{{getBackend $app $serviceName}}".servers."server-{{$task.ID | replace "." "-"}}{{getServiceNameSuffix $serviceName }}"] url = "{{getProtocol $app $serviceName}}://{{getBackendServer $task $app}}:{{getPort $task $app $serviceName}}" weight = {{getWeight $app $serviceName}} {{end}} @@ -598,27 +598,27 @@ var _templatesMarathonTmpl = []byte(`{{$apps := .Applications}} {{range $app := $apps}} {{range $serviceIndex, $serviceName := getServiceNames $app}} -[backends."backend{{getBackend $app $serviceName }}"] +[backends."{{getBackend $app $serviceName }}"] {{ if hasMaxConnLabels $app }} - [backends."backend{{getBackend $app $serviceName }}".maxconn] + [backends."{{getBackend $app $serviceName }}".maxconn] amount = {{getMaxConnAmount $app }} extractorfunc = "{{getMaxConnExtractorFunc $app }}" {{end}} {{ if hasLoadBalancerLabels $app }} - [backends."backend{{getBackend $app $serviceName }}".loadbalancer] + [backends."{{getBackend $app $serviceName }}".loadbalancer] method = "{{getLoadBalancerMethod $app }}" sticky = {{getSticky $app}} {{if hasStickinessLabel $app}} - [backends."backend{{getBackend $app $serviceName }}".loadbalancer.stickiness] + [backends."{{getBackend $app $serviceName }}".loadbalancer.stickiness] cookieName = "{{getStickinessCookieName $app}}" {{end}} {{end}} {{ if hasCircuitBreakerLabels $app }} - [backends."backend{{getBackend $app $serviceName }}".circuitbreaker] + [backends."{{getBackend $app $serviceName }}".circuitbreaker] expression = "{{getCircuitBreakerExpression $app }}" {{end}} {{ if hasHealthCheckLabels $app }} - [backends."backend{{getBackend $app $serviceName }}".healthcheck] + [backends."{{getBackend $app $serviceName }}".healthcheck] path = "{{getHealthCheckPath $app }}" interval = "{{getHealthCheckInterval $app }}" {{end}} @@ -627,7 +627,7 @@ var _templatesMarathonTmpl = []byte(`{{$apps := .Applications}} [frontends]{{range $app := $apps}}{{range $serviceIndex, $serviceName := getServiceNames .}} [frontends."{{ getFrontendName $app $serviceName }}"] - backend = "backend{{getBackend $app $serviceName}}" + backend = "{{getBackend $app $serviceName}}" passHostHeader = {{getPassHostHeader $app $serviceName}} priority = {{getPriority $app $serviceName}} entryPoints = [{{range getEntryPoints $app $serviceName}} diff --git a/integration/marathon_test.go b/integration/marathon_test.go index cb18c6e7a..57dee52f2 100644 --- a/integration/marathon_test.go +++ b/integration/marathon_test.go @@ -7,7 +7,7 @@ import ( "time" "github.com/containous/traefik/integration/try" - "github.com/containous/traefik/types" + "github.com/containous/traefik/provider/label" marathon "github.com/gambol99/go-marathon" "github.com/go-check/check" checker "github.com/vdemeester/shakers" @@ -109,7 +109,7 @@ func (s *MarathonSuite) TestConfigurationUpdate(c *check.C) { Name("/whoami"). CPU(0.1). Memory(32). - AddLabel(types.LabelFrontendRule, "PathPrefix:/service") + AddLabel(label.TraefikFrontendRule, "PathPrefix:/service") app.Container.Docker.Bridged(). Expose(80). Container("emilevauge/whoami") @@ -126,7 +126,7 @@ func (s *MarathonSuite) TestConfigurationUpdate(c *check.C) { Name("/whoami"). CPU(0.1). Memory(32). - AddLabel(types.ServiceLabel(types.LabelFrontendRule, "app"), "PathPrefix:/app") + AddLabel(label.GetServiceLabel(label.TraefikFrontendRule, "app"), "PathPrefix:/app") app.Container.Docker.Bridged(). Expose(80). Container("emilevauge/whoami") diff --git a/provider/marathon/builder_test.go b/provider/marathon/builder_test.go index bc9e2122d..0f821e46b 100644 --- a/provider/marathon/builder_test.go +++ b/provider/marathon/builder_test.go @@ -4,11 +4,11 @@ import ( "strings" "time" - "github.com/containous/traefik/types" + "github.com/containous/traefik/provider/label" "github.com/gambol99/go-marathon" ) -const testTaskName string = "taskID" +const testTaskName = "taskID" // Functions related to building applications. @@ -38,7 +38,7 @@ func appPorts(ports ...int) func(*marathon.Application) { } } -func label(key, value string) func(*marathon.Application) { +func withLabel(key, value string) func(*marathon.Application) { return func(app *marathon.Application) { app.AddLabel(key, value) } @@ -55,9 +55,9 @@ func labelWithService(key, value string, serviceName string) func(*marathon.Appl panic("serviceName can not be empty") } - property := strings.TrimPrefix(key, types.LabelPrefix) + property := strings.TrimPrefix(key, label.Prefix) return func(app *marathon.Application) { - app.AddLabel(types.LabelPrefix+serviceName+"."+property, value) + app.AddLabel(label.Prefix+serviceName+"."+property, value) } } diff --git a/provider/marathon/config.go b/provider/marathon/config.go new file mode 100644 index 000000000..6504b9a79 --- /dev/null +++ b/provider/marathon/config.go @@ -0,0 +1,416 @@ +package marathon + +import ( + "errors" + "fmt" + "math" + "net/url" + "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/gambol99/go-marathon" +) + +func (p *Provider) buildConfiguration() *types.Configuration { + var MarathonFuncMap = template.FuncMap{ + "getBackend": p.getBackend, + "getBackendServer": p.getBackendServer, + "getPort": getPort, + "getWeight": getFuncStringService(label.TraefikWeight, label.DefaultWeight), + "getDomain": getFuncStringService(label.TraefikDomain, p.Domain), // FIXME DEAD? + "getSubDomain": p.getSubDomain, // FIXME DEAD ? + "getProtocol": getFuncStringService(label.TraefikProtocol, label.DefaultProtocol), + "getPassHostHeader": getFuncStringService(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), + "getPriority": getFuncStringService(label.TraefikFrontendPriority, label.DefaultFrontendPriority), + "getEntryPoints": getFuncSliceStringService(label.TraefikFrontendEntryPoints), + "getFrontendRule": p.getFrontendRule, + "getFrontendName": p.getFrontendName, + "hasCircuitBreakerLabels": hasFunc(label.TraefikBackendCircuitBreakerExpression), + "hasLoadBalancerLabels": hasLoadBalancerLabels, + "hasMaxConnLabels": hasMaxConnLabels, + "getMaxConnExtractorFunc": getFuncString(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), + "getMaxConnAmount": getFuncInt64(label.TraefikBackendMaxConnAmount, math.MaxInt64), + "getLoadBalancerMethod": getFuncString(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), + "getCircuitBreakerExpression": getFuncString(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), + "getSticky": getSticky, + "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), + "getStickinessCookieName": getFuncString(label.TraefikBackendLoadBalancerStickinessCookieName, ""), + "hasHealthCheckLabels": hasFunc(label.TraefikBackendHealthCheckPath), + "getHealthCheckPath": getFuncString(label.TraefikBackendHealthCheckPath, ""), + "getHealthCheckInterval": getFuncString(label.TraefikBackendHealthCheckInterval, ""), + "getBasicAuth": getFuncSliceStringService(label.TraefikFrontendAuthBasic), + "getServiceNames": getServiceNames, + "getServiceNameSuffix": getServiceNameSuffix, + } + + v := url.Values{} + v.Add("embed", "apps.tasks") + v.Add("embed", "apps.deployments") + v.Add("embed", "apps.readiness") + applications, err := p.marathonClient.Applications(v) + if err != nil { + log.Errorf("Failed to retrieve Marathon applications: %v", err) + return nil + } + + filteredApps := fun.Filter(p.applicationFilter, applications.Apps).([]marathon.Application) + for i, app := range filteredApps { + filteredApps[i].Tasks = fun.Filter(func(task *marathon.Task) bool { + filtered := p.taskFilter(*task, app) + if filtered { + logIllegalServices(*task, app) + } + return filtered + }, app.Tasks).([]*marathon.Task) + } + + templateObjects := struct { + Applications []marathon.Application + Domain string // FIXME DEAD ? + }{ + Applications: filteredApps, + Domain: p.Domain, // FIXME DEAD ? + } + + configuration, err := p.GetConfiguration("templates/marathon.tmpl", MarathonFuncMap, templateObjects) + if err != nil { + log.Errorf("Failed to render Marathon configuration template: %v", err) + } + return configuration +} + +func (p *Provider) applicationFilter(app marathon.Application) bool { + // Filter disabled application. + if !label.IsEnabledP(app.Labels, p.ExposedByDefault) { + log.Debugf("Filtering disabled Marathon application %s", app.ID) + return false + } + + // Filter by constraints. + constraintTags := label.GetSliceStringValueP(app.Labels, label.TraefikTags) + if p.MarathonLBCompatibility { + if haGroup := label.GetStringValueP(app.Labels, labelLbCompatibilityGroup, ""); len(haGroup) > 0 { + constraintTags = append(constraintTags, haGroup) + } + } + if p.FilterMarathonConstraints && app.Constraints != nil { + for _, constraintParts := range *app.Constraints { + constraintTags = append(constraintTags, strings.Join(constraintParts, ":")) + } + } + if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok { + if failingConstraint != nil { + log.Debugf("Filtering Marathon application %s pruned by %q constraint", app.ID, failingConstraint.String()) + } + return false + } + + return true +} + +func (p *Provider) taskFilter(task marathon.Task, application marathon.Application) bool { + if task.State != string(taskStateRunning) { + return false + } + + // Filter task with existing, bad health check results. + if application.HasHealthChecks() { + if task.HasHealthCheckResults() { + for _, healthCheck := range task.HealthCheckResults { + if !healthCheck.Alive { + log.Debugf("Filtering Marathon task %s from application %s with bad health check", task.ID, application.ID) + return false + } + } + } + } + + if ready := p.readyChecker.Do(task, application); !ready { + log.Infof("Filtering unready task %s from application %s", task.ID, application.ID) + return false + } + + return true +} + +// getFrontendRule returns the frontend rule for the specified application, using +// its label. If service is provided, it will look for serviceName label before generic one. +// It returns a default one (Host) if the label is not present. +func (p *Provider) getFrontendRule(application marathon.Application, serviceName string) string { + labels := getLabels(application, serviceName) + lblFrontendRule := getLabelName(serviceName, label.SuffixFrontendRule) + if value := label.GetStringValue(labels, lblFrontendRule, ""); len(value) > 0 { + return value + } + + if p.MarathonLBCompatibility { + if value := label.GetStringValueP(application.Labels, labelLbCompatibility, ""); len(value) > 0 { + return "Host:" + value + } + } + + if len(serviceName) > 0 { + return "Host:" + strings.ToLower(provider.Normalize(serviceName)) + "." + p.getSubDomain(application.ID) + "." + p.Domain + } + return "Host:" + p.getSubDomain(application.ID) + "." + p.Domain +} + +func (p *Provider) getBackend(application marathon.Application, serviceName string) string { + labels := getLabels(application, serviceName) + lblBackend := getLabelName(serviceName, label.SuffixBackend) + value := label.GetStringValue(labels, lblBackend, "") + if len(value) > 0 { + return "backend" + value + } + return provider.Normalize("backend" + application.ID + getServiceNameSuffix(serviceName)) +} + +func (p *Provider) getFrontendName(application marathon.Application, serviceName string) string { + return provider.Normalize("frontend" + application.ID + getServiceNameSuffix(serviceName)) +} + +func (p *Provider) getSubDomain(name string) string { + if p.GroupsAsSubDomains { + splitedName := strings.Split(strings.TrimPrefix(name, "/"), "/") + provider.ReverseStringSlice(&splitedName) + reverseName := strings.Join(splitedName, ".") + return reverseName + } + return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) +} + +func (p *Provider) getBackendServer(task marathon.Task, application marathon.Application) string { + if application.IPAddressPerTask == nil || p.ForceTaskHostname { + return task.Host + } + + numTaskIPAddresses := len(task.IPAddresses) + switch numTaskIPAddresses { + case 0: + log.Errorf("Missing IP address for Marathon application %s on task %s", application.ID, task.ID) + return "" + case 1: + return task.IPAddresses[0].IPAddress + default: + ipAddressIdx := label.GetIntValueP(application.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", + numTaskIPAddresses, application.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", + numTaskIPAddresses, application.ID, task.ID) + return "" + } + + return task.IPAddresses[ipAddressIdx].IPAddress + } +} + +func identifier(app marathon.Application, task marathon.Task, serviceName string) string { + id := fmt.Sprintf("Marathon task %s from application %s", task.ID, app.ID) + if serviceName != "" { + id += fmt.Sprintf(" (service: %s)", serviceName) + } + return id +} + +// getServiceNames returns a list of service names for a given application +// An empty name "" will be added if no service specific properties exist, +// as an indication that there are no sub-services, but only main application +func getServiceNames(application marathon.Application) []string { + labelServiceProperties := label.ExtractServicePropertiesP(application.Labels) + + var names []string + for k := range labelServiceProperties { + names = append(names, k) + } + + // An empty name "" will be added if no service specific properties exist, + // as an indication that there are no sub-services, but only main application + if len(names) == 0 { + names = append(names, "") + } + return names +} + +func getServiceNameSuffix(serviceName string) string { + if len(serviceName) > 0 { + return "-service-" + provider.Normalize(serviceName) + } + return "" +} + +// logIllegalServices logs illegal service configurations. +// While we cannot filter on the service level, they will eventually get +// rejected once the server configuration is rendered. +func logIllegalServices(task marathon.Task, application marathon.Application) { + for _, serviceName := range getServiceNames(application) { + // Check for illegal/missing ports. + if _, err := processPorts(application, task, serviceName); err != nil { + log.Warnf("%s has an illegal configuration: no proper port available", identifier(application, task, serviceName)) + continue + } + + // Check for illegal port label combinations. + labels := getLabels(application, serviceName) + hasPortLabel := label.Has(labels, getLabelName(serviceName, label.SuffixPort)) + hasPortIndexLabel := label.Has(labels, getLabelName(serviceName, label.SuffixPortIndex)) + if hasPortLabel && hasPortIndexLabel { + log.Warnf("%s has both port and port index specified; port will take precedence", identifier(application, task, serviceName)) + } + } +} + +func hasLoadBalancerLabels(application marathon.Application) bool { + method := label.HasP(application.Labels, label.TraefikBackendLoadBalancerMethod) + sticky := label.HasP(application.Labels, label.TraefikBackendLoadBalancerSticky) + stickiness := label.HasP(application.Labels, label.TraefikBackendLoadBalancerStickiness) + return method || sticky || stickiness +} + +func hasMaxConnLabels(application marathon.Application) bool { + mca := label.HasP(application.Labels, label.TraefikBackendMaxConnAmount) + mcef := label.HasP(application.Labels, label.TraefikBackendMaxConnExtractorFunc) + return mca && mcef +} + +// TODO: Deprecated +// Deprecated replaced by Stickiness +func getSticky(application marathon.Application) string { + if label.HasP(application.Labels, label.TraefikBackendLoadBalancerSticky) { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) + } + return label.GetStringValueP(application.Labels, label.TraefikBackendLoadBalancerSticky, "false") +} + +func getPort(task marathon.Task, application marathon.Application, serviceName string) string { + port, err := processPorts(application, task, serviceName) + if err != nil { + log.Errorf("Unable to process ports for %s: %s", identifier(application, task, serviceName), err) + return "" + } + + return strconv.Itoa(port) +} + +// processPorts returns the configured port. +// An explicitly specified port is preferred. If none is specified, it selects +// one of the available port. The first such found port is returned unless an +// optional index is provided. +func processPorts(application marathon.Application, task marathon.Task, serviceName string) (int, error) { + labels := getLabels(application, serviceName) + lblPort := getLabelName(serviceName, label.SuffixPort) + + if label.Has(labels, lblPort) { + port := label.GetIntValue(labels, lblPort, 0) + + if port <= 0 { + return 0, fmt.Errorf("explicitly specified port %d must be larger than zero", port) + } else if port > 0 { + return port, nil + } + } + + ports := retrieveAvailablePorts(application, task) + if len(ports) == 0 { + return 0, errors.New("no port found") + } + + lblPortIndex := getLabelName(serviceName, label.SuffixPortIndex) + portIndex := label.GetIntValue(labels, lblPortIndex, 0) + if portIndex < 0 || portIndex > len(ports)-1 { + return 0, fmt.Errorf("index %d must be within range (0, %d)", portIndex, len(ports)-1) + } + return ports[portIndex], nil +} + +func retrieveAvailablePorts(application marathon.Application, task marathon.Task) []int { + // Using default port configuration + if len(task.Ports) > 0 { + return task.Ports + } + + // Using port definition if available + if application.PortDefinitions != nil && len(*application.PortDefinitions) > 0 { + var ports []int + for _, def := range *application.PortDefinitions { + if def.Port != nil { + ports = append(ports, *def.Port) + } + } + return ports + } + // If using IP-per-task using this port definition + if application.IPAddressPerTask != nil && len(*(application.IPAddressPerTask.Discovery).Ports) > 0 { + var ports []int + for _, def := range *(application.IPAddressPerTask.Discovery).Ports { + ports = append(ports, def.Number) + } + return ports + } + + return []int{} +} + +// Label functions + +func getLabels(application marathon.Application, serviceName string) map[string]string { + if len(serviceName) > 0 { + return label.ExtractServicePropertiesP(application.Labels)[serviceName] + } + + if application.Labels != nil { + return *application.Labels + } + + return make(map[string]string) +} + +func getLabelName(serviceName string, suffix string) string { + if len(serviceName) != 0 { + return suffix + } + return label.Prefix + suffix +} + +func hasFunc(labelName string) func(application marathon.Application) bool { + return func(application marathon.Application) bool { + return label.HasP(application.Labels, labelName) + } +} + +func getFuncStringService(labelName string, defaultValue string) func(application marathon.Application, serviceName string) string { + return func(application marathon.Application, serviceName string) string { + labels := getLabels(application, serviceName) + lbName := getLabelName(serviceName, labelName) + return label.GetStringValue(labels, lbName, defaultValue) + } +} + +func getFuncSliceStringService(labelName string) func(application marathon.Application, serviceName string) []string { + return func(application marathon.Application, serviceName string) []string { + labels := getLabels(application, serviceName) + return label.GetSliceStringValue(labels, getLabelName(serviceName, labelName)) + } +} + +func getFuncString(labelName string, defaultValue string) func(application marathon.Application) string { + return func(application marathon.Application) string { + return label.GetStringValueP(application.Labels, labelName, defaultValue) + } +} + +func getFuncInt64(labelName string, defaultValue int64) func(application marathon.Application) int64 { + return func(application marathon.Application) int64 { + return label.GetInt64ValueP(application.Labels, labelName, defaultValue) + } +} diff --git a/provider/marathon/marathon_test.go b/provider/marathon/config_test.go similarity index 57% rename from provider/marathon/marathon_test.go rename to provider/marathon/config_test.go index c2a531a48..ad429318a 100644 --- a/provider/marathon/marathon_test.go +++ b/provider/marathon/config_test.go @@ -3,12 +3,10 @@ package marathon import ( "errors" "fmt" - "math" - "reflect" "testing" + "github.com/containous/traefik/provider/label" "github.com/containous/traefik/provider/marathon/mocks" - "github.com/containous/traefik/testhelpers" "github.com/containous/traefik/types" "github.com/gambol99/go-marathon" "github.com/stretchr/testify/assert" @@ -30,19 +28,19 @@ func newFakeClient(applicationsError bool, applications marathon.Applications) * return fakeClient } -func TestMarathonLoadConfigAPIErrors(t *testing.T) { +func TestLoadConfigAPIErrors(t *testing.T) { fakeClient := newFakeClient(true, marathon.Applications{}) provider := &Provider{ marathonClient: fakeClient, } - actualConfig := provider.loadMarathonConfig() + actualConfig := provider.buildConfiguration() fakeClient.AssertExpectations(t) if actualConfig != nil { t.Errorf("configuration should have been nil, got %v", actualConfig) } } -func TestMarathonLoadConfigNonAPIErrors(t *testing.T) { +func TestLoadConfigNonAPIErrors(t *testing.T) { cases := []struct { desc string application marathon.Application @@ -101,8 +99,8 @@ func TestMarathonLoadConfigNonAPIErrors(t *testing.T) { desc: "load balancer / circuit breaker labels", application: application( appPorts(80), - label(types.LabelBackendLoadbalancerMethod, "drr"), - label(types.LabelBackendCircuitbreakerExpression, "NetworkErrorRatio() > 0.5"), + withLabel(label.TraefikBackendLoadBalancerMethod, "drr"), + withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), ), task: localhostTask(taskPorts(80)), expectedFrontends: map[string]*types.Frontend{ @@ -136,8 +134,8 @@ func TestMarathonLoadConfigNonAPIErrors(t *testing.T) { desc: "general max connection labels", application: application( appPorts(80), - label(types.LabelBackendMaxconnAmount, "1000"), - label(types.LabelBackendMaxconnExtractorfunc, "client.ip"), + withLabel(label.TraefikBackendMaxConnAmount, "1000"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), ), task: localhostTask(taskPorts(80)), expectedFrontends: map[string]*types.Frontend{ @@ -169,7 +167,7 @@ func TestMarathonLoadConfigNonAPIErrors(t *testing.T) { desc: "max connection amount label only", application: application( appPorts(80), - label(types.LabelBackendMaxconnAmount, "1000"), + withLabel(label.TraefikBackendMaxConnAmount, "1000"), ), task: localhostTask(taskPorts(80)), expectedFrontends: map[string]*types.Frontend{ @@ -198,7 +196,7 @@ func TestMarathonLoadConfigNonAPIErrors(t *testing.T) { desc: "max connection extractor function label only", application: application( appPorts(80), - label(types.LabelBackendMaxconnExtractorfunc, "client.ip"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), ), task: localhostTask(taskPorts(80)), expectedFrontends: map[string]*types.Frontend{ @@ -227,8 +225,8 @@ func TestMarathonLoadConfigNonAPIErrors(t *testing.T) { desc: "health check labels", application: application( appPorts(80), - label(types.LabelBackendHealthcheckPath, "/path"), - label(types.LabelBackendHealthcheckInterval, "5m"), + withLabel(label.TraefikBackendHealthCheckPath, "/path"), + withLabel(label.TraefikBackendHealthCheckInterval, "5m"), ), task: task( host("127.0.0.1"), @@ -292,13 +290,13 @@ func TestMarathonLoadConfigNonAPIErrors(t *testing.T) { desc: "multiple ports with services", application: application( appPorts(80, 81), - label(types.LabelBackendMaxconnAmount, "1000"), - label(types.LabelBackendMaxconnExtractorfunc, "client.ip"), - label("traefik.web.port", "80"), - label("traefik.admin.port", "81"), - label("traefik..port", "82"), // This should be ignored, as it fails to match the servicesPropertiesRegexp regex. - label("traefik.web.frontend.rule", "Host:web.app.docker.localhost"), - label("traefik.admin.frontend.rule", "Host:admin.app.docker.localhost"), + withLabel(label.TraefikBackendMaxConnAmount, "1000"), + withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"), + withLabel("traefik.web.port", "80"), + withLabel("traefik.admin.port", "81"), + withLabel("traefik..port", "82"), // This should be ignored, as it fails to match the servicesPropertiesRegexp regex. + withLabel("traefik.web.frontend.rule", "Host:web.app.docker.localhost"), + withLabel("traefik.admin.frontend.rule", "Host:admin.app.docker.localhost"), ), task: localhostTask( taskPorts(80, 81), @@ -375,7 +373,7 @@ func TestMarathonLoadConfigNonAPIErrors(t *testing.T) { ExposedByDefault: true, marathonClient: fakeClient, } - actualConfig := provider.loadMarathonConfig() + actualConfig := provider.buildConfiguration() fakeClient.AssertExpectations(t) expectedConfig := &types.Configuration{ @@ -387,7 +385,139 @@ func TestMarathonLoadConfigNonAPIErrors(t *testing.T) { } } -func TestMarathonTaskFilter(t *testing.T) { +func TestApplicationFilterConstraints(t *testing.T) { + cases := []struct { + desc string + application marathon.Application + marathonLBCompatibility bool + filterMarathonConstraints bool + expected bool + }{ + { + desc: "tags missing", + application: application(), + marathonLBCompatibility: false, + expected: false, + }, + { + desc: "tag matching", + application: application(withLabel(label.TraefikTags, "valid")), + marathonLBCompatibility: false, + expected: true, + }, + { + desc: "constraint missing", + application: application(), + marathonLBCompatibility: false, + filterMarathonConstraints: true, + expected: false, + }, + { + desc: "constraint invalid", + application: application(constraint("service_cluster:CLUSTER:test")), + marathonLBCompatibility: false, + filterMarathonConstraints: true, + expected: false, + }, + { + desc: "constraint valid", + application: application(constraint("valid")), + marathonLBCompatibility: false, + filterMarathonConstraints: true, + expected: true, + }, + { + desc: "LB compatibility tag matching", + application: application( + withLabel("HAPROXY_GROUP", "valid"), + withLabel(label.TraefikTags, "notvalid"), + ), + marathonLBCompatibility: true, + expected: true, + }, + } + + for _, c := range cases { + c := c + t.Run(c.desc, func(t *testing.T) { + t.Parallel() + provider := &Provider{ + ExposedByDefault: true, + MarathonLBCompatibility: c.marathonLBCompatibility, + FilterMarathonConstraints: c.filterMarathonConstraints, + } + constraint, err := types.NewConstraint("tag==valid") + if err != nil { + panic(fmt.Sprintf("failed to create constraint 'tag==valid': %s", err)) + } + provider.Constraints = types.Constraints{constraint} + actual := provider.applicationFilter(c.application) + if actual != c.expected { + t.Errorf("got %v, expected %v", actual, c.expected) + } + }) + } +} + +func TestApplicationFilterEnabled(t *testing.T) { + cases := []struct { + desc string + exposedByDefault bool + enabledLabel string + expected bool + }{ + { + desc: "exposed", + exposedByDefault: true, + enabledLabel: "", + expected: true, + }, + { + desc: "exposed and tolerated by valid label value", + exposedByDefault: true, + enabledLabel: "true", + expected: true, + }, + { + desc: "exposed and tolerated by invalid label value", + exposedByDefault: true, + enabledLabel: "invalid", + expected: true, + }, + { + desc: "exposed but overridden by label", + exposedByDefault: true, + enabledLabel: "false", + expected: false, + }, + { + desc: "non-exposed", + exposedByDefault: false, + enabledLabel: "", + expected: false, + }, + { + desc: "non-exposed but overridden by label", + exposedByDefault: false, + enabledLabel: "true", + expected: true, + }, + } + + for _, c := range cases { + c := c + t.Run(c.desc, func(t *testing.T) { + t.Parallel() + provider := &Provider{ExposedByDefault: c.exposedByDefault} + app := application(withLabel(label.TraefikEnable, c.enabledLabel)) + if provider.applicationFilter(app) != c.expected { + t.Errorf("got unexpected filtering = %t", !c.expected) + } + }) + } +} + +func TestTaskFilter(t *testing.T) { cases := []struct { desc string task marathon.Task @@ -421,8 +551,8 @@ func TestMarathonTaskFilter(t *testing.T) { task: task(taskPorts(80, 443)), application: application( appPorts(80, 443), - label(types.LabelPort, "443"), - label(types.LabelPortIndex, "1"), + withLabel(label.TraefikPort, "443"), + withLabel(label.TraefikPortIndex, "1"), ), expected: true, }, @@ -431,8 +561,8 @@ func TestMarathonTaskFilter(t *testing.T) { task: task(taskPorts(80, 81)), application: application( appPorts(80, 81), - labelWithService(types.LabelPort, "80", "web"), - labelWithService(types.LabelPort, "illegal", "admin"), + labelWithService(label.TraefikPort, "80", "web"), + labelWithService(label.TraefikPort, "illegal", "admin"), ), expected: true, }, @@ -441,7 +571,7 @@ func TestMarathonTaskFilter(t *testing.T) { task: task(taskPorts(80, 81)), application: application( appPorts(80, 81), - labelWithService(types.LabelPort, "81", "admin"), + labelWithService(label.TraefikPort, "81", "admin"), ), expected: true, }, @@ -517,141 +647,7 @@ func TestMarathonTaskFilter(t *testing.T) { } } -func TestMarathonApplicationFilterConstraints(t *testing.T) { - cases := []struct { - desc string - application marathon.Application - marathonLBCompatibility bool - filterMarathonConstraints bool - expected bool - }{ - { - desc: "tags missing", - application: application(), - marathonLBCompatibility: false, - expected: false, - }, - { - desc: "tag matching", - application: application(label(types.LabelTags, "valid")), - marathonLBCompatibility: false, - expected: true, - }, - { - desc: "constraint missing", - application: application(), - marathonLBCompatibility: false, - filterMarathonConstraints: true, - expected: false, - }, - { - desc: "constraint invalid", - application: application(constraint("service_cluster:CLUSTER:test")), - marathonLBCompatibility: false, - filterMarathonConstraints: true, - expected: false, - }, - { - desc: "constraint valid", - application: application(constraint("valid")), - marathonLBCompatibility: false, - filterMarathonConstraints: true, - expected: true, - }, - { - desc: "LB compatibility tag matching", - application: application( - label("HAPROXY_GROUP", "valid"), - label(types.LabelTags, "notvalid"), - ), - marathonLBCompatibility: true, - expected: true, - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{ - ExposedByDefault: true, - MarathonLBCompatibility: c.marathonLBCompatibility, - FilterMarathonConstraints: c.filterMarathonConstraints, - } - constraint, err := types.NewConstraint("tag==valid") - if err != nil { - panic(fmt.Sprintf("failed to create constraint 'tag==valid': %s", err)) - } - provider.Constraints = types.Constraints{constraint} - actual := provider.applicationFilter(c.application) - if actual != c.expected { - t.Errorf("got %v, expected %v", actual, c.expected) - } - }) - } -} - -func TestMarathonApplicationFilterEnabled(t *testing.T) { - cases := []struct { - desc string - exposedByDefault bool - enabledLabel string - expected bool - }{ - { - desc: "exposed", - exposedByDefault: true, - enabledLabel: "", - expected: true, - }, - { - desc: "exposed and tolerated by valid label value", - exposedByDefault: true, - enabledLabel: "true", - expected: true, - }, - { - desc: "exposed and tolerated by invalid label value", - exposedByDefault: true, - enabledLabel: "invalid", - expected: true, - }, - { - desc: "exposed but overridden by label", - exposedByDefault: true, - enabledLabel: "false", - expected: false, - }, - { - desc: "non-exposed", - exposedByDefault: false, - enabledLabel: "", - expected: false, - }, - { - desc: "non-exposed but overridden by label", - exposedByDefault: false, - enabledLabel: "true", - expected: true, - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{ExposedByDefault: c.exposedByDefault} - app := application(label(types.LabelEnable, c.enabledLabel)) - if provider.applicationFilter(app) != c.expected { - t.Errorf("got unexpected filtering = %t", !c.expected) - } - }) - } -} - -func TestMarathonGetPort(t *testing.T) { - provider := &Provider{} - +func TestGetPort(t *testing.T) { cases := []struct { desc string application marathon.Application @@ -667,19 +663,19 @@ func TestMarathonGetPort(t *testing.T) { }, { desc: "numeric port", - application: application(label(types.LabelPort, "80")), + application: application(withLabel(label.TraefikPort, "80")), task: task(), expected: "80", }, { desc: "string port", - application: application(label(types.LabelPort, "foobar")), + application: application(withLabel(label.TraefikPort, "foobar")), task: task(taskPorts(80)), expected: "", }, { desc: "negative port", - application: application(label(types.LabelPort, "-1")), + application: application(withLabel(label.TraefikPort, "-1")), task: task(taskPorts(80)), expected: "", }, @@ -711,21 +707,21 @@ func TestMarathonGetPort(t *testing.T) { }, { desc: "numeric port index specified", - application: application(label(types.LabelPortIndex, "1")), + application: application(withLabel(label.TraefikPortIndex, "1")), task: task(taskPorts(80, 443)), expected: "443", }, { desc: "string port index specified", - application: application(label(types.LabelPortIndex, "foobar")), + application: application(withLabel(label.TraefikPortIndex, "foobar")), task: task(taskPorts(80)), - expected: "", + expected: "80", }, { desc: "port and port index specified", application: application( - label(types.LabelPort, "80"), - label(types.LabelPortIndex, "1"), + withLabel(label.TraefikPort, "80"), + withLabel(label.TraefikPortIndex, "1"), ), task: task(taskPorts(80, 443)), expected: "80", @@ -738,21 +734,21 @@ func TestMarathonGetPort(t *testing.T) { }, { desc: "multiple task ports with service index available", - application: application(label(types.LabelPrefix+"http.portIndex", "0")), + application: application(withLabel(label.Prefix+"http.portIndex", "0")), task: task(taskPorts(80, 443)), serviceName: "http", expected: "80", }, { desc: "multiple task ports with service port available", - application: application(label(types.LabelPrefix+"https.port", "443")), + application: application(withLabel(label.Prefix+"https.port", "443")), task: task(taskPorts(80, 443)), serviceName: "https", expected: "443", }, { desc: "multiple task ports with services but default port available", - application: application(label(types.LabelPrefix+"http.weight", "100")), + application: application(withLabel(label.Prefix+"http.weight", "100")), task: task(taskPorts(80, 443)), serviceName: "http", expected: "80", @@ -763,45 +759,7 @@ func TestMarathonGetPort(t *testing.T) { c := c t.Run(c.desc, func(t *testing.T) { t.Parallel() - actual := provider.getPort(c.task, c.application, c.serviceName) - if actual != c.expected { - t.Errorf("actual %q, expected %q", c.expected, actual) - } - }) - } -} - -func TestMarathonGetWeight(t *testing.T) { - cases := []struct { - desc string - application marathon.Application - serviceName string - expected string - }{ - { - desc: "label missing", - application: application(), - expected: "0", - }, - { - desc: "label existing", - application: application(label(types.LabelWeight, "10")), - expected: "10", - }, - { - desc: "service label existing", - application: application(labelWithService(types.LabelWeight, "10", "app")), - serviceName: "app", - expected: "10", - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{} - actual := provider.getWeight(c.application, c.serviceName) + actual := getPort(c.task, c.application, c.serviceName) if actual != c.expected { t.Errorf("actual %q, expected %q", actual, c.expected) } @@ -809,77 +767,7 @@ func TestMarathonGetWeight(t *testing.T) { } } -func TestMarathonGetDomain(t *testing.T) { - cases := []struct { - desc string - application marathon.Application - expected string - }{ - { - desc: "label missing", - application: application(), - expected: "docker.localhost", - }, - { - desc: "label existing", - application: application(label(types.LabelDomain, "foo.bar")), - expected: "foo.bar", - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{ - Domain: "docker.localhost", - } - actual := provider.getDomain(c.application) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) - } - }) - } -} - -func TestMarathonGetProtocol(t *testing.T) { - cases := []struct { - desc string - application marathon.Application - serviceName string - expected string - }{ - { - desc: "label missing", - application: application(), - expected: "http", - }, - { - desc: "label existing", - application: application(label(types.LabelProtocol, "https")), - expected: "https", - }, - { - desc: "service label existing", - application: application(labelWithService(types.LabelProtocol, "https", "app")), - serviceName: "app", - expected: "https", - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{} - actual := provider.getProtocol(c.application, c.serviceName) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) - } - }) - } -} -func TestMarathonGetSticky(t *testing.T) { +func TestGetSticky(t *testing.T) { testCases := []struct { desc string application marathon.Application @@ -892,7 +780,7 @@ func TestMarathonGetSticky(t *testing.T) { }, { desc: "label existing", - application: application(label(types.LabelBackendLoadbalancerSticky, "true")), + application: application(withLabel(label.TraefikBackendLoadBalancerSticky, "true")), expected: "true", }, } @@ -901,8 +789,7 @@ func TestMarathonGetSticky(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - provider := &Provider{} - actual := provider.getSticky(test.application) + actual := getSticky(test.application) if actual != test.expected { t.Errorf("actual %q, expected %q", actual, test.expected) } @@ -910,242 +797,7 @@ func TestMarathonGetSticky(t *testing.T) { } } -func TestMarathonHasStickinessLabel(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - expected bool - }{ - { - desc: "label missing", - application: application(), - expected: false, - }, - { - desc: "stickiness=true", - application: application(label(types.LabelBackendLoadbalancerStickiness, "true")), - expected: true, - }, - { - desc: "stickiness=false ", - application: application(label(types.LabelBackendLoadbalancerStickiness, "true")), - expected: true, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - provider := &Provider{} - actual := provider.hasStickinessLabel(test.application) - if actual != test.expected { - t.Errorf("actual %q, expected %q", actual, test.expected) - } - }) - } -} - -func TestMarathonGetPassHostHeader(t *testing.T) { - cases := []struct { - desc string - application marathon.Application - serviceName string - expected string - }{ - { - desc: "label missing", - application: application(), - expected: "true", - }, - { - desc: "label existing", - application: application(label(types.LabelFrontendPassHostHeader, "false")), - expected: "false", - }, - { - desc: "label existing", - application: application(labelWithService(types.LabelFrontendPassHostHeader, "false", "app")), - serviceName: "app", - expected: "false", - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{} - actual := provider.getPassHostHeader(c.application, c.serviceName) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) - } - }) - } -} - -func TestMarathonMaxConnAmount(t *testing.T) { - cases := []struct { - desc string - application marathon.Application - expected int64 - }{ - { - desc: "label missing", - application: application(), - expected: math.MaxInt64, - }, - { - desc: "non-integer value", - application: application(label(types.LabelBackendMaxconnAmount, "foobar")), - expected: math.MaxInt64, - }, - { - desc: "label existing", - application: application(label(types.LabelBackendMaxconnAmount, "32")), - expected: 32, - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{} - actual := provider.getMaxConnAmount(c.application) - if actual != c.expected { - t.Errorf("actual %d, expected %d", actual, c.expected) - } - }) - } -} - -func TestMarathonGetMaxConnExtractorFunc(t *testing.T) { - cases := []struct { - desc string - application marathon.Application - expected string - }{ - { - desc: "label missing", - application: application(), - expected: "request.host", - }, - { - desc: "label existing", - application: application(label(types.LabelBackendMaxconnExtractorfunc, "client.ip")), - expected: "client.ip", - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{} - actual := provider.getMaxConnExtractorFunc(c.application) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) - } - }) - } -} - -func TestMarathonGetLoadBalancerMethod(t *testing.T) { - cases := []struct { - desc string - application marathon.Application - expected string - }{ - { - desc: "label missing", - application: application(), - expected: "wrr", - }, - { - desc: "label existing", - application: application(label(types.LabelBackendLoadbalancerMethod, "drr")), - expected: "drr", - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{} - actual := provider.getLoadBalancerMethod(c.application) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) - } - }) - } -} - -func TestMarathonGetCircuitBreakerExpression(t *testing.T) { - cases := []struct { - desc string - application marathon.Application - expected string - }{ - { - desc: "label missing", - application: application(), - expected: "NetworkErrorRatio() > 1", - }, - { - desc: "label existing", - application: application(label(types.LabelBackendCircuitbreakerExpression, "NetworkErrorRatio() > 0.5")), - expected: "NetworkErrorRatio() > 0.5", - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{} - actual := provider.getCircuitBreakerExpression(c.application) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) - } - }) - } -} - -func TestMarathonGetEntryPoints(t *testing.T) { - cases := []struct { - desc string - application marathon.Application - expected []string - }{ - { - desc: "label missing", - application: application(), - expected: []string{}, - }, - { - desc: "label existing", - application: application(label(types.LabelFrontendEntryPoints, "http,https")), - expected: []string{"http", "https"}, - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{} - actual := provider.getEntryPoints(c.application, "") - if !reflect.DeepEqual(actual, c.expected) { - t.Errorf("actual %#v, expected %#v", actual, c.expected) - } - }) - } -} - -func TestMarathonGetFrontendRule(t *testing.T) { +func TestGetFrontendRule(t *testing.T) { cases := []struct { desc string application marathon.Application @@ -1163,14 +815,14 @@ func TestMarathonGetFrontendRule(t *testing.T) { desc: "HAProxy vhost available and LB compat disabled", application: application( appID("test"), - label("HAPROXY_0_VHOST", "foo.bar"), + withLabel("HAPROXY_0_VHOST", "foo.bar"), ), marathonLBCompatibility: false, expected: "Host:test.docker.localhost", }, { desc: "HAProxy vhost available and LB compat enabled", - application: application(label("HAPROXY_0_VHOST", "foo.bar")), + application: application(withLabel("HAPROXY_0_VHOST", "foo.bar")), marathonLBCompatibility: true, expected: "Host:foo.bar", }, @@ -1178,15 +830,15 @@ func TestMarathonGetFrontendRule(t *testing.T) { desc: "frontend rule available", application: application( - label(types.LabelFrontendRule, "Host:foo.bar"), - label("HAPROXY_0_VHOST", "unused"), + withLabel(label.TraefikFrontendRule, "Host:foo.bar"), + withLabel("HAPROXY_0_VHOST", "unused"), ), marathonLBCompatibility: true, expected: "Host:foo.bar", }, { desc: "service label existing", - application: application(labelWithService(types.LabelFrontendRule, "Host:foo.bar", "app")), + application: application(labelWithService(label.TraefikFrontendRule, "Host:foo.bar", "app")), serviceName: "app", marathonLBCompatibility: true, expected: "Host:foo.bar", @@ -1197,11 +849,11 @@ func TestMarathonGetFrontendRule(t *testing.T) { c := c t.Run(c.desc, func(t *testing.T) { t.Parallel() - provider := &Provider{ + prov := &Provider{ Domain: "docker.localhost", MarathonLBCompatibility: c.marathonLBCompatibility, } - actual := provider.getFrontendRule(c.application, c.serviceName) + actual := prov.getFrontendRule(c.application, c.serviceName) if actual != c.expected { t.Errorf("actual %q, expected %q", actual, c.expected) } @@ -1209,7 +861,7 @@ func TestMarathonGetFrontendRule(t *testing.T) { } } -func TestMarathonGetBackend(t *testing.T) { +func TestGetBackend(t *testing.T) { cases := []struct { desc string application marathon.Application @@ -1219,18 +871,18 @@ func TestMarathonGetBackend(t *testing.T) { { desc: "label missing", application: application(appID("/group/app")), - expected: "-group-app", + expected: "backend-group-app", }, { desc: "label existing", - application: application(label(types.LabelBackend, "bar")), - expected: "bar", + application: application(withLabel(label.TraefikBackend, "bar")), + expected: "backendbar", }, { desc: "service label existing", - application: application(labelWithService(types.LabelBackend, "bar", "app")), + application: application(labelWithService(label.TraefikBackend, "bar", "app")), serviceName: "app", - expected: "bar", + expected: "backendbar", }, } @@ -1238,8 +890,8 @@ func TestMarathonGetBackend(t *testing.T) { c := c t.Run(c.desc, func(t *testing.T) { t.Parallel() - provider := &Provider{} - actual := provider.getBackend(c.application, c.serviceName) + prov := &Provider{} + actual := prov.getBackend(c.application, c.serviceName) if actual != c.expected { t.Errorf("actual %q, expected %q", actual, c.expected) } @@ -1247,7 +899,7 @@ func TestMarathonGetBackend(t *testing.T) { } } -func TestMarathonGetSubDomain(t *testing.T) { +func TestGetSubDomain(t *testing.T) { cases := []struct { path string expected string @@ -1269,116 +921,8 @@ func TestMarathonGetSubDomain(t *testing.T) { c := c t.Run(fmt.Sprintf("path=%s,group=%t", c.path, c.groupAsSubDomain), func(t *testing.T) { t.Parallel() - provider := &Provider{GroupsAsSubDomains: c.groupAsSubDomain} - actual := provider.getSubDomain(c.path) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) - } - }) - } -} - -func TestMarathonHasHealthCheckLabels(t *testing.T) { - cases := []struct { - desc string - value *string - expected bool - }{ - { - desc: "label missing", - value: nil, - expected: false, - }, - { - desc: "empty path", - value: testhelpers.Stringp(""), - expected: false, - }, - { - desc: "non-empty path", - value: testhelpers.Stringp("/path"), - expected: true, - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - app := application() - if c.value != nil { - app.AddLabel(types.LabelBackendHealthcheckPath, *c.value) - } - provider := &Provider{} - actual := provider.hasHealthCheckLabels(app) - if actual != c.expected { - t.Errorf("actual %t, expected %t", actual, c.expected) - } - }) - } -} - -func TestMarathonGetHealthCheckPath(t *testing.T) { - cases := []struct { - desc string - value string - expected string - }{ - { - desc: "label missing", - expected: "", - }, - { - desc: "path existing", - value: "/path", - expected: "/path", - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - app := application() - if c.value != "" { - app.AddLabel(types.LabelBackendHealthcheckPath, c.value) - } - provider := &Provider{} - actual := provider.getHealthCheckPath(app) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) - } - }) - } -} - -func TestMarathonGetHealthCheckInterval(t *testing.T) { - cases := []struct { - desc string - value string - expected string - }{ - { - desc: "label missing", - expected: "", - }, - { - desc: "interval existing", - value: "5m", - expected: "5m", - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - app := application() - if c.value != "" { - app.AddLabel(types.LabelBackendHealthcheckInterval, c.value) - } - provider := &Provider{} - actual := provider.getHealthCheckInterval(app) + prov := &Provider{GroupsAsSubDomains: c.groupAsSubDomain} + actual := prov.getSubDomain(c.path) if actual != c.expected { t.Errorf("actual %q, expected %q", actual, c.expected) } @@ -1427,7 +971,7 @@ func TestGetBackendServer(t *testing.T) { { desc: "multiple task IP addresses with invalid index label", application: application( - label("traefik.ipAddressIdx", "invalid"), + withLabel("traefik.ipAddressIdx", "invalid"), ipAddrPerTask(8000), ), task: task(ipAddresses("1.1.1.1", "2.2.2.2")), @@ -1436,7 +980,7 @@ func TestGetBackendServer(t *testing.T) { { desc: "multiple task IP addresses with valid index label", application: application( - label("traefik.ipAddressIdx", "1"), + withLabel("traefik.ipAddressIdx", "1"), ipAddrPerTask(8000), ), task: task(ipAddresses("1.1.1.1", "2.2.2.2")), @@ -1448,101 +992,12 @@ func TestGetBackendServer(t *testing.T) { c := c t.Run(c.desc, func(t *testing.T) { t.Parallel() - provider := &Provider{ForceTaskHostname: c.forceTaskHostname} + prov := &Provider{ForceTaskHostname: c.forceTaskHostname} c.task.Host = host - actualServer := provider.getBackendServer(c.task, c.application) + actualServer := prov.getBackendServer(c.task, c.application) if actualServer != c.expectedServer { t.Errorf("actual %q, expected %q", actualServer, c.expectedServer) } }) } } - -func TestParseIndex(t *testing.T) { - cases := []struct { - idxStr string - length int - shouldSucceed bool - parsed int - }{ - { - idxStr: "illegal", - length: 42, - shouldSucceed: false, - }, - { - idxStr: "-1", - length: 42, - shouldSucceed: false, - }, - { - idxStr: "10", - length: 1, - shouldSucceed: false, - }, - { - idxStr: "10", - length: 10, - shouldSucceed: false, - }, - { - idxStr: "0", - length: 1, - shouldSucceed: true, - parsed: 0, - }, - { - idxStr: "10", - length: 11, - shouldSucceed: true, - parsed: 10, - }, - } - - for _, c := range cases { - c := c - t.Run(fmt.Sprintf("parseIndex(%s, %d)", c.idxStr, c.length), func(t *testing.T) { - t.Parallel() - parsed, err := parseIndex(c.idxStr, c.length) - - if c.shouldSucceed != (err == nil) { - t.Fatalf("actual error %q, expected error: %t", err, !c.shouldSucceed) - } - - if c.shouldSucceed && parsed != c.parsed { - t.Errorf("actual parsed index %d, expected %d", parsed, c.parsed) - } - }) - } -} - -func TestMarathonGetBasicAuth(t *testing.T) { - cases := []struct { - desc string - application marathon.Application - expected []string - }{ - { - desc: "label missing", - application: application(), - expected: []string{}, - }, - { - desc: "label existing", - application: application(label(types.LabelFrontendAuthBasic, "user:password")), - expected: []string{"user:password"}, - }, - } - - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { - t.Parallel() - provider := &Provider{} - actual := provider.getBasicAuth(c.application, "") - if !reflect.DeepEqual(actual, c.expected) { - t.Errorf("actual %q, expected %q", actual, c.expected) - } - }) - } -} diff --git a/provider/marathon/marathon.go b/provider/marathon/marathon.go index 335481db2..9af7f34ad 100644 --- a/provider/marathon/marathon.go +++ b/provider/marathon/marathon.go @@ -1,19 +1,10 @@ package marathon import ( - "errors" - "fmt" - "math" "net" "net/http" - "net/url" - "regexp" - "strconv" - "strings" - "text/template" "time" - "github.com/BurntSushi/ty/fun" "github.com/Sirupsen/logrus" "github.com/cenk/backoff" "github.com/containous/flaeg" @@ -44,11 +35,13 @@ const ( taskStateStaging TaskState = "TASK_STAGING" ) -var _ provider.Provider = (*Provider)(nil) +const ( + labelIPAddressIdx = "traefik.ipAddressIdx" + labelLbCompatibilityGroup = "HAPROXY_GROUP" + labelLbCompatibility = "HAPROXY_0_VHOST" +) -// Regexp used to extract the name of the service and the name of the property for this service -// All properties are under the format traefik..frontend.*= except the port/portIndex/weight/protocol/backend directly after traefik.. -var servicesPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.(?Pport|portIndex|weight|protocol|backend|frontend\.(.*))$`) +var _ provider.Provider = (*Provider)(nil) // Provider holds configuration of the provider. type Provider struct { @@ -135,7 +128,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s return case event := <-update: log.Debugf("Received provider event %s", event) - configuration := p.loadMarathonConfig() + configuration := p.buildConfiguration() if configuration != nil { configurationChan <- types.ConfigMessage{ ProviderName: "marathon", @@ -146,7 +139,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s } }) } - configuration := p.loadMarathonConfig() + configuration := p.buildConfiguration() configurationChan <- types.ConfigMessage{ ProviderName: "marathon", Configuration: configuration, @@ -163,537 +156,3 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s } return nil } - -func (p *Provider) loadMarathonConfig() *types.Configuration { - var MarathonFuncMap = template.FuncMap{ - "getBackend": p.getBackend, - "getBackendServer": p.getBackendServer, - "getPort": p.getPort, - "getWeight": p.getWeight, - "getDomain": p.getDomain, - "getSubDomain": p.getSubDomain, - "getProtocol": p.getProtocol, - "getPassHostHeader": p.getPassHostHeader, - "getPriority": p.getPriority, - "getEntryPoints": p.getEntryPoints, - "getFrontendRule": p.getFrontendRule, - "getFrontendName": p.getFrontendName, - "hasCircuitBreakerLabels": p.hasCircuitBreakerLabels, - "hasLoadBalancerLabels": p.hasLoadBalancerLabels, - "hasMaxConnLabels": p.hasMaxConnLabels, - "getMaxConnExtractorFunc": p.getMaxConnExtractorFunc, - "getMaxConnAmount": p.getMaxConnAmount, - "getLoadBalancerMethod": p.getLoadBalancerMethod, - "getCircuitBreakerExpression": p.getCircuitBreakerExpression, - "getSticky": p.getSticky, - "hasStickinessLabel": p.hasStickinessLabel, - "getStickinessCookieName": p.getStickinessCookieName, - "hasHealthCheckLabels": p.hasHealthCheckLabels, - "getHealthCheckPath": p.getHealthCheckPath, - "getHealthCheckInterval": p.getHealthCheckInterval, - "hasServices": p.hasServices, - "getServiceNames": p.getServiceNames, - "getServiceNameSuffix": p.getServiceNameSuffix, - "getBasicAuth": p.getBasicAuth, - } - - v := url.Values{} - v.Add("embed", "apps.tasks") - v.Add("embed", "apps.deployments") - v.Add("embed", "apps.readiness") - applications, err := p.marathonClient.Applications(v) - if err != nil { - log.Errorf("Failed to retrieve Marathon applications: %s", err) - return nil - } - - filteredApps := fun.Filter(p.applicationFilter, applications.Apps).([]marathon.Application) - for i, app := range filteredApps { - filteredApps[i].Tasks = fun.Filter(func(task *marathon.Task) bool { - filtered := p.taskFilter(*task, app) - if filtered { - p.logIllegalServices(*task, app) - } - return filtered - }, app.Tasks).([]*marathon.Task) - } - - templateObjects := struct { - Applications []marathon.Application - Domain string - }{ - filteredApps, - p.Domain, - } - - configuration, err := p.GetConfiguration("templates/marathon.tmpl", MarathonFuncMap, templateObjects) - if err != nil { - log.Errorf("failed to render Marathon configuration template: %s", err) - } - return configuration -} - -func (p *Provider) applicationFilter(app marathon.Application) bool { - // Filter disabled application. - if !isApplicationEnabled(app, p.ExposedByDefault) { - log.Debugf("Filtering disabled Marathon application %s", app.ID) - return false - } - - // Filter by constraints. - label, _ := p.getAppLabel(app, types.LabelTags) - constraintTags := strings.Split(label, ",") - if p.MarathonLBCompatibility { - if label, ok := p.getAppLabel(app, "HAPROXY_GROUP"); ok { - constraintTags = append(constraintTags, label) - } - } - if p.FilterMarathonConstraints && app.Constraints != nil { - for _, constraintParts := range *app.Constraints { - constraintTags = append(constraintTags, strings.Join(constraintParts, ":")) - } - } - if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok { - if failingConstraint != nil { - log.Debugf("Filtering Marathon application %v pruned by '%v' constraint", app.ID, failingConstraint.String()) - } - return false - } - - return true -} - -func (p *Provider) taskFilter(task marathon.Task, application marathon.Application) bool { - if task.State != string(taskStateRunning) { - return false - } - - // Filter task with existing, bad health check results. - if application.HasHealthChecks() { - if task.HasHealthCheckResults() { - for _, healthcheck := range task.HealthCheckResults { - if !healthcheck.Alive { - log.Debugf("Filtering Marathon task %s from application %s with bad health check", task.ID, application.ID) - return false - } - } - } - } - - if ready := p.readyChecker.Do(task, application); !ready { - log.Infof("Filtering unready task %s from application %s", task.ID, application.ID) - return false - } - - return true -} - -func isApplicationEnabled(application marathon.Application, exposedByDefault bool) bool { - return exposedByDefault && (*application.Labels)[types.LabelEnable] != "false" || (*application.Labels)[types.LabelEnable] == "true" -} - -// logIllegalServices logs illegal service configurations. -// While we cannot filter on the service level, they will eventually get -// rejected once the server configuration is rendered. -func (p *Provider) logIllegalServices(task marathon.Task, application marathon.Application) { - for _, serviceName := range p.getServiceNames(application) { - // Check for illegal/missing ports. - if _, err := p.processPorts(application, task, serviceName); err != nil { - log.Warnf("%s has an illegal configuration: no proper port available", identifier(application, task, serviceName)) - continue - } - - // Check for illegal port label combinations. - _, hasPortLabel := p.getLabel(application, types.LabelPort, serviceName) - _, hasPortIndexLabel := p.getLabel(application, types.LabelPortIndex, serviceName) - if hasPortLabel && hasPortIndexLabel { - log.Warnf("%s has both port and port index specified; port will take precedence", identifier(application, task, serviceName)) - } - } -} - -//servicePropertyValues is a map of services properties -//an example value is: weight=42 -type servicePropertyValues map[string]string - -//serviceProperties is a map of service properties per service, which we can get with label[serviceName][propertyName]. It yields a property value. -type serviceProperties map[string]servicePropertyValues - -//hasServices checks if there are service-defining labels for the given application -func (p *Provider) hasServices(application marathon.Application) bool { - return len(extractServiceProperties(application.Labels)) > 0 -} - -//extractServiceProperties extracts the service labels for the given application -func extractServiceProperties(labels *map[string]string) serviceProperties { - v := make(serviceProperties) - - if labels != nil { - for label, value := range *labels { - matches := servicesPropertiesRegexp.FindStringSubmatch(label) - if matches == nil { - continue - } - - // According to the regex, match index 1 is "service_name" and match index 2 is the "property_name" - serviceName := matches[1] - propertyName := matches[2] - if _, ok := v[serviceName]; !ok { - v[serviceName] = make(servicePropertyValues) - } - v[serviceName][propertyName] = value - } - } - - return v -} - -//getServiceProperty returns the property for a service label searching in all labels of the given application -func getServiceProperty(application marathon.Application, serviceName string, property string) (string, bool) { - value, ok := extractServiceProperties(application.Labels)[serviceName][property] - return value, ok -} - -//getServiceNames returns a list of service names for a given application -//An empty name "" will be added if no service specific properties exist, as an indication that there are no sub-services, but only main application -func (p *Provider) getServiceNames(application marathon.Application) []string { - labelServiceProperties := extractServiceProperties(application.Labels) - var names []string - - for k := range labelServiceProperties { - names = append(names, k) - } - if len(names) == 0 { - names = append(names, "") - } - return names -} - -func (p *Provider) getServiceNameSuffix(serviceName string) string { - if len(serviceName) > 0 { - serviceName = strings.Replace(serviceName, "/", "-", -1) - serviceName = strings.Replace(serviceName, ".", "-", -1) - return "-service-" + serviceName - } - return "" -} - -//getAppLabel is a convenience function to get application label, when no serviceName is available -//it is identical to calling getLabel(application, label, "") -func (p *Provider) getAppLabel(application marathon.Application, label string) (string, bool) { - return p.getLabel(application, label, "") -} - -//getLabel returns a string value of a corresponding `label` argument -// If serviceName is non-empty, we look for a service label. If none exists or serviceName is empty, we look for an application label. -func (p *Provider) getLabel(application marathon.Application, label string, serviceName string) (string, bool) { - if len(serviceName) > 0 { - property := strings.TrimPrefix(label, types.LabelPrefix) - if value, ok := getServiceProperty(application, serviceName, property); ok { - return value, true - } - } - for key, value := range *application.Labels { - if key == label { - return value, true - } - } - return "", false -} - -func (p *Provider) getPort(task marathon.Task, application marathon.Application, serviceName string) string { - port, err := p.processPorts(application, task, serviceName) - if err != nil { - log.Errorf("Unable to process ports for %s: %s", identifier(application, task, serviceName), err) - return "" - } - - return strconv.Itoa(port) -} - -func (p *Provider) getWeight(application marathon.Application, serviceName string) string { - if label, ok := p.getLabel(application, types.LabelWeight, serviceName); ok { - return label - } - return "0" -} - -func (p *Provider) getDomain(application marathon.Application) string { - if label, ok := p.getAppLabel(application, types.LabelDomain); ok { - return label - } - return p.Domain -} - -func (p *Provider) getProtocol(application marathon.Application, serviceName string) string { - if label, ok := p.getLabel(application, types.LabelProtocol, serviceName); ok { - return label - } - return "http" -} - -func (p *Provider) getSticky(application marathon.Application) string { - if sticky, ok := p.getAppLabel(application, types.LabelBackendLoadbalancerSticky); ok { - log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) - return sticky - } - return "false" -} - -func (p *Provider) hasStickinessLabel(application marathon.Application) bool { - labelStickiness, okStickiness := p.getAppLabel(application, types.LabelBackendLoadbalancerStickiness) - return okStickiness && len(labelStickiness) > 0 && strings.EqualFold(strings.TrimSpace(labelStickiness), "true") -} - -func (p *Provider) getStickinessCookieName(application marathon.Application) string { - if label, ok := p.getAppLabel(application, types.LabelBackendLoadbalancerStickinessCookieName); ok { - return label - } - return "" -} - -func (p *Provider) getPassHostHeader(application marathon.Application, serviceName string) string { - if passHostHeader, ok := p.getLabel(application, types.LabelFrontendPassHostHeader, serviceName); ok { - return passHostHeader - } - return "true" -} - -func (p *Provider) getPriority(application marathon.Application, serviceName string) string { - if priority, ok := p.getLabel(application, types.LabelFrontendPriority, serviceName); ok { - return priority - } - return "0" -} - -func (p *Provider) getEntryPoints(application marathon.Application, serviceName string) []string { - if entryPoints, ok := p.getLabel(application, types.LabelFrontendEntryPoints, serviceName); ok { - return strings.Split(entryPoints, ",") - } - return []string{} -} - -// getFrontendRule returns the frontend rule for the specified application, using -// its label. If service is provided, it will look for serviceName label before generic one. -// It returns a default one (Host) if the label is not present. -func (p *Provider) getFrontendRule(application marathon.Application, serviceName string) string { - if label, ok := p.getLabel(application, types.LabelFrontendRule, serviceName); ok { - return label - } - if p.MarathonLBCompatibility { - if label, ok := p.getAppLabel(application, "HAPROXY_0_VHOST"); ok { - return "Host:" + label - } - } - if len(serviceName) > 0 { - return "Host:" + strings.ToLower(provider.Normalize(serviceName)) + "." + p.getSubDomain(application.ID) + "." + p.Domain - } - return "Host:" + p.getSubDomain(application.ID) + "." + p.Domain -} - -func (p *Provider) getBackend(application marathon.Application, serviceName string) string { - if label, ok := p.getLabel(application, types.LabelBackend, serviceName); ok { - return label - } - return strings.Replace(application.ID, "/", "-", -1) + p.getServiceNameSuffix(serviceName) -} - -func (p *Provider) getFrontendName(application marathon.Application, serviceName string) string { - appName := strings.Replace(application.ID, "/", "-", -1) - return "frontend" + appName + p.getServiceNameSuffix(serviceName) -} - -func (p *Provider) getSubDomain(name string) string { - if p.GroupsAsSubDomains { - splitedName := strings.Split(strings.TrimPrefix(name, "/"), "/") - provider.ReverseStringSlice(&splitedName) - reverseName := strings.Join(splitedName, ".") - return reverseName - } - return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) -} - -func (p *Provider) hasCircuitBreakerLabels(application marathon.Application) bool { - _, ok := p.getAppLabel(application, types.LabelBackendCircuitbreakerExpression) - return ok -} - -func (p *Provider) hasLoadBalancerLabels(application marathon.Application) bool { - _, errMethod := p.getAppLabel(application, types.LabelBackendLoadbalancerMethod) - _, errSticky := p.getAppLabel(application, types.LabelBackendLoadbalancerSticky) - return errMethod || errSticky -} - -func (p *Provider) hasMaxConnLabels(application marathon.Application) bool { - if _, ok := p.getAppLabel(application, types.LabelBackendMaxconnAmount); !ok { - return false - } - _, ok := p.getAppLabel(application, types.LabelBackendMaxconnExtractorfunc) - return ok -} - -func (p *Provider) getMaxConnAmount(application marathon.Application) int64 { - if label, ok := p.getAppLabel(application, types.LabelBackendMaxconnAmount); ok { - 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 (p *Provider) getMaxConnExtractorFunc(application marathon.Application) string { - if label, ok := p.getAppLabel(application, types.LabelBackendMaxconnExtractorfunc); ok { - return label - } - return "request.host" -} - -func (p *Provider) getLoadBalancerMethod(application marathon.Application) string { - if label, ok := p.getAppLabel(application, types.LabelBackendLoadbalancerMethod); ok { - return label - } - return "wrr" -} - -func (p *Provider) getCircuitBreakerExpression(application marathon.Application) string { - if label, ok := p.getAppLabel(application, types.LabelBackendCircuitbreakerExpression); ok { - return label - } - return "NetworkErrorRatio() > 1" -} - -func (p *Provider) hasHealthCheckLabels(application marathon.Application) bool { - return p.getHealthCheckPath(application) != "" -} - -func (p *Provider) getHealthCheckPath(application marathon.Application) string { - if label, ok := p.getAppLabel(application, types.LabelBackendHealthcheckPath); ok { - return label - } - return "" -} - -func (p *Provider) getHealthCheckInterval(application marathon.Application) string { - if label, ok := p.getAppLabel(application, types.LabelBackendHealthcheckInterval); ok { - return label - } - return "" -} - -func (p *Provider) getBasicAuth(application marathon.Application, serviceName string) []string { - if basicAuth, ok := p.getLabel(application, types.LabelFrontendAuthBasic, serviceName); ok { - return strings.Split(basicAuth, ",") - } - - return []string{} -} - -// processPorts returns the configured port. -// An explicitly specified port is preferred. If none is specified, it selects -// one of the available port. The first such found port is returned unless an -// optional index is provided. -func (p *Provider) processPorts(application marathon.Application, task marathon.Task, serviceName string) (int, error) { - if portLabel, ok := p.getLabel(application, types.LabelPort, serviceName); ok { - port, err := strconv.Atoi(portLabel) - switch { - case err != nil: - return 0, fmt.Errorf("failed to parse port label %q: %s", portLabel, err) - case port <= 0: - return 0, fmt.Errorf("explicitly specified port %d must be larger than zero", port) - } - return port, nil - } - - ports := retrieveAvailablePorts(application, task) - if len(ports) == 0 { - return 0, errors.New("no port found") - } - - portIndex := 0 - if portIndexLabel, ok := p.getLabel(application, types.LabelPortIndex, serviceName); ok { - var err error - portIndex, err = parseIndex(portIndexLabel, len(ports)) - if err != nil { - return 0, fmt.Errorf("cannot use port index to select from %d ports: %s", len(ports), err) - } - } - return ports[portIndex], nil -} - -func retrieveAvailablePorts(application marathon.Application, task marathon.Task) []int { - // Using default port configuration - if task.Ports != nil && len(task.Ports) > 0 { - return task.Ports - } - - // Using port definition if available - if application.PortDefinitions != nil && len(*application.PortDefinitions) > 0 { - var ports []int - for _, def := range *application.PortDefinitions { - if def.Port != nil { - ports = append(ports, *def.Port) - } - } - return ports - } - // If using IP-per-task using this port definition - if application.IPAddressPerTask != nil && len(*((*application.IPAddressPerTask).Discovery).Ports) > 0 { - var ports []int - for _, def := range *((*application.IPAddressPerTask).Discovery).Ports { - ports = append(ports, def.Number) - } - return ports - } - - return []int{} -} - -func (p *Provider) getBackendServer(task marathon.Task, application marathon.Application) string { - numTaskIPAddresses := len(task.IPAddresses) - switch { - case application.IPAddressPerTask == nil || p.ForceTaskHostname: - return task.Host - case numTaskIPAddresses == 0: - log.Errorf("Missing IP address for Marathon application %s on task %s", application.ID, task.ID) - return "" - case numTaskIPAddresses == 1: - return task.IPAddresses[0].IPAddress - default: - ipAddressIdxStr, ok := p.getAppLabel(application, "traefik.ipAddressIdx") - if !ok { - log.Errorf("Found %d task IP addresses but missing IP address index for Marathon application %s on task %s", numTaskIPAddresses, application.ID, task.ID) - return "" - } - - ipAddressIdx, err := parseIndex(ipAddressIdxStr, numTaskIPAddresses) - if err != nil { - log.Errorf("Cannot use IP address index to select from %d task IP addresses for Marathon application %s on task %s: %s", numTaskIPAddresses, application.ID, task.ID, err) - return "" - } - - return task.IPAddresses[ipAddressIdx].IPAddress - } -} - -func parseIndex(index string, length int) (int, error) { - parsed, err := strconv.Atoi(index) - switch { - case err != nil: - return 0, fmt.Errorf("failed to parse index %q: %s", index, err) - case parsed < 0, parsed > length-1: - return 0, fmt.Errorf("index %d must be within range (0, %d)", parsed, length-1) - } - - return parsed, nil -} - -func identifier(app marathon.Application, task marathon.Task, serviceName string) string { - id := fmt.Sprintf("Marathon task %s from application %s", task.ID, app.ID) - if serviceName != "" { - id += fmt.Sprintf(" (service: %s)", serviceName) - } - return id -} diff --git a/provider/marathon/readiness.go b/provider/marathon/readiness.go index 6a52ae6b7..d504e9d05 100644 --- a/provider/marathon/readiness.go +++ b/provider/marathon/readiness.go @@ -11,11 +11,11 @@ const ( // readinessCheckDefaultTimeout is the default timeout for a readiness // check if no check timeout is specified on the application spec. This // should really never be the case, but better be safe than sorry. - readinessCheckDefaultTimeout time.Duration = 10 * time.Second + readinessCheckDefaultTimeout = 10 * time.Second // readinessCheckSafetyMargin is some buffer duration to account for // small offsets in readiness check execution. - readinessCheckSafetyMargin time.Duration = 5 * time.Second - readinessLogHeader string = "Marathon readiness check: " + readinessCheckSafetyMargin = 5 * time.Second + readinessLogHeader = "Marathon readiness check: " ) type readinessChecker struct { diff --git a/provider/marathon/readiness_test.go b/provider/marathon/readiness_test.go index edfca8241..8d61c01c4 100644 --- a/provider/marathon/readiness_test.go +++ b/provider/marathon/readiness_test.go @@ -20,7 +20,7 @@ func TestDisabledReadinessChecker(t *testing.T) { readinessCheckResult(testTaskName, false), ) - if ready := rc.Do(tsk, app); ready == false { + if ready := rc.Do(tsk, app); !ready { t.Error("expected ready = true") } } diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index 4f342f02c..649abeb4b 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -3,7 +3,7 @@ {{range $app := $apps}} {{range $task := $app.Tasks}} {{range $serviceIndex, $serviceName := getServiceNames $app}} - [backends."backend{{getBackend $app $serviceName}}".servers."server-{{$task.ID | replace "." "-"}}{{getServiceNameSuffix $serviceName }}"] + [backends."{{getBackend $app $serviceName}}".servers."server-{{$task.ID | replace "." "-"}}{{getServiceNameSuffix $serviceName }}"] url = "{{getProtocol $app $serviceName}}://{{getBackendServer $task $app}}:{{getPort $task $app $serviceName}}" weight = {{getWeight $app $serviceName}} {{end}} @@ -12,27 +12,27 @@ {{range $app := $apps}} {{range $serviceIndex, $serviceName := getServiceNames $app}} -[backends."backend{{getBackend $app $serviceName }}"] +[backends."{{getBackend $app $serviceName }}"] {{ if hasMaxConnLabels $app }} - [backends."backend{{getBackend $app $serviceName }}".maxconn] + [backends."{{getBackend $app $serviceName }}".maxconn] amount = {{getMaxConnAmount $app }} extractorfunc = "{{getMaxConnExtractorFunc $app }}" {{end}} {{ if hasLoadBalancerLabels $app }} - [backends."backend{{getBackend $app $serviceName }}".loadbalancer] + [backends."{{getBackend $app $serviceName }}".loadbalancer] method = "{{getLoadBalancerMethod $app }}" sticky = {{getSticky $app}} {{if hasStickinessLabel $app}} - [backends."backend{{getBackend $app $serviceName }}".loadbalancer.stickiness] + [backends."{{getBackend $app $serviceName }}".loadbalancer.stickiness] cookieName = "{{getStickinessCookieName $app}}" {{end}} {{end}} {{ if hasCircuitBreakerLabels $app }} - [backends."backend{{getBackend $app $serviceName }}".circuitbreaker] + [backends."{{getBackend $app $serviceName }}".circuitbreaker] expression = "{{getCircuitBreakerExpression $app }}" {{end}} {{ if hasHealthCheckLabels $app }} - [backends."backend{{getBackend $app $serviceName }}".healthcheck] + [backends."{{getBackend $app $serviceName }}".healthcheck] path = "{{getHealthCheckPath $app }}" interval = "{{getHealthCheckInterval $app }}" {{end}} @@ -41,7 +41,7 @@ [frontends]{{range $app := $apps}}{{range $serviceIndex, $serviceName := getServiceNames .}} [frontends."{{ getFrontendName $app $serviceName }}"] - backend = "backend{{getBackend $app $serviceName}}" + backend = "{{getBackend $app $serviceName}}" passHostHeader = {{getPassHostHeader $app $serviceName}} priority = {{getPriority $app $serviceName}} entryPoints = [{{range getEntryPoints $app $serviceName}}