From 246b245959f1a16d78a17f13e9bc659bcafb3d47 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 29 Jan 2019 17:54:05 +0100 Subject: [PATCH] Adds Marathon support. Co-authored-by: Julien Salleyron --- cmd/configuration.go | 3 +- config/static/static_config.go | 2 +- integration/integration_test.go | 6 +- integration/marathon15_test.go | 5 +- integration/marathon_test.go | 5 +- log/deprecated.go | 61 +- old/configuration/configuration.go | 2 - old/provider/marathon/config.go | 390 ---- old/provider/marathon/config_segment_test.go | 388 ---- old/provider/marathon/config_test.go | 1341 -------------- old/provider/marathon/convert_types.go | 8 - provider/aggregator/aggregator.go | 4 + .../marathon/builder_test.go | 22 - provider/marathon/config.go | 276 +++ provider/marathon/config_test.go | 1587 +++++++++++++++++ .../marathon/fake_client_test.go | 2 +- provider/marathon/label.go | 51 + provider/marathon/label_test.go | 178 ++ .../marathon/marathon.go | 95 +- .../marathon/mocks/Marathon.go | 0 .../marathon/readiness.go | 0 .../marathon/readiness_test.go | 0 22 files changed, 2223 insertions(+), 2203 deletions(-) delete mode 100644 old/provider/marathon/config.go delete mode 100644 old/provider/marathon/config_segment_test.go delete mode 100644 old/provider/marathon/config_test.go delete mode 100644 old/provider/marathon/convert_types.go rename {old/provider => provider}/marathon/builder_test.go (86%) create mode 100644 provider/marathon/config.go create mode 100644 provider/marathon/config_test.go rename {old/provider => provider}/marathon/fake_client_test.go (90%) create mode 100644 provider/marathon/label.go create mode 100644 provider/marathon/label_test.go rename {old/provider => provider}/marathon/marathon.go (66%) rename {old/provider => provider}/marathon/mocks/Marathon.go (100%) rename {old/provider => provider}/marathon/readiness.go (100%) rename {old/provider => provider}/marathon/readiness_test.go (100%) diff --git a/cmd/configuration.go b/cmd/configuration.go index 1d758c7ae..ac6e4c74b 100644 --- a/cmd/configuration.go +++ b/cmd/configuration.go @@ -15,13 +15,13 @@ import ( "github.com/containous/traefik/old/provider/etcd" "github.com/containous/traefik/old/provider/eureka" "github.com/containous/traefik/old/provider/kubernetes" - "github.com/containous/traefik/old/provider/marathon" "github.com/containous/traefik/old/provider/mesos" "github.com/containous/traefik/old/provider/rancher" "github.com/containous/traefik/old/provider/zk" "github.com/containous/traefik/ping" "github.com/containous/traefik/provider/docker" "github.com/containous/traefik/provider/file" + "github.com/containous/traefik/provider/marathon" "github.com/containous/traefik/provider/rest" "github.com/containous/traefik/tracing/datadog" "github.com/containous/traefik/tracing/jaeger" @@ -170,6 +170,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { defaultMarathon.ResponseHeaderTimeout = parse.Duration(60 * time.Second) defaultMarathon.TLSHandshakeTimeout = parse.Duration(5 * time.Second) defaultMarathon.KeepAlive = parse.Duration(10 * time.Second) + defaultMarathon.DefaultRule = marathon.DefaultTemplateRule // default Consul var defaultConsul consul.Provider diff --git a/config/static/static_config.go b/config/static/static_config.go index f756b26c1..1e0056bf4 100644 --- a/config/static/static_config.go +++ b/config/static/static_config.go @@ -16,7 +16,6 @@ import ( "github.com/containous/traefik/old/provider/etcd" "github.com/containous/traefik/old/provider/eureka" "github.com/containous/traefik/old/provider/kubernetes" - "github.com/containous/traefik/old/provider/marathon" "github.com/containous/traefik/old/provider/mesos" "github.com/containous/traefik/old/provider/rancher" "github.com/containous/traefik/old/provider/zk" @@ -24,6 +23,7 @@ import ( acmeprovider "github.com/containous/traefik/provider/acme" "github.com/containous/traefik/provider/docker" "github.com/containous/traefik/provider/file" + "github.com/containous/traefik/provider/marathon" "github.com/containous/traefik/provider/rest" "github.com/containous/traefik/tls" "github.com/containous/traefik/tracing/datadog" diff --git a/integration/integration_test.go b/integration/integration_test.go index ae09fb392..e83ae4fd6 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -43,12 +43,8 @@ func init() { // check.Suite(&ConsulSuite{}) // check.Suite(&DynamoDBSuite{}) // check.Suite(&EurekaSuite{}) - // check.Suite(&MarathonSuite{}) - // check.Suite(&MarathonSuite15{}) // check.Suite(&MesosSuite{}) - // FIXME use docker - // FIXME use consulcatalog // check.Suite(&ConstraintSuite{}) @@ -64,6 +60,8 @@ func init() { check.Suite(&HostResolverSuite{}) check.Suite(&HTTPSSuite{}) check.Suite(&LogRotationSuite{}) + check.Suite(&MarathonSuite{}) + check.Suite(&MarathonSuite15{}) check.Suite(&RateLimitSuite{}) check.Suite(&RestSuite{}) check.Suite(&RetrySuite{}) diff --git a/integration/marathon15_test.go b/integration/marathon15_test.go index a4c2c1f8c..4db893df4 100644 --- a/integration/marathon15_test.go +++ b/integration/marathon15_test.go @@ -7,7 +7,6 @@ import ( "time" "github.com/containous/traefik/integration/try" - "github.com/containous/traefik/old/provider/label" "github.com/gambol99/go-marathon" "github.com/go-check/check" checker "github.com/vdemeester/shakers" @@ -98,7 +97,7 @@ func (s *MarathonSuite15) TestConfigurationUpdate(c *check.C) { CPU(0.1). Memory(32). EmptyNetworks(). - AddLabel(label.TraefikFrontendRule, "PathPrefix:/service") + AddLabel("traefik.Routers.rt.Rule", "PathPrefix:/service") app.Container. Expose(80). Docker. @@ -118,7 +117,7 @@ func (s *MarathonSuite15) TestConfigurationUpdate(c *check.C) { CPU(0.1). Memory(32). EmptyNetworks(). - AddLabel(label.Prefix+"app"+label.TraefikFrontendRule, "PathPrefix:/app") + AddLabel("traefik.Routers.app.Rule", "PathPrefix:/app") app.Container. Expose(80). Docker. diff --git a/integration/marathon_test.go b/integration/marathon_test.go index f14b8e974..51a042ea5 100644 --- a/integration/marathon_test.go +++ b/integration/marathon_test.go @@ -7,7 +7,6 @@ import ( "time" "github.com/containous/traefik/integration/try" - "github.com/containous/traefik/old/provider/label" "github.com/gambol99/go-marathon" "github.com/go-check/check" checker "github.com/vdemeester/shakers" @@ -109,7 +108,7 @@ func (s *MarathonSuite) TestConfigurationUpdate(c *check.C) { Name("/whoami"). CPU(0.1). Memory(32). - AddLabel(label.TraefikFrontendRule, "PathPrefix:/service") + AddLabel("traefik.Routers.rt.Rule", "PathPrefix:/service") app.Container.Docker.Bridged(). Expose(80). Container("containous/whoami") @@ -126,7 +125,7 @@ func (s *MarathonSuite) TestConfigurationUpdate(c *check.C) { Name("/whoami"). CPU(0.1). Memory(32). - AddLabel(label.Prefix+"app"+label.TraefikFrontendRule, "PathPrefix:/app") + AddLabel("traefik.Routers.app.Rule", "PathPrefix:/app") app.Container.Docker.Bridged(). Expose(80). Container("containous/whoami") diff --git a/log/deprecated.go b/log/deprecated.go index 0bf50c743..7c7f573b3 100644 --- a/log/deprecated.go +++ b/log/deprecated.go @@ -1,6 +1,12 @@ package log -import "github.com/sirupsen/logrus" +import ( + "bufio" + "io" + "runtime" + + "github.com/sirupsen/logrus" +) // Debug logs a message at level Debug on the standard logger. // Deprecated @@ -78,3 +84,56 @@ func Fatalf(format string, args ...interface{}) { func AddHook(hook logrus.Hook) { logrus.AddHook(hook) } + +// CustomWriterLevel logs writer for a specific level. (with a custom scanner buffer size.) +// adapted from github.com/Sirupsen/logrus/writer.go +func CustomWriterLevel(level logrus.Level, maxScanTokenSize int) *io.PipeWriter { + reader, writer := io.Pipe() + + var printFunc func(args ...interface{}) + + switch level { + case logrus.DebugLevel: + printFunc = Debug + case logrus.InfoLevel: + printFunc = Info + case logrus.WarnLevel: + printFunc = Warn + case logrus.ErrorLevel: + printFunc = Error + case logrus.FatalLevel: + printFunc = Fatal + case logrus.PanicLevel: + printFunc = Panic + default: + printFunc = mainLogger.Print + } + + go writerScanner(reader, maxScanTokenSize, printFunc) + runtime.SetFinalizer(writer, writerFinalizer) + + return writer +} + +// extract from github.com/Sirupsen/logrus/writer.go +// Hack the buffer size +func writerScanner(reader io.ReadCloser, scanTokenSize int, printFunc func(args ...interface{})) { + scanner := bufio.NewScanner(reader) + + if scanTokenSize > bufio.MaxScanTokenSize { + buf := make([]byte, bufio.MaxScanTokenSize) + scanner.Buffer(buf, scanTokenSize) + } + + for scanner.Scan() { + printFunc(scanner.Text()) + } + if err := scanner.Err(); err != nil { + Errorf("Error while reading from Writer: %s", err) + } + reader.Close() +} + +func writerFinalizer(writer *io.PipeWriter) { + writer.Close() +} diff --git a/old/configuration/configuration.go b/old/configuration/configuration.go index 01322c0be..3f0e98d19 100644 --- a/old/configuration/configuration.go +++ b/old/configuration/configuration.go @@ -22,7 +22,6 @@ import ( "github.com/containous/traefik/old/provider/etcd" "github.com/containous/traefik/old/provider/eureka" "github.com/containous/traefik/old/provider/kubernetes" - "github.com/containous/traefik/old/provider/marathon" "github.com/containous/traefik/old/provider/mesos" "github.com/containous/traefik/old/provider/rancher" "github.com/containous/traefik/old/provider/rest" @@ -88,7 +87,6 @@ type GlobalConfiguration struct { KeepTrailingSlash bool `description:"Do not remove trailing slash." export:"true"` // Deprecated Docker *docker.Provider `description:"Enable Docker backend with default settings" export:"true"` File *file.Provider `description:"Enable File backend with default settings" export:"true"` - Marathon *marathon.Provider `description:"Enable Marathon backend with default settings" export:"true"` Consul *consul.Provider `description:"Enable Consul backend with default settings" export:"true"` ConsulCatalog *consulcatalog.Provider `description:"Enable Consul catalog backend with default settings" export:"true"` Etcd *etcd.Provider `description:"Enable Etcd backend with default settings" export:"true"` diff --git a/old/provider/marathon/config.go b/old/provider/marathon/config.go deleted file mode 100644 index a74a18f06..000000000 --- a/old/provider/marathon/config.go +++ /dev/null @@ -1,390 +0,0 @@ -package marathon - -import ( - "errors" - "fmt" - "math" - "net" - "strconv" - "strings" - "text/template" - - "github.com/containous/traefik/old/log" - "github.com/containous/traefik/old/provider" - "github.com/containous/traefik/old/provider/label" - "github.com/containous/traefik/old/types" - "github.com/gambol99/go-marathon" -) - -type appData struct { - marathon.Application - SegmentLabels map[string]string - SegmentName string - LinkedApps []*appData -} - -func (p *Provider) buildConfiguration(applications *marathon.Applications) *types.Configuration { - var MarathonFuncMap = template.FuncMap{ - "getDomain": label.GetFuncString(label.TraefikDomain, p.Domain), // see https://github.com/containous/traefik/pull/1693 - "getSubDomain": p.getSubDomain, // see https://github.com/containous/traefik/pull/1693 - "getBackendName": p.getBackendName, - - // Backend functions - "getPort": getPort, - "getCircuitBreaker": label.GetCircuitBreaker, - "getLoadBalancer": label.GetLoadBalancer, - "getMaxConn": label.GetMaxConn, - "getHealthCheck": label.GetHealthCheck, - "getBuffering": label.GetBuffering, - "getResponseForwarding": label.GetResponseForwarding, - "getServers": p.getServers, - - // Frontend functions - "getSegmentNameSuffix": getSegmentNameSuffix, - "getFrontendRule": p.getFrontendRule, - "getFrontendName": p.getFrontendName, - "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), - "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), - "getPassTLSClientCert": label.GetTLSClientCert, - "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriority), - "getEntryPoints": label.GetFuncSliceString(label.TraefikFrontendEntryPoints), - "getBasicAuth": label.GetFuncSliceString(label.TraefikFrontendAuthBasic), // Deprecated - "getAuth": label.GetAuth, - "getRedirect": label.GetRedirect, - "getErrorPages": label.GetErrorPages, - "getRateLimit": label.GetRateLimit, - "getHeaders": label.GetHeaders, - "getWhiteList": label.GetWhiteList, - } - - apps := make(map[string]*appData) - for _, app := range applications.Apps { - if p.applicationFilter(app) { - // Tasks - var filteredTasks []*marathon.Task - for _, task := range app.Tasks { - if p.taskFilter(*task, app) { - filteredTasks = append(filteredTasks, task) - logIllegalServices(*task, app) - } - } - - app.Tasks = filteredTasks - - // segments - segmentProperties := label.ExtractTraefikLabels(stringValueMap(app.Labels)) - for segmentName, labels := range segmentProperties { - data := &appData{ - Application: app, - SegmentLabels: labels, - SegmentName: segmentName, - } - - backendName := p.getBackendName(*data) - if baseApp, ok := apps[backendName]; ok { - baseApp.LinkedApps = append(baseApp.LinkedApps, data) - } else { - apps[backendName] = data - } - } - } - } - - templateObjects := struct { - Applications map[string]*appData - Domain string - }{ - Applications: apps, - Domain: p.Domain, - } - - 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.IsEnabled(stringValueMap(app.Labels), p.ExposedByDefault) { - log.Debugf("Filtering disabled Marathon application %s", app.ID) - return false - } - - // Filter by constraints. - constraintTags := label.GetSliceStringValue(stringValueMap(app.Labels), label.TraefikTags) - if p.MarathonLBCompatibility { - if haGroup := label.GetStringValue(stringValueMap(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 - } - - 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 -} - -// 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, app marathon.Application) { - segmentProperties := label.ExtractTraefikLabels(stringValueMap(app.Labels)) - for segmentName, labels := range segmentProperties { - // Check for illegal/missing ports. - if _, err := processPorts(app, task, labels); err != nil { - log.Warnf("%s has an illegal configuration: no proper port available", identifier(app, task, segmentName)) - continue - } - - // Check for illegal port label combinations. - hasPortLabel := label.Has(labels, label.TraefikPort) - hasPortIndexLabel := label.Has(labels, label.TraefikPortIndex) - if hasPortLabel && hasPortIndexLabel { - log.Warnf("%s has both port and port index specified; port will take precedence", identifier(app, task, segmentName)) - } - } -} - -func getSegmentNameSuffix(serviceName string) string { - if len(serviceName) > 0 { - return "-service-" + provider.Normalize(serviceName) - } - return "" -} - -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) 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)) -} - -func (p *Provider) getFrontendName(app appData) string { - return provider.Normalize("frontend" + app.ID + getSegmentNameSuffix(app.SegmentName)) -} - -// 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(app appData) string { - if value := label.GetStringValue(app.SegmentLabels, label.TraefikFrontendRule, ""); len(value) > 0 { - return value - } - - if p.MarathonLBCompatibility { - if value := label.GetStringValue(stringValueMap(app.Labels), labelLbCompatibility, ""); len(value) > 0 { - return "Host:" + value - } - } - - domain := label.GetStringValue(app.SegmentLabels, label.TraefikDomain, p.Domain) - if len(domain) > 0 { - domain = "." + domain - } - - if len(app.SegmentName) > 0 { - return "Host:" + strings.ToLower(provider.Normalize(app.SegmentName)) + "." + p.getSubDomain(app.ID) + domain - } - return "Host:" + p.getSubDomain(app.ID) + domain -} - -func getPort(task marathon.Task, app appData) string { - port, err := processPorts(app.Application, task, app.SegmentLabels) - if err != nil { - log.Errorf("Unable to process ports for %s: %s", identifier(app.Application, task, app.SegmentName), 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(app marathon.Application, task marathon.Task, labels map[string]string) (int, error) { - if label.Has(labels, label.TraefikPort) { - port := label.GetIntValue(labels, label.TraefikPort, 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(app, task) - if len(ports) == 0 { - return 0, errors.New("no port found") - } - - portIndex := label.GetIntValue(labels, label.TraefikPortIndex, 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(app marathon.Application, task marathon.Task) []int { - // Using default port configuration - if len(task.Ports) > 0 { - return task.Ports - } - - // Using port definition if available - if app.PortDefinitions != nil && len(*app.PortDefinitions) > 0 { - var ports []int - for _, def := range *app.PortDefinitions { - if def.Port != nil { - ports = append(ports, *def.Port) - } - } - return ports - } - - // If using IP-per-task using this port definition - if app.IPAddressPerTask != nil && app.IPAddressPerTask.Discovery != nil && len(*(app.IPAddressPerTask.Discovery.Ports)) > 0 { - var ports []int - for _, def := range *(app.IPAddressPerTask.Discovery.Ports) { - ports = append(ports, def.Number) - } - return ports - } - - return []int{} -} - -func identifier(app marathon.Application, task marathon.Task, segmentName string) string { - id := fmt.Sprintf("Marathon task %s from application %s", task.ID, app.ID) - if segmentName != "" { - id += fmt.Sprintf(" (segment: %s)", segmentName) - } - return id -} - -func (p *Provider) getServers(app appData) map[string]types.Server { - var servers map[string]types.Server - - for _, task := range app.Tasks { - name, server, err := p.getServer(app, *task) - if err != nil { - log.Error(err) - continue - } - - if servers == nil { - servers = make(map[string]types.Server) - } - - servers[name] = *server - } - - 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) 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", protocol, net.JoinHostPort(host, port)), - Weight: label.GetIntValue(app.SegmentLabels, label.TraefikWeight, label.DefaultWeight), - }, nil -} - -func (p *Provider) getServerHost(task marathon.Task, app appData) (string, error) { - networks := app.Networks - var hostFlag bool - - if networks == nil { - hostFlag = app.IPAddressPerTask == nil - } else { - hostFlag = (*networks)[0].Mode != marathon.ContainerNetworkMode - } - - if hostFlag || p.ForceTaskHostname { - if len(task.Host) == 0 { - return "", fmt.Errorf("host is undefined for task %q app %q", task.ID, app.ID) - } - return task.Host, nil - } - - numTaskIPAddresses := len(task.IPAddresses) - switch numTaskIPAddresses { - case 0: - return "", fmt.Errorf("missing IP address for Marathon application %s on task %s", app.ID, task.ID) - case 1: - return task.IPAddresses[0].IPAddress, nil - default: - ipAddressIdx := label.GetIntValue(stringValueMap(app.Labels), labelIPAddressIdx, math.MinInt32) - - if ipAddressIdx == math.MinInt32 { - 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) - } - if ipAddressIdx < 0 || ipAddressIdx > numTaskIPAddresses { - 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 task.IPAddresses[ipAddressIdx].IPAddress, nil - } -} diff --git a/old/provider/marathon/config_segment_test.go b/old/provider/marathon/config_segment_test.go deleted file mode 100644 index a1575f9df..000000000 --- a/old/provider/marathon/config_segment_test.go +++ /dev/null @@ -1,388 +0,0 @@ -package marathon - -import ( - "testing" - "time" - - "github.com/containous/flaeg/parse" - "github.com/containous/traefik/old/provider/label" - "github.com/containous/traefik/old/types" - "github.com/gambol99/go-marathon" - "github.com/stretchr/testify/assert" -) - -func TestBuildConfigurationSegments(t *testing.T) { - testCases := []struct { - desc string - applications *marathon.Applications - expectedFrontends map[string]*types.Frontend - expectedBackends map[string]*types.Backend - }{ - { - 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"), - 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.marathon.localhost", - }, - }, - PassHostHeader: true, - EntryPoints: []string{}, - }, - "frontend-app-service-admin": { - Backend: "backend-app-service-admin", - Routes: map[string]types.Route{ - `route-host-app-service-admin`: { - Rule: "Host:admin.app.marathon.localhost", - }, - }, - PassHostHeader: true, - EntryPoints: []string{}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app-service-web": { - Servers: map[string]types.Server{ - "server-app-taskID-service-web": { - URL: "http://localhost:80", - Weight: label.DefaultWeight, - }, - }, - MaxConn: &types.MaxConn{ - Amount: 1000, - ExtractorFunc: "client.ip", - }, - }, - "backend-app-service-admin": { - Servers: map[string]types.Server{ - "server-app-taskID-service-admin": { - URL: "http://localhost:81", - Weight: label.DefaultWeight, - }, - }, - MaxConn: &types.MaxConn{ - Amount: 1000, - ExtractorFunc: "client.ip", - }, - }, - }, - }, - { - desc: "when all labels are set", - applications: withApplications( - application( - appID("/app"), - appPorts(80, 81), - withTasks(localhostTask(taskPorts(80, 81))), - - // withLabel(label.TraefikBackend, "foobar"), - - withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), - withLabel(label.TraefikBackendHealthCheckPath, "/health"), - withLabel(label.TraefikBackendHealthCheckPort, "880"), - withLabel(label.TraefikBackendHealthCheckInterval, "6"), - withLabel(label.TraefikBackendHealthCheckTimeout, "3"), - 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"), - - withSegmentLabel(label.TraefikPort, "80", "containous"), - withSegmentLabel(label.TraefikProtocol, "https", "containous"), - withSegmentLabel(label.TraefikWeight, "12", "containous"), - - withSegmentLabel(label.TraefikFrontendPassTLSClientCertPem, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosNotBefore, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosNotAfter, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosSans, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerCommonName, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerCountry, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerDomainComponent, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerLocality, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerOrganization, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerProvince, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerSerialNumber, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectCommonName, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectCountry, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectDomainComponent, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectLocality, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectOrganization, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectProvince, "true", "containous"), - withSegmentLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectSerialNumber, "true", "containous"), - - withSegmentLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "containous"), - withSegmentLabel(label.TraefikFrontendAuthBasicRemoveHeader, "true", "containous"), - withSegmentLabel(label.TraefikFrontendAuthBasicUsers, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "containous"), - withSegmentLabel(label.TraefikFrontendAuthBasicUsersFile, ".htpasswd", "containous"), - withSegmentLabel(label.TraefikFrontendAuthDigestRemoveHeader, "true", "containous"), - withSegmentLabel(label.TraefikFrontendAuthDigestUsers, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "containous"), - withSegmentLabel(label.TraefikFrontendAuthDigestUsersFile, ".htpasswd", "containous"), - withSegmentLabel(label.TraefikFrontendAuthForwardAddress, "auth.server", "containous"), - withSegmentLabel(label.TraefikFrontendAuthForwardTrustForwardHeader, "true", "containous"), - withSegmentLabel(label.TraefikFrontendAuthForwardTLSCa, "ca.crt", "containous"), - withSegmentLabel(label.TraefikFrontendAuthForwardTLSCaOptional, "true", "containous"), - withSegmentLabel(label.TraefikFrontendAuthForwardTLSCert, "server.crt", "containous"), - withSegmentLabel(label.TraefikFrontendAuthForwardTLSKey, "server.key", "containous"), - withSegmentLabel(label.TraefikFrontendAuthForwardTLSInsecureSkipVerify, "true", "containous"), - withSegmentLabel(label.TraefikFrontendAuthHeaderField, "X-WebAuth-User", "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.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.TraefikFrontendSSLForceHost, "true", "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"), - - 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{ - "http", - "https", - }, - Backend: "backend-app-service-containous", - Routes: map[string]types.Route{ - "route-host-app-service-containous": { - Rule: "Host:traefik.io", - }, - }, - PassHostHeader: true, - PassTLSCert: true, - Priority: 666, - PassTLSClientCert: &types.TLSClientHeaders{ - PEM: true, - Infos: &types.TLSClientCertificateInfos{ - NotBefore: true, - Sans: true, - NotAfter: true, - Subject: &types.TLSCLientCertificateDNInfos{ - CommonName: true, - Country: true, - DomainComponent: true, - Locality: true, - Organization: true, - Province: true, - SerialNumber: true, - }, - Issuer: &types.TLSCLientCertificateDNInfos{ - CommonName: true, - Country: true, - DomainComponent: true, - Locality: true, - Organization: true, - Province: true, - SerialNumber: true, - }, - }, - }, - Auth: &types.Auth{ - HeaderField: "X-WebAuth-User", - Basic: &types.Basic{ - RemoveHeader: true, - Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", - "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - UsersFile: ".htpasswd", - }, - }, - WhiteList: &types.WhiteList{ - SourceRange: []string{"10.10.10.10"}, - }, - Headers: &types.Headers{ - CustomRequestHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - CustomResponseHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - AllowedHosts: []string{ - "foo", - "bar", - "bor", - }, - HostsProxyHeaders: []string{ - "foo", - "bar", - "bor", - }, - SSLRedirect: true, - SSLTemporaryRedirect: true, - SSLForceHost: true, - SSLHost: "foo", - SSLProxyHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - STSSeconds: 666, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - CustomFrameOptionsValue: "foo", - ContentTypeNosniff: true, - BrowserXSSFilter: true, - CustomBrowserXSSValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - IsDevelopment: true, - }, - Errors: map[string]*types.ErrorPage{ - "bar": { - Status: []string{ - "500", - "600", - }, - Backend: "backendfoobar", - Query: "bar_query", - }, - "foo": { - Status: []string{ - "404", - }, - Backend: "backendfoobar", - Query: "foo_query", - }, - }, - RateLimit: &types.RateLimit{ - RateSet: map[string]*types.Rate{ - "bar": { - Period: parse.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - "foo": { - Period: parse.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - }, - ExtractorFunc: "client.ip", - }, - Redirect: &types.Redirect{ - EntryPoint: "https", - Permanent: true, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app-service-containous": { - Servers: map[string]types.Server{ - "server-app-taskID-service-containous": { - URL: "https://localhost:80", - Weight: 12, - }, - }, - CircuitBreaker: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - LoadBalancer: &types.LoadBalancer{ - Method: "drr", - Stickiness: &types.Stickiness{ - CookieName: "chocolate", - }, - }, - MaxConn: &types.MaxConn{ - Amount: 666, - ExtractorFunc: "client.ip", - }, - HealthCheck: &types.HealthCheck{ - Path: "/health", - Port: 880, - Interval: "6", - Timeout: "3", - }, - Buffering: &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() - - p := &Provider{ - Domain: "marathon.localhost", - ExposedByDefault: true, - } - - actualConfig := p.buildConfiguration(test.applications) - - assert.NotNil(t, actualConfig) - assert.Equal(t, test.expectedBackends, actualConfig.Backends) - assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) - }) - } -} diff --git a/old/provider/marathon/config_test.go b/old/provider/marathon/config_test.go deleted file mode 100644 index 114645103..000000000 --- a/old/provider/marathon/config_test.go +++ /dev/null @@ -1,1341 +0,0 @@ -package marathon - -import ( - "fmt" - "testing" - "time" - - "github.com/containous/flaeg/parse" - "github.com/containous/traefik/old/provider/label" - "github.com/containous/traefik/old/types" - "github.com/gambol99/go-marathon" - "github.com/stretchr/testify/assert" -) - -func TestGetConfigurationAPIErrors(t *testing.T) { - fakeClient := newFakeClient(true, marathon.Applications{}) - - p := &Provider{ - marathonClient: fakeClient, - } - - actualConfig := p.getConfiguration() - fakeClient.AssertExpectations(t) - - if actualConfig != nil { - t.Errorf("configuration should have been nil, got %v", actualConfig) - } -} - -func TestBuildConfiguration(t *testing.T) { - testCases := []struct { - desc string - applications *marathon.Applications - expectedFrontends map[string]*types.Frontend - expectedBackends map[string]*types.Backend - }{ - { - desc: "simple application", - 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.marathon.localhost", - }, - }, - PassHostHeader: true, - EntryPoints: []string{}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": { - Servers: map[string]types.Server{ - "server-app-taskID": { - URL: "http://localhost:80", - Weight: label.DefaultWeight, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - { - desc: "filtered task", - 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.marathon.localhost", - }, - }, - PassHostHeader: true, - 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, - EntryPoints: []string{}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": { - Servers: map[string]types.Server{ - "server-app-taskID": { - URL: "http://localhost:80", - Weight: label.DefaultWeight, - }, - }, - MaxConn: nil, - }, - }, - }, - { - desc: "multiple ports", - 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.marathon.localhost", - }, - }, - PassHostHeader: true, - EntryPoints: []string{}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": { - Servers: map[string]types.Server{ - "server-app-taskID": { - URL: "http://localhost:80", - Weight: label.DefaultWeight, - }, - }, - }, - }, - }, - { - desc: "with basic auth", - applications: withApplications( - application( - appID("/app"), - appPorts(80), - withLabel(label.TraefikFrontendAuthHeaderField, "X-WebAuth-User"), - withLabel(label.TraefikFrontendAuthBasicRemoveHeader, "true"), - withLabel(label.TraefikFrontendAuthBasicUsers, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - withLabel(label.TraefikFrontendAuthBasicUsersFile, ".htpasswd"), - 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.marathon.localhost", - }, - }, - Auth: &types.Auth{ - HeaderField: "X-WebAuth-User", - Basic: &types.Basic{ - RemoveHeader: true, - Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", - "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - UsersFile: ".htpasswd", - }, - }, - PassHostHeader: true, - EntryPoints: []string{}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": { - Servers: map[string]types.Server{ - "server-app-taskID": { - URL: "http://localhost:80", - Weight: label.DefaultWeight, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - { - desc: "with basic auth with backward compatibility", - applications: withApplications( - application( - appID("/app"), - appPorts(80), - withLabel(label.TraefikFrontendAuthHeaderField, "X-WebAuth-User"), - withLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - 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.marathon.localhost", - }, - }, - Auth: &types.Auth{ - HeaderField: "X-WebAuth-User", - Basic: &types.Basic{ - Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", - "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - }, - }, - PassHostHeader: true, - EntryPoints: []string{}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": { - Servers: map[string]types.Server{ - "server-app-taskID": { - URL: "http://localhost:80", - Weight: label.DefaultWeight, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - { - desc: "with digest auth", - applications: withApplications( - application( - appID("/app"), - appPorts(80), - withLabel(label.TraefikFrontendAuthHeaderField, "X-WebAuth-User"), - withLabel(label.TraefikFrontendAuthDigestRemoveHeader, "true"), - withLabel(label.TraefikFrontendAuthDigestUsers, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - withLabel(label.TraefikFrontendAuthDigestUsersFile, ".htpasswd"), - 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.marathon.localhost", - }, - }, - Auth: &types.Auth{ - HeaderField: "X-WebAuth-User", - Digest: &types.Digest{ - RemoveHeader: true, - Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", - "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - UsersFile: ".htpasswd", - }, - }, - PassHostHeader: true, - EntryPoints: []string{}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": { - Servers: map[string]types.Server{ - "server-app-taskID": { - URL: "http://localhost:80", - Weight: label.DefaultWeight, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - { - desc: "with forward auth", - applications: withApplications( - application( - appID("/app"), - appPorts(80), - withLabel(label.TraefikFrontendAuthHeaderField, "X-WebAuth-User"), - withLabel(label.TraefikFrontendAuthForwardAddress, "auth.server"), - withLabel(label.TraefikFrontendAuthForwardTrustForwardHeader, "true"), - withLabel(label.TraefikFrontendAuthForwardTLSCa, "ca.crt"), - withLabel(label.TraefikFrontendAuthForwardTLSCaOptional, "true"), - withLabel(label.TraefikFrontendAuthForwardTLSCert, "server.crt"), - withLabel(label.TraefikFrontendAuthForwardTLSKey, "server.key"), - withLabel(label.TraefikFrontendAuthForwardTLSInsecureSkipVerify, "true"), - withLabel(label.TraefikFrontendAuthForwardAuthResponseHeaders, "X-Auth-User,X-Auth-Token"), - - 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.marathon.localhost", - }, - }, - Auth: &types.Auth{ - HeaderField: "X-WebAuth-User", - Forward: &types.Forward{ - Address: "auth.server", - TLS: &types.ClientTLS{ - CA: "ca.crt", - CAOptional: true, - InsecureSkipVerify: true, - Cert: "server.crt", - Key: "server.key", - }, - TrustForwardHeader: true, - AuthResponseHeaders: []string{"X-Auth-User", "X-Auth-Token"}, - }, - }, - PassHostHeader: true, - EntryPoints: []string{}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-app": { - Servers: map[string]types.Server{ - "server-app-taskID": { - URL: "http://localhost:80", - Weight: label.DefaultWeight, - }, - }, - CircuitBreaker: nil, - }, - }, - }, - { - desc: "with all labels", - 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.TraefikBackend, "foobar"), - - withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), - withLabel(label.TraefikBackendResponseForwardingFlushInterval, "10ms"), - withLabel(label.TraefikBackendHealthCheckScheme, "http"), - withLabel(label.TraefikBackendHealthCheckPath, "/health"), - withLabel(label.TraefikBackendHealthCheckPort, "880"), - withLabel(label.TraefikBackendHealthCheckInterval, "6"), - withLabel(label.TraefikBackendHealthCheckTimeout, "3"), - withLabel(label.TraefikBackendHealthCheckHostname, "foo.com"), - withLabel(label.TraefikBackendHealthCheckHeaders, "Foo:bar || Bar:foo"), - - 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.TraefikFrontendPassTLSClientCertPem, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosNotBefore, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosNotAfter, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosSans, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerCommonName, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerCountry, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerDomainComponent, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerLocality, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerOrganization, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerProvince, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosIssuerSerialNumber, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectCommonName, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectCountry, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectDomainComponent, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectLocality, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectOrganization, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectProvince, "true"), - withLabel(label.TraefikFrontendPassTLSClientCertInfosSubjectSerialNumber, "true"), - - withLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - withLabel(label.TraefikFrontendAuthBasicRemoveHeader, "true"), - withLabel(label.TraefikFrontendAuthBasicUsers, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - withLabel(label.TraefikFrontendAuthBasicUsersFile, ".htpasswd"), - withLabel(label.TraefikFrontendAuthDigestRemoveHeader, "true"), - withLabel(label.TraefikFrontendAuthDigestUsers, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - withLabel(label.TraefikFrontendAuthDigestUsersFile, ".htpasswd"), - withLabel(label.TraefikFrontendAuthForwardAddress, "auth.server"), - withLabel(label.TraefikFrontendAuthForwardTrustForwardHeader, "true"), - withLabel(label.TraefikFrontendAuthForwardTLSCa, "ca.crt"), - withLabel(label.TraefikFrontendAuthForwardTLSCaOptional, "true"), - withLabel(label.TraefikFrontendAuthForwardTLSCert, "server.crt"), - withLabel(label.TraefikFrontendAuthForwardTLSKey, "server.key"), - withLabel(label.TraefikFrontendAuthForwardTLSInsecureSkipVerify, "true"), - withLabel(label.TraefikFrontendAuthHeaderField, "X-WebAuth-User"), - - 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.TraefikFrontendWhiteListIPStrategyExcludedIPS, "10.10.10.10,10.10.10.11"), - withLabel(label.TraefikFrontendWhiteListIPStrategyDepth, "5"), - - 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.TraefikFrontendSSLForceHost, "true"), - 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"), - )), - expectedFrontends: map[string]*types.Frontend{ - "frontend-app": { - EntryPoints: []string{ - "http", - "https", - }, - Backend: "backendfoobar", - Routes: map[string]types.Route{ - "route-host-app": { - Rule: "Host:traefik.io", - }, - }, - PassHostHeader: true, - PassTLSCert: true, - Priority: 666, - PassTLSClientCert: &types.TLSClientHeaders{ - PEM: true, - Infos: &types.TLSClientCertificateInfos{ - NotBefore: true, - Sans: true, - NotAfter: true, - Subject: &types.TLSCLientCertificateDNInfos{ - CommonName: true, - Country: true, - DomainComponent: true, - Locality: true, - Organization: true, - Province: true, - SerialNumber: true, - }, - Issuer: &types.TLSCLientCertificateDNInfos{ - CommonName: true, - Country: true, - DomainComponent: true, - Locality: true, - Organization: true, - Province: true, - SerialNumber: true, - }, - }, - }, - Auth: &types.Auth{ - HeaderField: "X-WebAuth-User", - Basic: &types.Basic{ - RemoveHeader: true, - Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", - "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - UsersFile: ".htpasswd", - }, - }, - WhiteList: &types.WhiteList{ - SourceRange: []string{"10.10.10.10"}, - IPStrategy: &types.IPStrategy{ - Depth: 5, - ExcludedIPs: []string{"10.10.10.10", "10.10.10.11"}, - }, - }, - Headers: &types.Headers{ - CustomRequestHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - CustomResponseHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - AllowedHosts: []string{ - "foo", - "bar", - "bor", - }, - HostsProxyHeaders: []string{ - "foo", - "bar", - "bor", - }, - SSLRedirect: true, - SSLTemporaryRedirect: true, - SSLForceHost: true, - SSLHost: "foo", - SSLProxyHeaders: map[string]string{ - "Access-Control-Allow-Methods": "POST,GET,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }, - STSSeconds: 666, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - CustomFrameOptionsValue: "foo", - ContentTypeNosniff: true, - BrowserXSSFilter: true, - CustomBrowserXSSValue: "foo", - ContentSecurityPolicy: "foo", - PublicKey: "foo", - ReferrerPolicy: "foo", - IsDevelopment: true, - }, - Errors: map[string]*types.ErrorPage{ - "bar": { - Status: []string{ - "500", - "600", - }, - Backend: "backendfoobar", - Query: "bar_query", - }, - "foo": { - Status: []string{ - "404", - }, - Backend: "backendfoobar", - Query: "foo_query", - }, - }, - RateLimit: &types.RateLimit{ - RateSet: map[string]*types.Rate{ - "bar": { - Period: parse.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - "foo": { - Period: parse.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - }, - ExtractorFunc: "client.ip", - }, - Redirect: &types.Redirect{ - EntryPoint: "https", - Permanent: true, - }, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backendfoobar": { - Servers: map[string]types.Server{ - "server-app-taskID": { - URL: "https://127.0.0.1:666", - Weight: 12, - }, - }, - CircuitBreaker: &types.CircuitBreaker{ - Expression: "NetworkErrorRatio() > 0.5", - }, - ResponseForwarding: &types.ResponseForwarding{ - FlushInterval: "10ms", - }, - LoadBalancer: &types.LoadBalancer{ - Method: "drr", - Stickiness: &types.Stickiness{ - CookieName: "chocolate", - }, - }, - MaxConn: &types.MaxConn{ - Amount: 666, - ExtractorFunc: "client.ip", - }, - HealthCheck: &types.HealthCheck{ - Scheme: "http", - Path: "/health", - Port: 880, - Interval: "6", - Timeout: "3", - Hostname: "foo.com", - Headers: map[string]string{ - "Foo": "bar", - "Bar": "foo", - }, - }, - Buffering: &types.Buffering{ - MaxResponseBodyBytes: 10485760, - MemResponseBodyBytes: 2097152, - MaxRequestBodyBytes: 10485760, - MemRequestBodyBytes: 2097152, - RetryExpression: "IsNetworkError() && Attempts() <= 2", - }, - }, - }, - }, - { - 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, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backendtest-foo": { - Servers: map[string]types.Server{ - "server-foo-v000-taskID-service-main": { - URL: "http://localhost:8080", - Weight: label.DefaultWeight, - }, - "server-foo-v001-taskID-service-main": { - URL: "http://localhost:8081", - Weight: label.DefaultWeight, - }, - }, - }, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - p := &Provider{ - Domain: "marathon.localhost", - ExposedByDefault: true, - } - - actualConfig := p.buildConfiguration(test.applications) - - assert.NotNil(t, actualConfig) - assert.Equal(t, test.expectedBackends, actualConfig.Backends) - assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) - }) - } -} - -func TestApplicationFilterConstraints(t *testing.T) { - testCases := []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 _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - p := &Provider{ - ExposedByDefault: true, - MarathonLBCompatibility: test.marathonLBCompatibility, - FilterMarathonConstraints: test.filterMarathonConstraints, - } - - constraint, err := types.NewConstraint("tag==valid") - if err != nil { - t.Fatalf("failed to create constraint 'tag==valid': %v", err) - } - p.Constraints = types.Constraints{constraint} - - actual := p.applicationFilter(test.application) - - if actual != test.expected { - t.Errorf("got %v, expected %v", actual, test.expected) - } - }) - } -} - -func TestApplicationFilterEnabled(t *testing.T) { - testCases := []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 _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - provider := &Provider{ExposedByDefault: test.exposedByDefault} - - app := application(withLabel(label.TraefikEnable, test.enabledLabel)) - - if provider.applicationFilter(app) != test.expected { - t.Errorf("got unexpected filtering = %t", !test.expected) - } - }) - } -} - -func TestTaskFilter(t *testing.T) { - testCases := []struct { - desc string - task marathon.Task - application marathon.Application - readyChecker *readinessChecker - expected bool - }{ - { - desc: "missing port", - task: task(), - application: application(), - expected: true, - }, - { - desc: "task not running", - task: task( - taskPorts(80), - taskState(taskStateStaging), - ), - application: application(appPorts(80)), - expected: false, - }, - { - desc: "existing port", - task: task(taskPorts(80)), - application: application(appPorts(80)), - expected: true, - }, - { - desc: "ambiguous port specification", - task: task(taskPorts(80, 443)), - application: application( - appPorts(80, 443), - withLabel(label.TraefikPort, "443"), - withLabel(label.TraefikPortIndex, "1"), - ), - expected: true, - }, - { - desc: "single service without port", - task: task(taskPorts(80, 81)), - application: application( - appPorts(80, 81), - withSegmentLabel(label.TraefikPort, "80", "web"), - withSegmentLabel(label.TraefikPort, "illegal", "admin"), - ), - expected: true, - }, - { - desc: "single service missing port", - task: task(taskPorts(80, 81)), - application: application( - appPorts(80, 81), - withSegmentLabel(label.TraefikPort, "81", "admin"), - ), - expected: true, - }, - { - desc: "readiness check false", - task: task(taskPorts(80)), - application: application( - appPorts(80), - deployments("deploymentId"), - readinessCheck(0), - readinessCheckResult(testTaskName, false), - ), - readyChecker: testReadinessChecker(), - expected: false, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - p := &Provider{readyChecker: test.readyChecker} - - actual := p.taskFilter(test.task, test.application) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetSubDomain(t *testing.T) { - testCases := []struct { - path string - expected string - groupAsSubDomain bool - }{ - {"/test", "test", false}, - {"/test", "test", true}, - {"/a/b/c/d", "d.c.b.a", true}, - {"/b/a/d/c", "c.d.a.b", true}, - {"/d/c/b/a", "a.b.c.d", true}, - {"/c/d/a/b", "b.a.d.c", true}, - {"/a/b/c/d", "a-b-c-d", false}, - {"/b/a/d/c", "b-a-d-c", false}, - {"/d/c/b/a", "d-c-b-a", false}, - {"/c/d/a/b", "c-d-a-b", false}, - } - - for _, test := range testCases { - test := test - t.Run(fmt.Sprintf("path=%s,group=%t", test.path, test.groupAsSubDomain), func(t *testing.T) { - t.Parallel() - - p := &Provider{GroupsAsSubDomains: test.groupAsSubDomain} - - actual := p.getSubDomain(test.path) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetPort(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - task marathon.Task - segmentName string - expected string - }{ - { - desc: "port missing", - application: application(), - task: task(), - expected: "", - }, - { - desc: "numeric port", - application: application(withLabel(label.TraefikPort, "80")), - task: task(), - expected: "80", - }, - { - desc: "string port", - application: application(withLabel(label.TraefikPort, "foobar")), - task: task(taskPorts(80)), - expected: "", - }, - { - desc: "negative port", - application: application(withLabel(label.TraefikPort, "-1")), - task: task(taskPorts(80)), - expected: "", - }, - { - desc: "task port available", - application: application(), - task: task(taskPorts(80)), - expected: "80", - }, - { - desc: "port definition available", - application: application( - portDefinition(443), - ), - task: task(), - expected: "443", - }, - { - desc: "IP-per-task port available", - application: application(ipAddrPerTask(8000)), - task: task(), - expected: "8000", - }, - { - desc: "multiple task ports available", - application: application(), - task: task(taskPorts(80, 443)), - expected: "80", - }, - { - desc: "numeric port index specified", - application: application(withLabel(label.TraefikPortIndex, "1")), - task: task(taskPorts(80, 443)), - expected: "443", - }, - { - desc: "string port index specified", - application: application(withLabel(label.TraefikPortIndex, "foobar")), - task: task(taskPorts(80)), - expected: "80", - }, - { - desc: "port and port index specified", - application: application( - withLabel(label.TraefikPort, "80"), - withLabel(label.TraefikPortIndex, "1"), - ), - task: task(taskPorts(80, 443)), - expected: "80", - }, - { - desc: "task and application ports specified", - application: application(appPorts(9999)), - task: task(taskPorts(7777)), - expected: "7777", - }, - { - desc: "multiple task ports with service index available", - application: application(withSegmentLabel(label.TraefikPortIndex, "0", "http")), - task: task(taskPorts(80, 443)), - segmentName: "http", - expected: "80", - }, - { - desc: "multiple task ports with service port available", - application: application(withSegmentLabel(label.TraefikPort, "443", "https")), - task: task(taskPorts(80, 443)), - segmentName: "https", - expected: "443", - }, - { - desc: "multiple task ports with services but default port available", - application: application(withSegmentLabel(label.TraefikWeight, "100", "http")), - task: task(taskPorts(80, 443)), - segmentName: "http", - expected: "80", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getPort(test.task, withAppData(test.application, test.segmentName)) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetFrontendRule(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - segmentName string - expected string - marathonLBCompatibility bool - }{ - { - desc: "label missing", - application: application(appID("test")), - marathonLBCompatibility: true, - expected: "Host:test.marathon.localhost", - }, - { - desc: "label domain", - application: application( - appID("test"), - withLabel(label.TraefikDomain, "traefik.localhost"), - ), - marathonLBCompatibility: true, - expected: "Host:test.traefik.localhost", - }, - { - desc: "HAProxy vhost available and LB compat disabled", - application: application( - appID("test"), - withLabel("HAPROXY_0_VHOST", "foo.bar"), - ), - marathonLBCompatibility: false, - expected: "Host:test.marathon.localhost", - }, - { - desc: "HAProxy vhost available and LB compat enabled", - application: application(withLabel("HAPROXY_0_VHOST", "foo.bar")), - marathonLBCompatibility: true, - expected: "Host:foo.bar", - }, - { - desc: "frontend rule available", - application: application( - withLabel(label.TraefikFrontendRule, "Host:foo.bar"), - withLabel("HAPROXY_0_VHOST", "unused"), - ), - marathonLBCompatibility: true, - expected: "Host:foo.bar", - }, - { - desc: "segment label frontend rule", - application: application(withSegmentLabel(label.TraefikFrontendRule, "Host:foo.bar", "app")), - segmentName: "app", - marathonLBCompatibility: true, - expected: "Host:foo.bar", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - p := &Provider{ - Domain: "marathon.localhost", - MarathonLBCompatibility: test.marathonLBCompatibility, - } - - actual := p.getFrontendRule(withAppData(test.application, test.segmentName)) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetBackendName(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - segmentName string - expected string - }{ - { - desc: "label missing", - application: application(appID("/group/app")), - expected: "backend-group-app", - }, - { - desc: "label existing", - application: application(withLabel(label.TraefikBackend, "bar")), - expected: "backendbar", - }, - { - desc: "segment label existing", - application: application(withSegmentLabel(label.TraefikBackend, "bar", "app")), - segmentName: "app", - expected: "backendbar", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - p := &Provider{} - - actual := p.getBackendName(withAppData(test.application, test.segmentName)) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestGetServers(t *testing.T) { - testCases := []struct { - desc string - application marathon.Application - segmentName string - expected map[string]types.Server - }{ - { - desc: "should return nil when no task", - application: application(ipAddrPerTask(80)), - expected: nil, - }, - { - desc: "should return nil when all hosts are empty", - application: application( - withTasks( - task(ipAddresses("1.1.1.1"), withTaskID("A"), taskPorts(80)), - task(ipAddresses("1.1.1.2"), withTaskID("B"), taskPorts(80)), - task(ipAddresses("1.1.1.3"), withTaskID("C"), taskPorts(80))), - ), - expected: nil, - }, - { - desc: "with 3 tasks and hosts set", - application: application( - withTasks( - task(ipAddresses("1.1.1.1"), host("2.2.2.2"), withTaskID("A"), taskPorts(80)), - task(ipAddresses("1.1.1.2"), host("2.2.2.2"), withTaskID("B"), taskPorts(81)), - task(ipAddresses("1.1.1.3"), host("2.2.2.2"), withTaskID("C"), taskPorts(82))), - ), - expected: map[string]types.Server{ - "server-A": { - URL: "http://2.2.2.2:80", - Weight: label.DefaultWeight, - }, - "server-B": { - URL: "http://2.2.2.2:81", - Weight: label.DefaultWeight, - }, - "server-C": { - URL: "http://2.2.2.2:82", - Weight: label.DefaultWeight, - }, - }, - }, - { - desc: "with 3 tasks and ipAddrPerTask set", - application: application( - ipAddrPerTask(80), - withTasks( - task(ipAddresses("1.1.1.1"), withTaskID("A"), taskPorts(80)), - task(ipAddresses("1.1.1.2"), withTaskID("B"), taskPorts(80)), - task(ipAddresses("1.1.1.3"), withTaskID("C"), taskPorts(80))), - ), - expected: map[string]types.Server{ - "server-A": { - URL: "http://1.1.1.1:80", - Weight: label.DefaultWeight, - }, - "server-B": { - URL: "http://1.1.1.2:80", - Weight: label.DefaultWeight, - }, - "server-C": { - URL: "http://1.1.1.3:80", - Weight: label.DefaultWeight, - }, - }, - }, - { - desc: "with 3 tasks and bridge network", - application: application( - bridgeNetwork(), - withTasks( - task(ipAddresses("1.1.1.1"), host("2.2.2.2"), withTaskID("A"), taskPorts(80)), - task(ipAddresses("1.1.1.2"), host("2.2.2.2"), withTaskID("B"), taskPorts(81)), - task(ipAddresses("1.1.1.3"), host("2.2.2.2"), withTaskID("C"), taskPorts(82))), - ), - expected: map[string]types.Server{ - "server-A": { - URL: "http://2.2.2.2:80", - Weight: label.DefaultWeight, - }, - "server-B": { - URL: "http://2.2.2.2:81", - Weight: label.DefaultWeight, - }, - "server-C": { - URL: "http://2.2.2.2:82", - Weight: label.DefaultWeight, - }, - }, - }, - { - desc: "with 3 tasks and cni set", - application: application( - containerNetwork(), - withTasks( - task(ipAddresses("1.1.1.1"), withTaskID("A"), taskPorts(80)), - task(ipAddresses("1.1.1.2"), withTaskID("B"), taskPorts(80)), - task(ipAddresses("1.1.1.3"), withTaskID("C"), taskPorts(80))), - ), - expected: map[string]types.Server{ - "server-A": { - URL: "http://1.1.1.1:80", - Weight: label.DefaultWeight, - }, - "server-B": { - URL: "http://1.1.1.2:80", - Weight: label.DefaultWeight, - }, - "server-C": { - URL: "http://1.1.1.3:80", - Weight: label.DefaultWeight, - }, - }, - }, - } - - p := &Provider{} - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := p.getServers(withAppData(test.application, test.segmentName)) - - assert.Equal(t, test.expected, actual) - }) - } -} diff --git a/old/provider/marathon/convert_types.go b/old/provider/marathon/convert_types.go deleted file mode 100644 index fb2895b19..000000000 --- a/old/provider/marathon/convert_types.go +++ /dev/null @@ -1,8 +0,0 @@ -package marathon - -func stringValueMap(mp *map[string]string) map[string]string { - if mp != nil { - return *mp - } - return make(map[string]string) -} diff --git a/provider/aggregator/aggregator.go b/provider/aggregator/aggregator.go index 3a7fe6d85..d140d2735 100644 --- a/provider/aggregator/aggregator.go +++ b/provider/aggregator/aggregator.go @@ -27,6 +27,10 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator { p.quietAddProvider(conf.Docker) } + if conf.Marathon != nil { + p.quietAddProvider(conf.Marathon) + } + if conf.Rest != nil { p.quietAddProvider(conf.Rest) } diff --git a/old/provider/marathon/builder_test.go b/provider/marathon/builder_test.go similarity index 86% rename from old/provider/marathon/builder_test.go rename to provider/marathon/builder_test.go index 00021cd54..a63758499 100644 --- a/old/provider/marathon/builder_test.go +++ b/provider/marathon/builder_test.go @@ -4,22 +4,11 @@ import ( "strings" "time" - "github.com/containous/traefik/old/provider/label" "github.com/gambol99/go-marathon" ) const testTaskName = "taskID" -func withAppData(app marathon.Application, segmentName string) appData { - segmentProperties := label.ExtractTraefikLabels(stringValueMap(app.Labels)) - return appData{ - Application: app, - SegmentLabels: segmentProperties[segmentName], - SegmentName: segmentName, - LinkedApps: nil, - } -} - // Functions related to building applications. func withApplications(apps ...marathon.Application) *marathon.Applications { @@ -64,17 +53,6 @@ func constraint(value string) func(*marathon.Application) { } } -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+segmentName+"."+property, value) - } -} - func portDefinition(port int) func(*marathon.Application) { return func(app *marathon.Application) { app.AddPortDefinition(marathon.PortDefinition{ diff --git a/provider/marathon/config.go b/provider/marathon/config.go new file mode 100644 index 000000000..eae1fe1b0 --- /dev/null +++ b/provider/marathon/config.go @@ -0,0 +1,276 @@ +package marathon + +import ( + "context" + "errors" + "fmt" + "math" + "net" + "strconv" + "strings" + + "github.com/containous/traefik/config" + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/label" + "github.com/gambol99/go-marathon" +) + +func (p *Provider) buildConfiguration(ctx context.Context, applications *marathon.Applications) *config.Configuration { + configurations := make(map[string]*config.Configuration) + + for _, app := range applications.Apps { + ctxApp := log.With(ctx, log.Str("applicationID", app.ID)) + logger := log.FromContext(ctxApp) + + extraConf, err := p.getConfiguration(app) + if err != nil { + logger.Errorf("Skip application: %v", err) + continue + } + + if !p.keepApplication(ctxApp, extraConf) { + continue + } + + confFromLabel, err := label.DecodeConfiguration(stringValueMap(app.Labels)) + if err != nil { + logger.Error(err) + continue + } + + err = p.buildServiceConfiguration(ctxApp, app, extraConf, confFromLabel) + if err != nil { + logger.Error(err) + continue + } + + model := struct { + Name string + Labels map[string]string + }{ + Name: app.ID, + Labels: stringValueMap(app.Labels), + } + + serviceName := getServiceName(app) + + provider.BuildRouterConfiguration(ctxApp, confFromLabel, serviceName, p.defaultRuleTpl, model) + + configurations[app.ID] = confFromLabel + } + + return provider.Merge(ctx, configurations) +} + +func getServiceName(app marathon.Application) string { + return strings.Replace(strings.TrimPrefix(app.ID, "/"), "/", "_", -1) +} + +func (p *Provider) buildServiceConfiguration(ctx context.Context, app marathon.Application, extraConf configuration, conf *config.Configuration) error { + appName := getServiceName(app) + appCtx := log.With(ctx, log.Str("ApplicationID", appName)) + + if len(conf.Services) == 0 { + conf.Services = make(map[string]*config.Service) + lb := &config.LoadBalancerService{} + lb.SetDefaults() + conf.Services[appName] = &config.Service{ + LoadBalancer: lb, + } + } + + for serviceName, service := range conf.Services { + var servers []config.Server + + defaultServer := config.Server{} + defaultServer.SetDefaults() + + if len(service.LoadBalancer.Servers) > 0 { + defaultServer = service.LoadBalancer.Servers[0] + } + + for _, task := range app.Tasks { + if p.taskFilter(ctx, *task, app) { + server, err := p.getServer(app, *task, extraConf, defaultServer) + if err != nil { + log.FromContext(appCtx).Errorf("Skip task: %v", err) + continue + } + servers = append(servers, server) + } + } + if len(servers) == 0 { + return fmt.Errorf("no server for the service %s", serviceName) + } + service.LoadBalancer.Servers = servers + } + + return nil +} + +func (p *Provider) keepApplication(ctx context.Context, extraConf configuration) bool { + logger := log.FromContext(ctx) + + // Filter disabled application. + if !extraConf.Enable { + logger.Debug("Filtering disabled Marathon application") + return false + } + + // Filter by constraints. + if ok, failingConstraint := p.MatchConstraints(extraConf.Tags); !ok { + if failingConstraint != nil { + logger.Debugf("Filtering Marathon application, pruned by %q constraint", failingConstraint.String()) + } + return false + } + + return true +} + +func (p *Provider) taskFilter(ctx context.Context, task marathon.Task, application marathon.Application) bool { + if task.State != string(taskStateRunning) { + return false + } + + if ready := p.readyChecker.Do(task, application); !ready { + log.FromContext(ctx).Infof("Filtering unready task %s from application %s", task.ID, application.ID) + return false + } + + return true +} + +func (p *Provider) getServer(app marathon.Application, task marathon.Task, extraConf configuration, defaultServer config.Server) (config.Server, error) { + host, err := p.getServerHost(task, app, extraConf) + if len(host) == 0 { + return config.Server{}, err + } + + port, err := getPort(task, app, defaultServer.Port) + if err != nil { + return config.Server{}, err + } + + server := config.Server{ + URL: fmt.Sprintf("%s://%s", defaultServer.Scheme, net.JoinHostPort(host, port)), + Weight: 1, + } + + return server, nil +} + +func (p *Provider) getServerHost(task marathon.Task, app marathon.Application, extraConf configuration) (string, error) { + networks := app.Networks + var hostFlag bool + + if networks == nil { + hostFlag = app.IPAddressPerTask == nil + } else { + hostFlag = (*networks)[0].Mode != marathon.ContainerNetworkMode + } + + if hostFlag || p.ForceTaskHostname { + if len(task.Host) == 0 { + return "", fmt.Errorf("host is undefined for task %q app %q", task.ID, app.ID) + } + return task.Host, nil + } + + numTaskIPAddresses := len(task.IPAddresses) + switch numTaskIPAddresses { + case 0: + return "", fmt.Errorf("missing IP address for Marathon application %s on task %s", app.ID, task.ID) + case 1: + return task.IPAddresses[0].IPAddress, nil + default: + if extraConf.Marathon.IPAddressIdx == math.MinInt32 { + 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) + } + if extraConf.Marathon.IPAddressIdx < 0 || extraConf.Marathon.IPAddressIdx > numTaskIPAddresses { + 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 task.IPAddresses[extraConf.Marathon.IPAddressIdx].IPAddress, nil + } +} + +func getPort(task marathon.Task, app marathon.Application, serverPort string) (string, error) { + port, err := processPorts(app, task, serverPort) + if err != nil { + return "", fmt.Errorf("unable to process ports for %s %s: %v", app.ID, task.ID, err) + } + + return strconv.Itoa(port), nil +} + +// 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(app marathon.Application, task marathon.Task, serverPort string) (int, error) { + if len(serverPort) > 0 && !strings.HasPrefix(serverPort, "index:") { + port, err := strconv.Atoi(serverPort) + if err != nil { + return 0, err + } + + if port <= 0 { + return 0, fmt.Errorf("explicitly specified port %d must be greater than zero", port) + } else if port > 0 { + return port, nil + } + } + + ports := retrieveAvailablePorts(app, task) + if len(ports) == 0 { + return 0, errors.New("no port found") + } + + portIndex := 0 + if strings.HasPrefix(serverPort, "index:") { + split := strings.SplitN(serverPort, ":", 2) + index, err := strconv.Atoi(split[1]) + if err != nil { + return 0, err + } + + if index < 0 || index > len(ports)-1 { + return 0, fmt.Errorf("index %d must be within range (0, %d)", index, len(ports)-1) + } + portIndex = index + } + return ports[portIndex], nil +} + +func retrieveAvailablePorts(app marathon.Application, task marathon.Task) []int { + // Using default port configuration + if len(task.Ports) > 0 { + return task.Ports + } + + // Using port definition if available + if app.PortDefinitions != nil && len(*app.PortDefinitions) > 0 { + var ports []int + for _, def := range *app.PortDefinitions { + if def.Port != nil { + ports = append(ports, *def.Port) + } + } + return ports + } + + // If using IP-per-task using this port definition + if app.IPAddressPerTask != nil && app.IPAddressPerTask.Discovery != nil && len(*(app.IPAddressPerTask.Discovery.Ports)) > 0 { + var ports []int + for _, def := range *(app.IPAddressPerTask.Discovery.Ports) { + ports = append(ports, def.Number) + } + return ports + } + + return []int{} +} diff --git a/provider/marathon/config_test.go b/provider/marathon/config_test.go new file mode 100644 index 000000000..74310f943 --- /dev/null +++ b/provider/marathon/config_test.go @@ -0,0 +1,1587 @@ +package marathon + +import ( + "context" + "math" + "testing" + + "github.com/containous/traefik/config" + "github.com/containous/traefik/types" + "github.com/gambol99/go-marathon" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetConfigurationAPIErrors(t *testing.T) { + fakeClient := newFakeClient(true, marathon.Applications{}) + + p := &Provider{ + marathonClient: fakeClient, + } + + actualConfig := p.getConfigurations(context.Background()) + fakeClient.AssertExpectations(t) + + if actualConfig != nil { + t.Errorf("configuration should have been nil, got %v", actualConfig) + } +} + +func TestBuildConfiguration(t *testing.T) { + testCases := []struct { + desc string + applications *marathon.Applications + constraints types.Constraints + filterMarathonConstraints bool + defaultRule string + expected *config.Configuration + }{ + { + desc: "simple application", + applications: withApplications( + application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80))), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "app", + Rule: "Host:app.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "app": {LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }}, + }, + }, + }, + { + desc: "filtered task", + applications: withApplications( + application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80), taskState(taskStateStaging))), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + { + desc: "multiple ports", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "app", + Rule: "Host:app.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "app": {LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }}, + }, + }, + }, + { + desc: "with basic auth", + applications: withApplications( + application( + appID("/app"), + appPorts(80), + withLabel("traefik.middlewares.Middleware1.basicauth.users", "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + withLabel("traefik.routers.app.middlewares", "Middleware1"), + withTasks(localhostTask(taskPorts(80))), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "app", + Rule: "Host:app.marathon.localhost", + Middlewares: []string{"Middleware1"}, + }, + }, + Middlewares: map[string]*config.Middleware{ + "Middleware1": { + BasicAuth: &config.BasicAuth{ + Users: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + }, + }, + }, + Services: map[string]*config.Service{ + "app": {LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }}, + }, + }, + }, + { + desc: "2 applications in the same service", + applications: withApplications( + application( + appID("/foo-v000"), + withTasks(localhostTask(taskPorts(8080))), + + withLabel("traefik.services.Service1.LoadBalancer.server.port", "index:0"), + withLabel("traefik.routers.Router1.rule", "Host:app.marathon.localhost"), + ), + application( + appID("/foo-v001"), + withTasks(localhostTask(taskPorts(8081))), + + withLabel("traefik.services.Service1.LoadBalancer.server.port", "index:0"), + withLabel("traefik.routers.Router1.rule", "Host:app.marathon.localhost"), + ), + ), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "Router1": { + Service: "Service1", + Rule: "Host:app.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "Service1": {LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:8080", + Weight: 1, + }, + { + URL: "http://localhost:8081", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }}, + }, + }, + }, + { + desc: "2 applications with 2 tasks in the same service", + applications: withApplications( + application( + appID("/foo-v000"), + withTasks(localhostTask(taskPorts(8080))), + withTasks(localhostTask(taskPorts(8081))), + + withLabel("traefik.services.Service1.LoadBalancer.server.port", "index:0"), + withLabel("traefik.routers.Router1.rule", "Host:app.marathon.localhost"), + ), + application( + appID("/foo-v001"), + withTasks(localhostTask(taskPorts(8082))), + withTasks(localhostTask(taskPorts(8083))), + + withLabel("traefik.services.Service1.LoadBalancer.server.port", "index:0"), + withLabel("traefik.routers.Router1.rule", "Host:app.marathon.localhost"), + ), + ), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "Router1": { + Service: "Service1", + Rule: "Host:app.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "Service1": {LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:8080", + Weight: 1, + }, + { + URL: "http://localhost:8081", + Weight: 1, + }, + { + URL: "http://localhost:8082", + Weight: 1, + }, + { + URL: "http://localhost:8083", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }}, + }, + }, + }, + { + desc: "2 applications", + applications: withApplications( + application( + appID("/foo"), + withTasks(localhostTask(taskPorts(8080))), + ), + application( + appID("/bar"), + withTasks(localhostTask(taskPorts(8081))), + ), + ), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "foo": { + Service: "foo", + Rule: "Host:foo.marathon.localhost", + }, + "bar": { + Service: "bar", + Rule: "Host:bar.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "foo": {LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:8080", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }}, + "bar": {LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:8081", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }}, + }, + }, + }, + { + desc: "two tasks no labels", + applications: withApplications( + application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80)), localhostTask(taskPorts(81))), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "app", + Rule: "Host:app.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "app": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + { + URL: "http://localhost:81", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "simple application with label on service", + applications: withApplications( + application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80))), + withLabel("traefik.services.Service1.loadbalancer.method", "drr"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "Service1", + Rule: "Host:app.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "Service1": {LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "drr", + PassHostHeader: true, + }}, + }, + }, + }, + { + desc: "one app with labels", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.services.Service1.loadbalancer.method", "wrr"), + withLabel("traefik.routers.Router1.rule", "Host:foo.com"), + withLabel("traefik.routers.Router1.service", "Service1"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "Router1": { + Service: "Service1", + Rule: "Host:foo.com", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "Service1": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "one app with rule label", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.routers.Router1.rule", "Host:foo.com"), + )), + expected: &config.Configuration{ + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "app": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + Routers: map[string]*config.Router{ + "Router1": { + Service: "app", + Rule: "Host:foo.com", + }, + }, + }, + }, + { + desc: "one app with rule label and one service", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.routers.Router1.rule", "Host:foo.com"), + withLabel("traefik.services.Service1.loadbalancer.method", "wrr"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "Router1": { + Service: "Service1", + Rule: "Host:foo.com", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "Service1": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "one app with rule label and two services", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.routers.Router1.rule", "Host:foo.com"), + withLabel("traefik.services.Service1.loadbalancer.method", "wrr"), + withLabel("traefik.services.Service2.loadbalancer.method", "wrr"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "Service1": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + "Service2": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "two apps with same service name and different LB methods", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.services.Service1.loadbalancer.method", "wrr"), + ), + application( + appID("/app2"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.services.Service1.loadbalancer.method", "drr"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "Service1", + Rule: "Host:app.marathon.localhost", + }, + "app2": { + Service: "Service1", + Rule: "Host:app2.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + { + desc: "two apps with two identical middleware", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.middlewares.Middleware1.maxconn.amount", "42"), + ), + application( + appID("/app2"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.middlewares.Middleware1.maxconn.amount", "42"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "app", + Rule: "Host:app.marathon.localhost", + }, + "app2": { + Service: "app2", + Rule: "Host:app2.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{ + "Middleware1": { + MaxConn: &config.MaxConn{ + Amount: 42, + ExtractorFunc: "request.host", + }, + }, + }, + Services: map[string]*config.Service{ + "app": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + "app2": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "two apps with two different middlewares", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.middlewares.Middleware1.maxconn.amount", "42"), + ), + application( + appID("/app2"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.middlewares.Middleware1.maxconn.amount", "41"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "app", + Rule: "Host:app.marathon.localhost", + }, + "app2": { + Service: "app2", + Rule: "Host:app2.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "app": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + "app2": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "two apps with two different routers with same name", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.routers.Router1.rule", "Host:foo.com"), + ), + application( + appID("/app2"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.routers.Router1.rule", "Host:bar.com"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "app": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + "app2": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "two apps with two identical routers with same name", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.routers.Router1.rule", "Host:foo.com"), + withLabel("traefik.services.Service1.LoadBalancer.method", "wrr"), + ), + application( + appID("/app2"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.routers.Router1.rule", "Host:foo.com"), + withLabel("traefik.services.Service1.LoadBalancer.method", "wrr"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "Router1": { + Service: "Service1", + Rule: "Host:foo.com", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "Service1": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "two apps with two identical routers with same name", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.routers.Router1.rule", "Host:foo.com"), + ), + application( + appID("/app2"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.routers.Router1.rule", "Host:foo.com"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "app": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + "app2": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "one app with wrong label", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.wrong.label", "tchouk"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "app", + Rule: "Host:app.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "app": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "one app with label port", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.services.Service1.LoadBalancer.server.scheme", "h2c"), + withLabel("traefik.services.Service1.LoadBalancer.server.port", "90"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "Service1", + Rule: "Host:app.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "Service1": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "h2c://localhost:90", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "one app with label port on two services", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.services.Service1.LoadBalancer.server.port", ""), + withLabel("traefik.services.Service2.LoadBalancer.server.port", "8080"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "Service1": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + "Service2": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:8080", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "one app without port", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask()), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + { + desc: "one app without port with middleware", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask()), + withLabel("traefik.middlewares.Middleware1.basicauth.users", "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + { + desc: "one app with traefik.enable=false", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask()), + withLabel("traefik.enable", "false"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + { + desc: "one app with traefik.enable=false", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask()), + withLabel("traefik.enable", "false"), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + { + desc: "one app with non matching constraint", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.tags", "foo"), + )), + constraints: types.Constraints{ + &types.Constraint{ + Key: "tag", + MustMatch: true, + Regex: "bar", + }, + }, + expected: &config.Configuration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + { + desc: "one app with non matching marathon constraint", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + constraint("rack_id:CLUSTER:rack-1"), + )), + filterMarathonConstraints: true, + constraints: types.Constraints{ + &types.Constraint{ + Key: "tag", + MustMatch: true, + Regex: "rack_id:CLUSTER:rack-2", + }, + }, + expected: &config.Configuration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + { + desc: "one app with matching marathon constraint", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + constraint("rack_id:CLUSTER:rack-1"), + )), + filterMarathonConstraints: true, + constraints: types.Constraints{ + &types.Constraint{ + Key: "tag", + MustMatch: true, + Regex: "rack_id:CLUSTER:rack-1", + }, + }, + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "app", + Rule: "Host:app.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "app": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "one app with matching constraint", + applications: withApplications( + application( + appID("/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + withLabel("traefik.tags", "bar"), + )), + + constraints: types.Constraints{ + &types.Constraint{ + Key: "tag", + MustMatch: true, + Regex: "bar", + }, + }, + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "app": { + Service: "app", + Rule: "Host:app.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "app": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + { + desc: "one app with group as subdomain rule", + defaultRule: `Host:{{ .Name | trimPrefix "/" | splitList "/" | strsToItfs | reverse | join "." }}.marathon.localhost`, + applications: withApplications( + application( + appID("/a/b/app"), + appPorts(80, 81), + withTasks(localhostTask(taskPorts(80, 81))), + )), + expected: &config.Configuration{ + Routers: map[string]*config.Router{ + "a_b_app": { + Service: "a_b_app", + Rule: "Host:app.b.a.marathon.localhost", + }, + }, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{ + "a_b_app": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://localhost:80", + Weight: 1, + }, + }, + Method: "wrr", + PassHostHeader: true, + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + defaultRule := DefaultTemplateRule + ".marathon.localhost" + if len(test.defaultRule) > 0 { + defaultRule = test.defaultRule + } + + p := &Provider{ + DefaultRule: defaultRule, + ExposedByDefault: true, + FilterMarathonConstraints: test.filterMarathonConstraints, + } + p.Constraints = test.constraints + + err := p.Init() + require.NoError(t, err) + + actualConfig := p.buildConfiguration(context.Background(), test.applications) + + assert.NotNil(t, actualConfig) + assert.Equal(t, test.expected, actualConfig) + }) + } +} + +func TestApplicationFilterEnabled(t *testing.T) { + testCases := []struct { + desc string + exposedByDefault bool + enabledLabel string + expected bool + }{ + { + desc: "exposed and tolerated by valid label value", + exposedByDefault: true, + enabledLabel: "true", + expected: true, + }, + { + desc: "exposed but overridden by label", + exposedByDefault: true, + enabledLabel: "false", + expected: false, + }, + { + desc: "non-exposed but overridden by label", + exposedByDefault: false, + enabledLabel: "true", + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + provider := &Provider{ExposedByDefault: test.exposedByDefault} + + app := application(withLabel("traefik.enable", test.enabledLabel)) + + extraConf, err := provider.getConfiguration(app) + require.NoError(t, err) + + if provider.keepApplication(context.Background(), extraConf) != test.expected { + t.Errorf("got unexpected filtering = %t", !test.expected) + } + }) + } +} + +func TestGetServer(t *testing.T) { + type expected struct { + server config.Server + error string + } + + testCases := []struct { + desc string + provider Provider + app marathon.Application + extraConf configuration + defaultServer config.Server + expected expected + }{ + { + desc: "undefined host", + provider: Provider{}, + app: application(), + extraConf: configuration{}, + defaultServer: config.Server{}, + expected: expected{ + error: `host is undefined for task "taskID" app ""`, + }, + }, + { + desc: "with task port", + provider: Provider{}, + app: application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80))), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Weight: 1, + }, + expected: expected{ + server: config.Server{ + URL: "http://localhost:80", + Weight: 1, + }, + }, + }, + { + desc: "without task port", + provider: Provider{}, + app: application( + appID("/app"), + appPorts(80), + withTasks(localhostTask()), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Weight: 1, + }, + expected: expected{ + error: "unable to process ports for /app taskID: no port found", + }, + }, + { + desc: "with default server port", + provider: Provider{}, + app: application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80))), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Port: "88", + Weight: 1, + }, + expected: expected{ + server: config.Server{ + URL: "http://localhost:88", + Weight: 1, + }, + }, + }, + { + desc: "with invalid default server port", + provider: Provider{}, + app: application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80))), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Port: "aaaa", + Weight: 1, + }, + expected: expected{ + error: `unable to process ports for /app taskID: strconv.Atoi: parsing "aaaa": invalid syntax`, + }, + }, + { + desc: "with negative default server port", + provider: Provider{}, + app: application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80))), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Port: "-6", + Weight: 1, + }, + expected: expected{ + error: `unable to process ports for /app taskID: explicitly specified port -6 must be greater than zero`, + }, + }, + { + desc: "with port index", + provider: Provider{}, + app: application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80, 81))), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Port: "index:1", + Weight: 1, + }, + expected: expected{ + server: config.Server{ + URL: "http://localhost:81", + Weight: 1, + }, + }, + }, + { + desc: "with out of range port index", + provider: Provider{}, + app: application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80, 81))), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Port: "index:2", + Weight: 1, + }, + expected: expected{ + error: "unable to process ports for /app taskID: index 2 must be within range (0, 1)", + }, + }, + { + desc: "with invalid port index", + provider: Provider{}, + app: application( + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80, 81))), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Port: "index:aaa", + Weight: 1, + }, + expected: expected{ + error: `unable to process ports for /app taskID: strconv.Atoi: parsing "aaa": invalid syntax`, + }, + }, + { + desc: "with application port and no task port", + provider: Provider{}, + app: application( + appID("/app"), + appPorts(80), + portDefinition(80), + withTasks(localhostTask()), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Weight: 1, + }, + expected: expected{ + server: config.Server{ + URL: "http://localhost:80", + Weight: 1, + }, + }, + }, + { + desc: "with IP per task", + provider: Provider{}, + app: application( + appID("/app"), + appPorts(80), + ipAddrPerTask(88), + withTasks(localhostTask()), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Weight: 1, + }, + expected: expected{ + server: config.Server{ + URL: "http://127.0.0.1:88", + Weight: 1, + }, + }, + }, + { + desc: "with container network", + provider: Provider{}, + app: application( + containerNetwork(), + appID("/app"), + appPorts(80), + withTasks(localhostTask(taskPorts(80, 81))), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Weight: 1, + }, + expected: expected{ + server: config.Server{ + URL: "http://127.0.0.1:80", + Weight: 1, + }, + }, + }, + { + desc: "with bridge network", + provider: Provider{}, + app: application( + bridgeNetwork(), + appID("/app"), + appPorts(83), + withTasks(localhostTask(taskPorts(80, 81))), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Weight: 1, + }, + expected: expected{ + server: config.Server{ + URL: "http://localhost:80", + Weight: 1, + }, + }, + }, + { + desc: "with several IP addresses on task", + provider: Provider{}, + app: application( + ipAddrPerTask(88), + appID("/app"), + appPorts(83), + withTasks( + task( + withTaskID("myTask"), + host("localhost"), + ipAddresses("127.0.0.1", "127.0.0.2"), + taskState(taskStateRunning), + )), + ), + extraConf: configuration{ + Marathon: specificConfiguration{ + IPAddressIdx: 0, + }, + }, + defaultServer: config.Server{ + Scheme: "http", + Weight: 1, + }, + expected: expected{ + server: config.Server{ + URL: "http://127.0.0.1:88", + Weight: 1, + }, + }, + }, + { + desc: "with several IP addresses on task, undefined [MinInt32] IPAddressIdx", + provider: Provider{}, + app: application( + ipAddrPerTask(88), + appID("/app"), + appPorts(83), + withTasks( + task( + host("localhost"), + ipAddresses("127.0.0.1", "127.0.0.2"), + taskState(taskStateRunning), + )), + ), + extraConf: configuration{ + Marathon: specificConfiguration{ + IPAddressIdx: math.MinInt32, + }, + }, + defaultServer: config.Server{ + Scheme: "http", + Weight: 1, + }, + expected: expected{ + error: "found 2 task IP addresses but missing IP address index for Marathon application /app on task taskID", + }, + }, + { + desc: "with several IP addresses on task, IPAddressIdx out of range", + provider: Provider{}, + app: application( + ipAddrPerTask(88), + appID("/app"), + appPorts(83), + withTasks( + task( + host("localhost"), + ipAddresses("127.0.0.1", "127.0.0.2"), + taskState(taskStateRunning), + )), + ), + extraConf: configuration{ + Marathon: specificConfiguration{ + IPAddressIdx: 3, + }, + }, + defaultServer: config.Server{ + Scheme: "http", + Weight: 1, + }, + expected: expected{ + error: "cannot use IP address index to select from 2 task IP addresses for Marathon application /app on task taskID", + }, + }, + { + desc: "with task without IP address", + provider: Provider{}, + app: application( + ipAddrPerTask(88), + appID("/app"), + appPorts(83), + withTasks( + task( + host("localhost"), + taskState(taskStateRunning), + )), + ), + extraConf: configuration{}, + defaultServer: config.Server{ + Scheme: "http", + Weight: 1, + }, + expected: expected{ + error: "missing IP address for Marathon application /app on task taskID", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + task := task() + if len(test.app.Tasks) > 0 { + task = *test.app.Tasks[0] + } + + server, err := test.provider.getServer(test.app, task, test.extraConf, test.defaultServer) + if len(test.expected.error) > 0 { + require.EqualError(t, err, test.expected.error) + } else { + require.NoError(t, err) + + assert.Equal(t, test.expected.server, server) + } + }) + } +} diff --git a/old/provider/marathon/fake_client_test.go b/provider/marathon/fake_client_test.go similarity index 90% rename from old/provider/marathon/fake_client_test.go rename to provider/marathon/fake_client_test.go index 70e51d830..42f5e0235 100644 --- a/old/provider/marathon/fake_client_test.go +++ b/provider/marathon/fake_client_test.go @@ -3,7 +3,7 @@ package marathon import ( "errors" - "github.com/containous/traefik/old/provider/marathon/mocks" + "github.com/containous/traefik/provider/marathon/mocks" "github.com/gambol99/go-marathon" "github.com/stretchr/testify/mock" ) diff --git a/provider/marathon/label.go b/provider/marathon/label.go new file mode 100644 index 000000000..4bddb4020 --- /dev/null +++ b/provider/marathon/label.go @@ -0,0 +1,51 @@ +package marathon + +import ( + "math" + "strings" + + "github.com/containous/traefik/provider/label" + "github.com/gambol99/go-marathon" +) + +type configuration struct { + Enable bool + Tags []string + Marathon specificConfiguration +} + +type specificConfiguration struct { + IPAddressIdx int +} + +func (p *Provider) getConfiguration(app marathon.Application) (configuration, error) { + labels := stringValueMap(app.Labels) + + conf := configuration{ + Enable: p.ExposedByDefault, + Tags: nil, + Marathon: specificConfiguration{ + IPAddressIdx: math.MinInt32, + }, + } + + err := label.Decode(labels, &conf, "traefik.marathon.", "traefik.enable", "traefik.tags") + if err != nil { + return configuration{}, err + } + + if p.FilterMarathonConstraints && app.Constraints != nil { + for _, constraintParts := range *app.Constraints { + conf.Tags = append(conf.Tags, strings.Join(constraintParts, ":")) + } + } + + return conf, nil +} + +func stringValueMap(mp *map[string]string) map[string]string { + if mp != nil { + return *mp + } + return make(map[string]string) +} diff --git a/provider/marathon/label_test.go b/provider/marathon/label_test.go new file mode 100644 index 000000000..523e32ca0 --- /dev/null +++ b/provider/marathon/label_test.go @@ -0,0 +1,178 @@ +package marathon + +import ( + "math" + "testing" + + "github.com/containous/traefik/provider" + "github.com/gambol99/go-marathon" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetConfiguration(t *testing.T) { + testCases := []struct { + desc string + app marathon.Application + p Provider + expected configuration + }{ + { + desc: "Empty labels", + app: marathon.Application{ + Constraints: &[][]string{}, + Labels: &map[string]string{}, + }, + p: Provider{ + BaseProvider: provider.BaseProvider{}, + ExposedByDefault: false, + FilterMarathonConstraints: false, + }, + expected: configuration{ + Enable: false, + Tags: nil, + Marathon: specificConfiguration{ + IPAddressIdx: math.MinInt32, + }, + }, + }, + { + desc: "label enable", + app: marathon.Application{ + Constraints: &[][]string{}, + Labels: &map[string]string{ + "traefik.enable": "true", + }, + }, + p: Provider{ + BaseProvider: provider.BaseProvider{}, + ExposedByDefault: false, + FilterMarathonConstraints: false, + }, + expected: configuration{ + Enable: true, + Tags: nil, + Marathon: specificConfiguration{ + IPAddressIdx: math.MinInt32, + }, + }, + }, + { + desc: "Use ip address index", + app: marathon.Application{ + Constraints: &[][]string{}, + Labels: &map[string]string{ + "traefik.marathon.IPAddressIdx": "4", + }, + }, + p: Provider{ + BaseProvider: provider.BaseProvider{}, + ExposedByDefault: false, + FilterMarathonConstraints: false, + }, + expected: configuration{ + Enable: false, + Tags: nil, + Marathon: specificConfiguration{ + IPAddressIdx: 4, + }, + }, + }, + { + desc: "Use marathon constraints", + app: marathon.Application{ + Constraints: &[][]string{ + {"key", "value"}, + }, + Labels: &map[string]string{}, + }, + p: Provider{ + BaseProvider: provider.BaseProvider{}, + ExposedByDefault: false, + FilterMarathonConstraints: true, + }, + expected: configuration{ + Enable: false, + Tags: []string{ + "key:value", + }, + Marathon: specificConfiguration{ + IPAddressIdx: math.MinInt32, + }, + }, + }, + { + desc: "ExposedByDefault and no enable label", + app: marathon.Application{ + Constraints: &[][]string{}, + Labels: &map[string]string{}, + }, + p: Provider{ + BaseProvider: provider.BaseProvider{}, + ExposedByDefault: true, + FilterMarathonConstraints: false, + }, + expected: configuration{ + Enable: true, + Tags: nil, + Marathon: specificConfiguration{ + IPAddressIdx: math.MinInt32, + }, + }, + }, + { + desc: "ExposedByDefault and enable label false", + app: marathon.Application{ + Constraints: &[][]string{}, + Labels: &map[string]string{ + "traefik.enable": "false", + }, + }, + p: Provider{ + BaseProvider: provider.BaseProvider{}, + ExposedByDefault: true, + FilterMarathonConstraints: false, + }, + expected: configuration{ + Enable: false, + Tags: nil, + Marathon: specificConfiguration{ + IPAddressIdx: math.MinInt32, + }, + }, + }, + { + desc: "Tags in label", + app: marathon.Application{ + Constraints: &[][]string{}, + Labels: &map[string]string{ + "traefik.tags": "mytags", + }, + }, + p: Provider{ + BaseProvider: provider.BaseProvider{}, + ExposedByDefault: true, + FilterMarathonConstraints: false, + }, + expected: configuration{ + Enable: true, + Tags: []string{"mytags"}, + Marathon: specificConfiguration{ + IPAddressIdx: math.MinInt32, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + extraConf, err := test.p.getConfiguration(test.app) + require.NoError(t, err) + + assert.Equal(t, test.expected, extraConf) + }) + } +} diff --git a/old/provider/marathon/marathon.go b/provider/marathon/marathon.go similarity index 66% rename from old/provider/marathon/marathon.go rename to provider/marathon/marathon.go index 8f74e00f9..f1d797dba 100644 --- a/old/provider/marathon/marathon.go +++ b/provider/marathon/marathon.go @@ -1,23 +1,29 @@ package marathon import ( + "context" + "fmt" "net" "net/http" "net/url" + "text/template" "time" "github.com/cenk/backoff" "github.com/containous/flaeg/parse" + "github.com/containous/traefik/config" "github.com/containous/traefik/job" - "github.com/containous/traefik/old/log" - "github.com/containous/traefik/old/provider" + "github.com/containous/traefik/log" "github.com/containous/traefik/old/types" + "github.com/containous/traefik/provider" "github.com/containous/traefik/safe" "github.com/gambol99/go-marathon" "github.com/sirupsen/logrus" ) const ( + // DefaultTemplateRule The default template for the default rule. + DefaultTemplateRule = "Host:{{ normalize .Name }}" traceMaxScanTokenSize = 1024 * 1024 marathonEventIDs = marathon.EventIDApplications | marathon.EventIDAddHealthCheck | @@ -36,23 +42,15 @@ const ( taskStateStaging TaskState = "TASK_STAGING" ) -const ( - labelIPAddressIdx = "traefik.ipAddressIdx" - labelLbCompatibilityGroup = "HAPROXY_GROUP" - labelLbCompatibility = "HAPROXY_0_VHOST" -) - var _ provider.Provider = (*Provider)(nil) // Provider holds configuration of the provider. type Provider struct { provider.BaseProvider Endpoint string `description:"Marathon server endpoint. You can also specify multiple endpoint for Marathon" export:"true"` - Domain string `description:"Default domain used" export:"true"` + DefaultRule string `description:"Default rule"` ExposedByDefault bool `description:"Expose Marathon apps by default" export:"true"` - GroupsAsSubDomains bool `description:"Convert Marathon groups to subdomains" export:"true"` DCOSToken string `description:"DCOSToken for DCOS environment, This will override the Authorization header" export:"true"` - MarathonLBCompatibility bool `description:"Add compatibility with marathon-lb labels" export:"true"` FilterMarathonConstraints bool `description:"Enable use of Marathon constraints in constraint filtering" export:"true"` TLS *types.ClientTLS `description:"Enable TLS support" export:"true"` DialerTimeout parse.Duration `description:"Set a dialer timeout for Marathon" export:"true"` @@ -64,6 +62,7 @@ type Provider struct { RespectReadinessChecks bool `description:"Filter out tasks with non-successful readiness checks during deployments" export:"true"` readyChecker *readinessChecker marathonClient marathon.Marathon + defaultRuleTpl *template.Template } // Basic holds basic authentication specific configurations @@ -73,39 +72,59 @@ type Basic struct { } // Init the provider -func (p *Provider) Init(constraints types.Constraints) error { - return p.BaseProvider.Init(constraints) +func (p *Provider) Init() error { + fm := template.FuncMap{ + "strsToItfs": func(values []string) []interface{} { + var r []interface{} + for _, v := range values { + r = append(r, v) + } + return r + }, + } + + defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, fm) + if err != nil { + return fmt.Errorf("error while parsing default rule: %v", err) + } + + p.defaultRuleTpl = defaultRuleTpl + return p.BaseProvider.Init() } // Provide allows the marathon provider to provide configurations to traefik // using the given configuration channel. -func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (p *Provider) Provide(configurationChan chan<- config.Message, pool *safe.Pool) error { + ctx := log.With(context.Background(), log.Str(log.ProviderName, "marathon")) + logger := log.FromContext(ctx) + operation := func() error { - config := marathon.NewDefaultConfig() - config.URL = p.Endpoint - config.EventsTransport = marathon.EventsTransportSSE + + confg := marathon.NewDefaultConfig() + confg.URL = p.Endpoint + confg.EventsTransport = marathon.EventsTransportSSE if p.Trace { - config.LogOutput = log.CustomWriterLevel(logrus.DebugLevel, traceMaxScanTokenSize) + confg.LogOutput = log.CustomWriterLevel(logrus.DebugLevel, traceMaxScanTokenSize) } if p.Basic != nil { - config.HTTPBasicAuthUser = p.Basic.HTTPBasicAuthUser - config.HTTPBasicPassword = p.Basic.HTTPBasicPassword + confg.HTTPBasicAuthUser = p.Basic.HTTPBasicAuthUser + confg.HTTPBasicPassword = p.Basic.HTTPBasicPassword } var rc *readinessChecker if p.RespectReadinessChecks { - log.Debug("Enabling Marathon readiness checker") + logger.Debug("Enabling Marathon readiness checker") rc = defaultReadinessChecker(p.Trace) } p.readyChecker = rc if len(p.DCOSToken) > 0 { - config.DCOSToken = p.DCOSToken + confg.DCOSToken = p.DCOSToken } TLSConfig, err := p.TLS.CreateTLSConfig() if err != nil { return err } - config.HTTPClient = &http.Client{ + confg.HTTPClient = &http.Client{ Transport: &http.Transport{ DialContext: (&net.Dialer{ KeepAlive: time.Duration(p.KeepAlive), @@ -116,9 +135,9 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s TLSClientConfig: TLSConfig, }, } - client, err := marathon.NewClient(config) + client, err := marathon.NewClient(confg) if err != nil { - log.Errorf("Failed to create a client for marathon, error: %s", err) + logger.Errorf("Failed to create a client for marathon, error: %s", err) return err } p.marathonClient = client @@ -126,7 +145,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s if p.Watch { update, err := client.AddEventsListener(marathonEventIDs) if err != nil { - log.Errorf("Failed to register for events, %s", err) + logger.Errorf("Failed to register for events, %s", err) return err } pool.Go(func(stop chan bool) { @@ -136,13 +155,13 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s case <-stop: return case event := <-update: - log.Debugf("Received provider event %s", event) + logger.Debugf("Received provider event %s", event) - configuration := p.getConfiguration() - if configuration != nil { - configurationChan <- types.ConfigMessage{ + conf := p.getConfigurations(ctx) + if conf != nil { + configurationChan <- config.Message{ ProviderName: "marathon", - Configuration: configuration, + Configuration: conf, } } } @@ -150,8 +169,8 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s }) } - configuration := p.getConfiguration() - configurationChan <- types.ConfigMessage{ + configuration := p.getConfigurations(ctx) + configurationChan <- config.Message{ ProviderName: "marathon", Configuration: configuration, } @@ -159,23 +178,23 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s } notify := func(err error, time time.Duration) { - log.Errorf("Provider connection error %+v, retrying in %s", err, time) + logger.Errorf("Provider connection error %+v, retrying in %s", err, time) } err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify) if err != nil { - log.Errorf("Cannot connect to Provider server %+v", err) + logger.Errorf("Cannot connect to Provider server: %+v", err) } return nil } -func (p *Provider) getConfiguration() *types.Configuration { +func (p *Provider) getConfigurations(ctx context.Context) *config.Configuration { applications, err := p.getApplications() if err != nil { - log.Errorf("Failed to retrieve Marathon applications: %v", err) + log.FromContext(ctx).Errorf("Failed to retrieve Marathon applications: %v", err) return nil } - return p.buildConfiguration(applications) + return p.buildConfiguration(ctx, applications) } func (p *Provider) getApplications() (*marathon.Applications, error) { diff --git a/old/provider/marathon/mocks/Marathon.go b/provider/marathon/mocks/Marathon.go similarity index 100% rename from old/provider/marathon/mocks/Marathon.go rename to provider/marathon/mocks/Marathon.go diff --git a/old/provider/marathon/readiness.go b/provider/marathon/readiness.go similarity index 100% rename from old/provider/marathon/readiness.go rename to provider/marathon/readiness.go diff --git a/old/provider/marathon/readiness_test.go b/provider/marathon/readiness_test.go similarity index 100% rename from old/provider/marathon/readiness_test.go rename to provider/marathon/readiness_test.go