Adds Marathon support.

Co-authored-by: Julien Salleyron <julien@containo.us>
This commit is contained in:
Ludovic Fernandez 2019-01-29 17:54:05 +01:00 committed by Traefiker Bot
parent a433e469cc
commit 246b245959
22 changed files with 2223 additions and 2203 deletions

View file

@ -15,13 +15,13 @@ import (
"github.com/containous/traefik/old/provider/etcd" "github.com/containous/traefik/old/provider/etcd"
"github.com/containous/traefik/old/provider/eureka" "github.com/containous/traefik/old/provider/eureka"
"github.com/containous/traefik/old/provider/kubernetes" "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/mesos"
"github.com/containous/traefik/old/provider/rancher" "github.com/containous/traefik/old/provider/rancher"
"github.com/containous/traefik/old/provider/zk" "github.com/containous/traefik/old/provider/zk"
"github.com/containous/traefik/ping" "github.com/containous/traefik/ping"
"github.com/containous/traefik/provider/docker" "github.com/containous/traefik/provider/docker"
"github.com/containous/traefik/provider/file" "github.com/containous/traefik/provider/file"
"github.com/containous/traefik/provider/marathon"
"github.com/containous/traefik/provider/rest" "github.com/containous/traefik/provider/rest"
"github.com/containous/traefik/tracing/datadog" "github.com/containous/traefik/tracing/datadog"
"github.com/containous/traefik/tracing/jaeger" "github.com/containous/traefik/tracing/jaeger"
@ -170,6 +170,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
defaultMarathon.ResponseHeaderTimeout = parse.Duration(60 * time.Second) defaultMarathon.ResponseHeaderTimeout = parse.Duration(60 * time.Second)
defaultMarathon.TLSHandshakeTimeout = parse.Duration(5 * time.Second) defaultMarathon.TLSHandshakeTimeout = parse.Duration(5 * time.Second)
defaultMarathon.KeepAlive = parse.Duration(10 * time.Second) defaultMarathon.KeepAlive = parse.Duration(10 * time.Second)
defaultMarathon.DefaultRule = marathon.DefaultTemplateRule
// default Consul // default Consul
var defaultConsul consul.Provider var defaultConsul consul.Provider

View file

@ -16,7 +16,6 @@ import (
"github.com/containous/traefik/old/provider/etcd" "github.com/containous/traefik/old/provider/etcd"
"github.com/containous/traefik/old/provider/eureka" "github.com/containous/traefik/old/provider/eureka"
"github.com/containous/traefik/old/provider/kubernetes" "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/mesos"
"github.com/containous/traefik/old/provider/rancher" "github.com/containous/traefik/old/provider/rancher"
"github.com/containous/traefik/old/provider/zk" "github.com/containous/traefik/old/provider/zk"
@ -24,6 +23,7 @@ import (
acmeprovider "github.com/containous/traefik/provider/acme" acmeprovider "github.com/containous/traefik/provider/acme"
"github.com/containous/traefik/provider/docker" "github.com/containous/traefik/provider/docker"
"github.com/containous/traefik/provider/file" "github.com/containous/traefik/provider/file"
"github.com/containous/traefik/provider/marathon"
"github.com/containous/traefik/provider/rest" "github.com/containous/traefik/provider/rest"
"github.com/containous/traefik/tls" "github.com/containous/traefik/tls"
"github.com/containous/traefik/tracing/datadog" "github.com/containous/traefik/tracing/datadog"

View file

@ -43,12 +43,8 @@ func init() {
// check.Suite(&ConsulSuite{}) // check.Suite(&ConsulSuite{})
// check.Suite(&DynamoDBSuite{}) // check.Suite(&DynamoDBSuite{})
// check.Suite(&EurekaSuite{}) // check.Suite(&EurekaSuite{})
// check.Suite(&MarathonSuite{})
// check.Suite(&MarathonSuite15{})
// check.Suite(&MesosSuite{}) // check.Suite(&MesosSuite{})
// FIXME use docker
// FIXME use consulcatalog // FIXME use consulcatalog
// check.Suite(&ConstraintSuite{}) // check.Suite(&ConstraintSuite{})
@ -64,6 +60,8 @@ func init() {
check.Suite(&HostResolverSuite{}) check.Suite(&HostResolverSuite{})
check.Suite(&HTTPSSuite{}) check.Suite(&HTTPSSuite{})
check.Suite(&LogRotationSuite{}) check.Suite(&LogRotationSuite{})
check.Suite(&MarathonSuite{})
check.Suite(&MarathonSuite15{})
check.Suite(&RateLimitSuite{}) check.Suite(&RateLimitSuite{})
check.Suite(&RestSuite{}) check.Suite(&RestSuite{})
check.Suite(&RetrySuite{}) check.Suite(&RetrySuite{})

View file

@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/containous/traefik/integration/try" "github.com/containous/traefik/integration/try"
"github.com/containous/traefik/old/provider/label"
"github.com/gambol99/go-marathon" "github.com/gambol99/go-marathon"
"github.com/go-check/check" "github.com/go-check/check"
checker "github.com/vdemeester/shakers" checker "github.com/vdemeester/shakers"
@ -98,7 +97,7 @@ func (s *MarathonSuite15) TestConfigurationUpdate(c *check.C) {
CPU(0.1). CPU(0.1).
Memory(32). Memory(32).
EmptyNetworks(). EmptyNetworks().
AddLabel(label.TraefikFrontendRule, "PathPrefix:/service") AddLabel("traefik.Routers.rt.Rule", "PathPrefix:/service")
app.Container. app.Container.
Expose(80). Expose(80).
Docker. Docker.
@ -118,7 +117,7 @@ func (s *MarathonSuite15) TestConfigurationUpdate(c *check.C) {
CPU(0.1). CPU(0.1).
Memory(32). Memory(32).
EmptyNetworks(). EmptyNetworks().
AddLabel(label.Prefix+"app"+label.TraefikFrontendRule, "PathPrefix:/app") AddLabel("traefik.Routers.app.Rule", "PathPrefix:/app")
app.Container. app.Container.
Expose(80). Expose(80).
Docker. Docker.

View file

@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/containous/traefik/integration/try" "github.com/containous/traefik/integration/try"
"github.com/containous/traefik/old/provider/label"
"github.com/gambol99/go-marathon" "github.com/gambol99/go-marathon"
"github.com/go-check/check" "github.com/go-check/check"
checker "github.com/vdemeester/shakers" checker "github.com/vdemeester/shakers"
@ -109,7 +108,7 @@ func (s *MarathonSuite) TestConfigurationUpdate(c *check.C) {
Name("/whoami"). Name("/whoami").
CPU(0.1). CPU(0.1).
Memory(32). Memory(32).
AddLabel(label.TraefikFrontendRule, "PathPrefix:/service") AddLabel("traefik.Routers.rt.Rule", "PathPrefix:/service")
app.Container.Docker.Bridged(). app.Container.Docker.Bridged().
Expose(80). Expose(80).
Container("containous/whoami") Container("containous/whoami")
@ -126,7 +125,7 @@ func (s *MarathonSuite) TestConfigurationUpdate(c *check.C) {
Name("/whoami"). Name("/whoami").
CPU(0.1). CPU(0.1).
Memory(32). Memory(32).
AddLabel(label.Prefix+"app"+label.TraefikFrontendRule, "PathPrefix:/app") AddLabel("traefik.Routers.app.Rule", "PathPrefix:/app")
app.Container.Docker.Bridged(). app.Container.Docker.Bridged().
Expose(80). Expose(80).
Container("containous/whoami") Container("containous/whoami")

View file

@ -1,6 +1,12 @@
package log 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. // Debug logs a message at level Debug on the standard logger.
// Deprecated // Deprecated
@ -78,3 +84,56 @@ func Fatalf(format string, args ...interface{}) {
func AddHook(hook logrus.Hook) { func AddHook(hook logrus.Hook) {
logrus.AddHook(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()
}

View file

@ -22,7 +22,6 @@ import (
"github.com/containous/traefik/old/provider/etcd" "github.com/containous/traefik/old/provider/etcd"
"github.com/containous/traefik/old/provider/eureka" "github.com/containous/traefik/old/provider/eureka"
"github.com/containous/traefik/old/provider/kubernetes" "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/mesos"
"github.com/containous/traefik/old/provider/rancher" "github.com/containous/traefik/old/provider/rancher"
"github.com/containous/traefik/old/provider/rest" "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 KeepTrailingSlash bool `description:"Do not remove trailing slash." export:"true"` // Deprecated
Docker *docker.Provider `description:"Enable Docker backend with default settings" export:"true"` Docker *docker.Provider `description:"Enable Docker backend with default settings" export:"true"`
File *file.Provider `description:"Enable File 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"` 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"` 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"` Etcd *etcd.Provider `description:"Enable Etcd backend with default settings" export:"true"`

View file

@ -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
}
}

View file

@ -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)
})
}
}

File diff suppressed because it is too large Load diff

View file

@ -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)
}

View file

@ -27,6 +27,10 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator {
p.quietAddProvider(conf.Docker) p.quietAddProvider(conf.Docker)
} }
if conf.Marathon != nil {
p.quietAddProvider(conf.Marathon)
}
if conf.Rest != nil { if conf.Rest != nil {
p.quietAddProvider(conf.Rest) p.quietAddProvider(conf.Rest)
} }

View file

@ -4,22 +4,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/containous/traefik/old/provider/label"
"github.com/gambol99/go-marathon" "github.com/gambol99/go-marathon"
) )
const testTaskName = "taskID" 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. // Functions related to building applications.
func withApplications(apps ...marathon.Application) *marathon.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) { func portDefinition(port int) func(*marathon.Application) {
return func(app *marathon.Application) { return func(app *marathon.Application) {
app.AddPortDefinition(marathon.PortDefinition{ app.AddPortDefinition(marathon.PortDefinition{

276
provider/marathon/config.go Normal file
View file

@ -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{}
}

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ package marathon
import ( import (
"errors" "errors"
"github.com/containous/traefik/old/provider/marathon/mocks" "github.com/containous/traefik/provider/marathon/mocks"
"github.com/gambol99/go-marathon" "github.com/gambol99/go-marathon"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )

View file

@ -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)
}

View file

@ -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)
})
}
}

View file

@ -1,23 +1,29 @@
package marathon package marathon
import ( import (
"context"
"fmt"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"text/template"
"time" "time"
"github.com/cenk/backoff" "github.com/cenk/backoff"
"github.com/containous/flaeg/parse" "github.com/containous/flaeg/parse"
"github.com/containous/traefik/config"
"github.com/containous/traefik/job" "github.com/containous/traefik/job"
"github.com/containous/traefik/old/log" "github.com/containous/traefik/log"
"github.com/containous/traefik/old/provider"
"github.com/containous/traefik/old/types" "github.com/containous/traefik/old/types"
"github.com/containous/traefik/provider"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
"github.com/gambol99/go-marathon" "github.com/gambol99/go-marathon"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const ( const (
// DefaultTemplateRule The default template for the default rule.
DefaultTemplateRule = "Host:{{ normalize .Name }}"
traceMaxScanTokenSize = 1024 * 1024 traceMaxScanTokenSize = 1024 * 1024
marathonEventIDs = marathon.EventIDApplications | marathonEventIDs = marathon.EventIDApplications |
marathon.EventIDAddHealthCheck | marathon.EventIDAddHealthCheck |
@ -36,23 +42,15 @@ const (
taskStateStaging TaskState = "TASK_STAGING" taskStateStaging TaskState = "TASK_STAGING"
) )
const (
labelIPAddressIdx = "traefik.ipAddressIdx"
labelLbCompatibilityGroup = "HAPROXY_GROUP"
labelLbCompatibility = "HAPROXY_0_VHOST"
)
var _ provider.Provider = (*Provider)(nil) var _ provider.Provider = (*Provider)(nil)
// Provider holds configuration of the provider. // Provider holds configuration of the provider.
type Provider struct { type Provider struct {
provider.BaseProvider provider.BaseProvider
Endpoint string `description:"Marathon server endpoint. You can also specify multiple endpoint for Marathon" export:"true"` 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"` 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"` 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"` FilterMarathonConstraints bool `description:"Enable use of Marathon constraints in constraint filtering" export:"true"`
TLS *types.ClientTLS `description:"Enable TLS support" export:"true"` TLS *types.ClientTLS `description:"Enable TLS support" export:"true"`
DialerTimeout parse.Duration `description:"Set a dialer timeout for Marathon" 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"` RespectReadinessChecks bool `description:"Filter out tasks with non-successful readiness checks during deployments" export:"true"`
readyChecker *readinessChecker readyChecker *readinessChecker
marathonClient marathon.Marathon marathonClient marathon.Marathon
defaultRuleTpl *template.Template
} }
// Basic holds basic authentication specific configurations // Basic holds basic authentication specific configurations
@ -73,39 +72,59 @@ type Basic struct {
} }
// Init the provider // Init the provider
func (p *Provider) Init(constraints types.Constraints) error { func (p *Provider) Init() error {
return p.BaseProvider.Init(constraints) 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 // Provide allows the marathon provider to provide configurations to traefik
// using the given configuration channel. // 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 { operation := func() error {
config := marathon.NewDefaultConfig()
config.URL = p.Endpoint confg := marathon.NewDefaultConfig()
config.EventsTransport = marathon.EventsTransportSSE confg.URL = p.Endpoint
confg.EventsTransport = marathon.EventsTransportSSE
if p.Trace { if p.Trace {
config.LogOutput = log.CustomWriterLevel(logrus.DebugLevel, traceMaxScanTokenSize) confg.LogOutput = log.CustomWriterLevel(logrus.DebugLevel, traceMaxScanTokenSize)
} }
if p.Basic != nil { if p.Basic != nil {
config.HTTPBasicAuthUser = p.Basic.HTTPBasicAuthUser confg.HTTPBasicAuthUser = p.Basic.HTTPBasicAuthUser
config.HTTPBasicPassword = p.Basic.HTTPBasicPassword confg.HTTPBasicPassword = p.Basic.HTTPBasicPassword
} }
var rc *readinessChecker var rc *readinessChecker
if p.RespectReadinessChecks { if p.RespectReadinessChecks {
log.Debug("Enabling Marathon readiness checker") logger.Debug("Enabling Marathon readiness checker")
rc = defaultReadinessChecker(p.Trace) rc = defaultReadinessChecker(p.Trace)
} }
p.readyChecker = rc p.readyChecker = rc
if len(p.DCOSToken) > 0 { if len(p.DCOSToken) > 0 {
config.DCOSToken = p.DCOSToken confg.DCOSToken = p.DCOSToken
} }
TLSConfig, err := p.TLS.CreateTLSConfig() TLSConfig, err := p.TLS.CreateTLSConfig()
if err != nil { if err != nil {
return err return err
} }
config.HTTPClient = &http.Client{ confg.HTTPClient = &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: (&net.Dialer{ DialContext: (&net.Dialer{
KeepAlive: time.Duration(p.KeepAlive), KeepAlive: time.Duration(p.KeepAlive),
@ -116,9 +135,9 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
TLSClientConfig: TLSConfig, TLSClientConfig: TLSConfig,
}, },
} }
client, err := marathon.NewClient(config) client, err := marathon.NewClient(confg)
if err != nil { 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 return err
} }
p.marathonClient = client p.marathonClient = client
@ -126,7 +145,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
if p.Watch { if p.Watch {
update, err := client.AddEventsListener(marathonEventIDs) update, err := client.AddEventsListener(marathonEventIDs)
if err != nil { if err != nil {
log.Errorf("Failed to register for events, %s", err) logger.Errorf("Failed to register for events, %s", err)
return err return err
} }
pool.Go(func(stop chan bool) { pool.Go(func(stop chan bool) {
@ -136,13 +155,13 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
case <-stop: case <-stop:
return return
case event := <-update: case event := <-update:
log.Debugf("Received provider event %s", event) logger.Debugf("Received provider event %s", event)
configuration := p.getConfiguration() conf := p.getConfigurations(ctx)
if configuration != nil { if conf != nil {
configurationChan <- types.ConfigMessage{ configurationChan <- config.Message{
ProviderName: "marathon", ProviderName: "marathon",
Configuration: configuration, Configuration: conf,
} }
} }
} }
@ -150,8 +169,8 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
}) })
} }
configuration := p.getConfiguration() configuration := p.getConfigurations(ctx)
configurationChan <- types.ConfigMessage{ configurationChan <- config.Message{
ProviderName: "marathon", ProviderName: "marathon",
Configuration: configuration, Configuration: configuration,
} }
@ -159,23 +178,23 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
} }
notify := func(err error, time time.Duration) { 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) err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
if err != nil { if err != nil {
log.Errorf("Cannot connect to Provider server %+v", err) logger.Errorf("Cannot connect to Provider server: %+v", err)
} }
return nil return nil
} }
func (p *Provider) getConfiguration() *types.Configuration { func (p *Provider) getConfigurations(ctx context.Context) *config.Configuration {
applications, err := p.getApplications() applications, err := p.getApplications()
if err != nil { 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 nil
} }
return p.buildConfiguration(applications) return p.buildConfiguration(ctx, applications)
} }
func (p *Provider) getApplications() (*marathon.Applications, error) { func (p *Provider) getApplications() (*marathon.Applications, error) {