diff --git a/README.md b/README.md index e24d19774..5df97971a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Træfɪk is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease. -It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Kubernetes](http://kubernetes.io/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), Rest API, file...) to manage its configuration automatically and dynamically. +It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Mesos](https://github.com/apache/mesos), [Kubernetes](http://kubernetes.io/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), Rest API, file...) to manage its configuration automatically and dynamically. ## Overview diff --git a/configuration.go b/configuration.go index 2aa4fe920..7cfdebc4a 100644 --- a/configuration.go +++ b/configuration.go @@ -42,6 +42,7 @@ type GlobalConfiguration struct { Zookeeper *provider.Zookepper `description:"Enable Zookeeper backend"` Boltdb *provider.BoltDb `description:"Enable Boltdb backend"` Kubernetes *provider.Kubernetes `description:"Enable Kubernetes backend"` + Mesos *provider.Mesos `description:"Enable Mesos backend"` } // DefaultEntryPoints holds default entry points @@ -277,6 +278,13 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { defaultKubernetes.LabelSelector = "" defaultKubernetes.Constraints = []types.Constraint{} + // default Mesos + var defaultMesos provider.Mesos + defaultMesos.Watch = true + defaultMesos.Endpoint = "http://127.0.0.1:5050" + defaultMesos.ExposedByDefault = true + defaultMesos.Constraints = []types.Constraint{} + defaultConfiguration := GlobalConfiguration{ Docker: &defaultDocker, File: &defaultFile, @@ -288,6 +296,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { Zookeeper: &defaultZookeeper, Boltdb: &defaultBoltDb, Kubernetes: &defaultKubernetes, + Mesos: &defaultMesos, Retry: &Retry{}, } return &TraefikConfiguration{ diff --git a/glide.lock b/glide.lock index c2c0495a3..e7694a0e2 100644 --- a/glide.lock +++ b/glide.lock @@ -87,6 +87,12 @@ imports: version: a558128c87724cd7430060ef5aedf39f83937f55 - name: github.com/go-check/check version: 4f90aeace3a26ad7021961c297b22c42160c7b25 +- name: github.com/gogo/protobuf + version: 8b3113fff1787050d4f5fcbf1173b857eec36566 + subpackages: + - proto +- name: github.com/golang/glog + version: fca8c8854093a154ff1eb580aae10276ad6b1b5f - name: github.com/google/go-querystring version: 9235644dd9e52eeae6fa48efd539fdc351a0af53 subpackages: @@ -104,6 +110,8 @@ imports: subpackages: - coordinate - serf +- name: github.com/jarcoal/httpmock + version: 145b10d659265440f062c31ea15326166bae56ee - name: github.com/libkermit/docker version: 3b5eb2973efff7af33cfb65141deaf4ed25c6d02 - name: github.com/libkermit/docker-check @@ -114,6 +122,15 @@ imports: version: fd192d755b00c968d312d23f521eb0cdc6f66bd0 - name: github.com/mattn/go-shellwords version: 525bedee691b5a8df547cb5cf9f86b7fb1883e24 +- name: github.com/mesos/mesos-go + version: 7064d8760d60f029f568b9295e6842612e89e347 + subpackages: + - mesosproto + - mesos + - upid + - mesosutil + - detector + - detector/zoo - name: github.com/Microsoft/go-winio version: ce2922f643c8fd76b46cadc7f404a06282678b34 - name: github.com/miekg/dns @@ -134,6 +151,19 @@ imports: - difflib - name: github.com/ryanuber/go-glob version: 572520ed46dbddaed19ea3d9541bdd0494163693 +- name: github.com/mesosphere/mesos-dns + vcs: git + repo: https://github.com/saagie/mesos-dns.git + version: 618029acf9827913fdb9cc67fb6f44b681e60a37 + subpackages: + - detect + - records + - records/state + - util + - logging + - errorutil + - models + - records/labels - name: github.com/samuel/go-zookeeper version: e64db453f3512cade908163702045e0f31137843 subpackages: @@ -151,6 +181,8 @@ imports: - assert - name: github.com/thoas/stats version: 69e3c072eec2df2df41afe6214f62eb940e4cd80 +- name: github.com/tv42/zbase32 + version: 03389da7e0bf9844767f82690f4d68fc097a1306 - name: github.com/unrolled/render version: 198ad4d8b8a4612176b804ca10555b222a086b40 - name: github.com/vdemeester/docker-events diff --git a/glide.yaml b/glide.yaml index 3ca211d25..36e73fa4b 100644 --- a/glide.yaml +++ b/glide.yaml @@ -82,3 +82,16 @@ import: - package: github.com/mattn/go-shellwords - package: github.com/vdemeester/shakers - package: github.com/ryanuber/go-glob +- package: github.com/mesos/mesos-go + subpackages: + - mesosproto + - mesos + - upid + - mesosutil + - detector +- package: github.com/jarcoal/httpmock +- package: github.com/mesosphere/mesos-dns + vcs: git + repo: https://github.com/containous/mesos-dns.git + version: b47dc4c19f215e98da687b15b4c64e70f629bea5 +- package: github.com/tv42/zbase32 diff --git a/integration/fixtures/mesos/simple.toml b/integration/fixtures/mesos/simple.toml new file mode 100644 index 000000000..0b330b62a --- /dev/null +++ b/integration/fixtures/mesos/simple.toml @@ -0,0 +1,9 @@ +defaultEntryPoints = ["http"] + +[entryPoints] + [entryPoints.http] + address = ":8000" + +logLevel = "DEBUG" + +[mesos] diff --git a/integration/integration_test.go b/integration/integration_test.go index 9d7bff2bb..56cf59ced 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -32,6 +32,7 @@ func init() { check.Suite(&EtcdSuite{}) check.Suite(&MarathonSuite{}) check.Suite(&ConstraintSuite{}) + check.Suite(&MesosSuite{}) } var traefikBinary = "../dist/traefik" diff --git a/integration/mesos_test.go b/integration/mesos_test.go new file mode 100644 index 000000000..d8cd1654a --- /dev/null +++ b/integration/mesos_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "net/http" + "os/exec" + "time" + + "github.com/go-check/check" + + checker "github.com/vdemeester/shakers" +) + +// Mesos test suites (using libcompose) +type MesosSuite struct{ BaseSuite } + +func (s *MesosSuite) SetUpSuite(c *check.C) { + s.createComposeProject(c, "mesos") +} + +func (s *MesosSuite) TestSimpleConfiguration(c *check.C) { + cmd := exec.Command(traefikBinary, "--configFile=fixtures/mesos/simple.toml") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + time.Sleep(500 * time.Millisecond) + // TODO validate : run on 80 + resp, err := http.Get("http://127.0.0.1:8000/") + + // Expected a 404 as we did not configure anything + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, 404) +} diff --git a/integration/resources/compose/mesos.yml b/integration/resources/compose/mesos.yml new file mode 100644 index 000000000..14181143e --- /dev/null +++ b/integration/resources/compose/mesos.yml @@ -0,0 +1,34 @@ +zk: + image: bobrik/zookeeper + net: host + environment: + ZK_CONFIG: tickTime=2000,initLimit=10,syncLimit=5,maxClientCnxns=128,forceSync=no,clientPort=2181 + ZK_ID: " 1" + +master: + image: mesosphere/mesos-master:0.28.1-2.0.20.ubuntu1404 + net: host + environment: + MESOS_ZK: zk://127.0.0.1:2181/mesos + MESOS_HOSTNAME: 127.0.0.1 + MESOS_IP: 127.0.0.1 + MESOS_QUORUM: " 1" + MESOS_CLUSTER: docker-compose + MESOS_WORK_DIR: /var/lib/mesos + +slave: + image: mesosphere/mesos-slave:0.28.1-2.0.20.ubuntu1404 + net: host + pid: host + privileged: true + environment: + MESOS_MASTER: zk://127.0.0.1:2181/mesos + MESOS_HOSTNAME: 127.0.0.1 + MESOS_IP: 127.0.0.1 + MESOS_CONTAINERIZERS: docker,mesos + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup + - /usr/bin/docker:/usr/bin/docker:ro + - /usr/lib/x86_64-linux-gnu/libapparmor.so.1:/usr/lib/x86_64-linux-gnu/libapparmor.so.1:ro + - /var/run/docker.sock:/var/run/docker.sock + - /lib/x86_64-linux-gnu/libsystemd-journal.so.0:/lib/x86_64-linux-gnu/libsystemd-journal.so.0 diff --git a/provider/mesos.go b/provider/mesos.go new file mode 100644 index 000000000..25ac4056e --- /dev/null +++ b/provider/mesos.go @@ -0,0 +1,444 @@ +package provider + +import ( + "errors" + "strconv" + "strings" + "text/template" + + "fmt" + "github.com/BurntSushi/ty/fun" + log "github.com/Sirupsen/logrus" + "github.com/cenkalti/backoff" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" + "github.com/mesos/mesos-go/detector" + _ "github.com/mesos/mesos-go/detector/zoo" // Registers the ZK detector + "github.com/mesosphere/mesos-dns/detect" + "github.com/mesosphere/mesos-dns/logging" + "github.com/mesosphere/mesos-dns/records" + "github.com/mesosphere/mesos-dns/records/state" + "github.com/mesosphere/mesos-dns/util" + "sort" + "time" +) + +//Mesos holds configuration of the mesos provider. +type Mesos struct { + BaseProvider + Endpoint string `description:"Mesos server endpoint. You can also specify multiple endpoint for Mesos"` + Domain string `description:"Default domain used"` + ExposedByDefault bool `description:"Expose Mesos apps by default"` + GroupsAsSubDomains bool `description:"Convert Mesos groups to subdomains"` + ZkDetectionTimeout int `description:"ZkDetectionTimeout"` + RefreshSeconds int `description:"RefreshSeconds"` + IPSources string `description:"IPSources"` // e.g. "host", "docker", "mesos", "rkt" + StateTimeoutSecond int `description:"HTTP Timeout (in seconds)"` + Masters []string +} + +// Provide allows the provider to provide configurations to traefik +// using the given configuration channel. +func (provider *Mesos) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []types.Constraint) error { + operation := func() error { + + // initialize logging + logging.SetupLogs() + + log.Debugf("%s", provider.IPSources) + + var zk string + var masters []string + + if strings.HasPrefix(provider.Endpoint, "zk://") { + zk = provider.Endpoint + } else { + masters = strings.Split(provider.Endpoint, ",") + } + + errch := make(chan error) + + changed := detectMasters(zk, masters) + reload := time.NewTicker(time.Second * time.Duration(provider.RefreshSeconds)) + zkTimeout := time.Second * time.Duration(provider.ZkDetectionTimeout) + timeout := time.AfterFunc(zkTimeout, func() { + if zkTimeout > 0 { + errch <- fmt.Errorf("master detection timed out after %s", zkTimeout) + } + }) + + defer reload.Stop() + defer util.HandleCrash() + + if !provider.Watch { + reload.Stop() + timeout.Stop() + } + + for { + select { + case <-reload.C: + configuration := provider.loadMesosConfig() + if configuration != nil { + configurationChan <- types.ConfigMessage{ + ProviderName: "mesos", + Configuration: configuration, + } + } + case masters := <-changed: + if len(masters) == 0 || masters[0] == "" { + // no leader + timeout.Reset(zkTimeout) + } else { + timeout.Stop() + } + log.Debugf("new masters detected: %v", masters) + provider.Masters = masters + configuration := provider.loadMesosConfig() + if configuration != nil { + configurationChan <- types.ConfigMessage{ + ProviderName: "mesos", + Configuration: configuration, + } + } + case err := <-errch: + log.Errorf("%s", err) + } + } + } + + notify := func(err error, time time.Duration) { + log.Errorf("mesos connection error %+v, retrying in %s", err, time) + } + err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify) + if err != nil { + log.Fatalf("Cannot connect to mesos server %+v", err) + } + return nil +} + +func (provider *Mesos) loadMesosConfig() *types.Configuration { + var mesosFuncMap = template.FuncMap{ + "getBackend": provider.getBackend, + "getPort": provider.getPort, + "getHost": provider.getHost, + "getWeight": provider.getWeight, + "getDomain": provider.getDomain, + "getProtocol": provider.getProtocol, + "getPassHostHeader": provider.getPassHostHeader, + "getPriority": provider.getPriority, + "getEntryPoints": provider.getEntryPoints, + "getFrontendRule": provider.getFrontendRule, + "getFrontendBackend": provider.getFrontendBackend, + "getID": provider.getID, + "getFrontEndName": provider.getFrontEndName, + "replace": replace, + } + + t := records.NewRecordGenerator(time.Duration(provider.StateTimeoutSecond) * time.Second) + sj, err := t.FindMaster(provider.Masters...) + if err != nil { + log.Errorf("Failed to create a client for mesos, error: %s", err) + return nil + } + tasks := provider.taskRecords(sj) + + //filter tasks + filteredTasks := fun.Filter(func(task state.Task) bool { + return mesosTaskFilter(task, provider.ExposedByDefault) + }, tasks).([]state.Task) + + filteredApps := []state.Task{} + for _, value := range filteredTasks { + if !taskInSlice(value, filteredApps) { + filteredApps = append(filteredApps, value) + } + } + + templateObjects := struct { + Applications []state.Task + Tasks []state.Task + Domain string + }{ + filteredApps, + filteredTasks, + provider.Domain, + } + + configuration, err := provider.getConfiguration("templates/mesos.tmpl", mesosFuncMap, templateObjects) + if err != nil { + log.Error(err) + } + return configuration +} + +func taskInSlice(a state.Task, list []state.Task) bool { + for _, b := range list { + if b.DiscoveryInfo.Name == a.DiscoveryInfo.Name { + return true + } + } + return false +} + +// labels returns all given Status.[]Labels' values whose keys are equal +// to the given key +func labels(task state.Task, key string) string { + for _, l := range task.Labels { + if l.Key == key { + return l.Value + } + } + return "" +} + +func mesosTaskFilter(task state.Task, exposedByDefaultFlag bool) bool { + if len(task.DiscoveryInfo.Ports.DiscoveryPorts) == 0 { + log.Debugf("Filtering mesos task without port %s", task.Name) + return false + } + if !isMesosApplicationEnabled(task, exposedByDefaultFlag) { + log.Debugf("Filtering disabled mesos task %s", task.DiscoveryInfo.Name) + return false + } + + //filter indeterminable task port + portIndexLabel := labels(task, "traefik.portIndex") + portValueLabel := labels(task, "traefik.port") + if portIndexLabel != "" && portValueLabel != "" { + log.Debugf("Filtering mesos task %s specifying both traefik.portIndex and traefik.port labels", task.Name) + return false + } + if portIndexLabel == "" && portValueLabel == "" && len(task.DiscoveryInfo.Ports.DiscoveryPorts) > 1 { + log.Debugf("Filtering mesos task %s with more than 1 port and no traefik.portIndex or traefik.port label", task.Name) + return false + } + if portIndexLabel != "" { + index, err := strconv.Atoi(labels(task, "traefik.portIndex")) + if err != nil || index < 0 || index > len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1 { + log.Debugf("Filtering mesos task %s with unexpected value for traefik.portIndex label", task.Name) + return false + } + } + if portValueLabel != "" { + port, err := strconv.Atoi(labels(task, "traefik.port")) + if err != nil { + log.Debugf("Filtering mesos task %s with unexpected value for traefik.port label", task.Name) + return false + } + + var foundPort bool + for _, exposedPort := range task.DiscoveryInfo.Ports.DiscoveryPorts { + if port == exposedPort.Number { + foundPort = true + break + } + } + + if !foundPort { + log.Debugf("Filtering mesos task %s without a matching port for traefik.port label", task.Name) + return false + } + } + + //filter healthchecks + if task.Statuses != nil && len(task.Statuses) > 0 && task.Statuses[0].Healthy != nil && !*task.Statuses[0].Healthy { + log.Debugf("Filtering mesos task %s with bad healthcheck", task.DiscoveryInfo.Name) + return false + + } + return true +} + +func getMesos(task state.Task, apps []state.Task) (state.Task, error) { + for _, application := range apps { + if application.DiscoveryInfo.Name == task.DiscoveryInfo.Name { + return application, nil + } + } + return state.Task{}, errors.New("Application not found: " + task.DiscoveryInfo.Name) +} + +func isMesosApplicationEnabled(task state.Task, exposedByDefault bool) bool { + return exposedByDefault && labels(task, "traefik.enable") != "false" || labels(task, "traefik.enable") == "true" +} + +func (provider *Mesos) getLabel(task state.Task, label string) (string, error) { + for _, tmpLabel := range task.Labels { + if tmpLabel.Key == label { + return tmpLabel.Value, nil + } + } + return "", errors.New("Label not found:" + label) +} + +func (provider *Mesos) getPort(task state.Task, applications []state.Task) string { + application, err := getMesos(task, applications) + if err != nil { + log.Errorf("Unable to get mesos application from task %s", task.DiscoveryInfo.Name) + return "" + } + + if portIndexLabel, err := provider.getLabel(application, "traefik.portIndex"); err == nil { + if index, err := strconv.Atoi(portIndexLabel); err == nil { + return strconv.Itoa(task.DiscoveryInfo.Ports.DiscoveryPorts[index].Number) + } + } + if portValueLabel, err := provider.getLabel(application, "traefik.port"); err == nil { + return portValueLabel + } + + for _, port := range task.DiscoveryInfo.Ports.DiscoveryPorts { + return strconv.Itoa(port.Number) + } + return "" +} + +func (provider *Mesos) getWeight(task state.Task, applications []state.Task) string { + application, errApp := getMesos(task, applications) + if errApp != nil { + log.Errorf("Unable to get mesos application from task %s", task.DiscoveryInfo.Name) + return "0" + } + + if label, err := provider.getLabel(application, "traefik.weight"); err == nil { + return label + } + return "0" +} + +func (provider *Mesos) getDomain(task state.Task) string { + if label, err := provider.getLabel(task, "traefik.domain"); err == nil { + return label + } + return provider.Domain +} + +func (provider *Mesos) getProtocol(task state.Task, applications []state.Task) string { + application, errApp := getMesos(task, applications) + if errApp != nil { + log.Errorf("Unable to get mesos application from task %s", task.DiscoveryInfo.Name) + return "http" + } + if label, err := provider.getLabel(application, "traefik.protocol"); err == nil { + return label + } + return "http" +} + +func (provider *Mesos) getPassHostHeader(task state.Task) string { + if passHostHeader, err := provider.getLabel(task, "traefik.frontend.passHostHeader"); err == nil { + return passHostHeader + } + return "false" +} + +func (provider *Mesos) getPriority(task state.Task) string { + if priority, err := provider.getLabel(task, "traefik.frontend.priority"); err == nil { + return priority + } + return "0" +} + +func (provider *Mesos) getEntryPoints(task state.Task) []string { + if entryPoints, err := provider.getLabel(task, "traefik.frontend.entryPoints"); err == nil { + return strings.Split(entryPoints, ",") + } + return []string{} +} + +// getFrontendRule returns the frontend rule for the specified application, using +// it's label. It returns a default one (Host) if the label is not present. +func (provider *Mesos) getFrontendRule(task state.Task) string { + if label, err := provider.getLabel(task, "traefik.frontend.rule"); err == nil { + return label + } + return "Host:" + strings.ToLower(strings.Replace(provider.getSubDomain(task.DiscoveryInfo.Name), "_", "-", -1)) + "." + provider.Domain +} + +func (provider *Mesos) getBackend(task state.Task, applications []state.Task) string { + application, errApp := getMesos(task, applications) + if errApp != nil { + log.Errorf("Unable to get mesos application from task %s", task.DiscoveryInfo.Name) + return "" + } + return provider.getFrontendBackend(application) +} + +func (provider *Mesos) getFrontendBackend(task state.Task) string { + if label, err := provider.getLabel(task, "traefik.backend"); err == nil { + return label + } + return "-" + cleanupSpecialChars(task.DiscoveryInfo.Name) +} + +func (provider *Mesos) getHost(task state.Task) string { + return task.IP(strings.Split(provider.IPSources, ",")...) +} + +func (provider *Mesos) getID(task state.Task) string { + return cleanupSpecialChars(task.ID) +} + +func (provider *Mesos) getFrontEndName(task state.Task) string { + return strings.Replace(cleanupSpecialChars(task.ID), "/", "-", -1) +} + +func cleanupSpecialChars(s string) string { + return strings.Replace(strings.Replace(strings.Replace(s, ".", "-", -1), ":", "-", -1), "_", "-", -1) +} + +func detectMasters(zk string, masters []string) <-chan []string { + changed := make(chan []string, 1) + if zk != "" { + log.Debugf("Starting master detector for ZK ", zk) + if md, err := detector.New(zk); err != nil { + log.Fatalf("failed to create master detector: %v", err) + } else if err := md.Detect(detect.NewMasters(masters, changed)); err != nil { + log.Fatalf("failed to initialize master detector: %v", err) + } + } else { + changed <- masters + } + return changed +} + +func (provider *Mesos) taskRecords(sj state.State) []state.Task { + var p []state.Task // == nil + for _, f := range sj.Frameworks { + for _, task := range f.Tasks { + for _, slave := range sj.Slaves { + if task.SlaveID == slave.ID { + task.SlaveIP = slave.Hostname + } + } + + // only do running and discoverable tasks + if task.State == "TASK_RUNNING" { + p = append(p, task) + } + } + } + + return p +} + +// ErrorFunction A function definition that returns an error +// to be passed to the Ignore or Panic error handler +type ErrorFunction func() error + +// Ignore Calls an ErrorFunction, and ignores the result. +// This allows us to be more explicit when there is no error +// handling to be done, for example in defers +func Ignore(f ErrorFunction) { + _ = f() +} +func (provider *Mesos) getSubDomain(name string) string { + if provider.GroupsAsSubDomains { + splitedName := strings.Split(strings.TrimPrefix(name, "/"), "/") + sort.Sort(sort.Reverse(sort.StringSlice(splitedName))) + reverseName := strings.Join(splitedName, ".") + return reverseName + } + return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) +} diff --git a/provider/mesos_test.go b/provider/mesos_test.go new file mode 100644 index 000000000..1c1eb2376 --- /dev/null +++ b/provider/mesos_test.go @@ -0,0 +1,351 @@ +package provider + +import ( + log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/types" + "github.com/mesosphere/mesos-dns/records/state" + "reflect" + "testing" +) + +func TestMesosTaskFilter(t *testing.T) { + + cases := []struct { + mesosTask state.Task + expected bool + exposedByDefault bool + }{ + { + mesosTask: state.Task{}, + expected: false, + exposedByDefault: true, + }, + { + mesosTask: task(statuses(status(setState("TASK_RUNNING")))), + expected: false, + exposedByDefault: true, + }, + { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "false"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: false, // because label traefik.enable = false + exposedByDefault: false, + }, + { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "true"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: true, + exposedByDefault: false, + }, + { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "true"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: true, + exposedByDefault: true, + }, + { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "false"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: false, // because label traefik.enable = false (even wherek exposedByDefault = true) + exposedByDefault: true, + }, + { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "true", + "traefik.portIndex", "1", + "traefik.port", "80"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: false, // traefik.portIndex & traefik.port cannot be set both + exposedByDefault: true, + }, + { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "true", + "traefik.portIndex", "1"), + discovery(setDiscoveryPorts("TCP", 80, "WEB HTTP", "TCP", 443, "WEB HTTPS")), + ), + expected: true, + exposedByDefault: true, + }, + { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "true"), + discovery(setDiscoveryPorts("TCP", 80, "WEB HTTP", "TCP", 443, "WEB HTTPS")), + ), + expected: false, // more than 1 discovery port but no traefik.port* label + exposedByDefault: true, + }, + { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "true", + "traefik.portIndex", "1"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: false, // traefik.portIndex and discoveryPorts don't correspond + exposedByDefault: true, + }, { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "true", + "traefik.portIndex", "0"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: true, // traefik.portIndex and discoveryPorts correspond + exposedByDefault: true, + }, { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "true", + "traefik.port", "TRAEFIK"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: false, // traefik.port is not an integer + exposedByDefault: true, + }, { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "true", + "traefik.port", "443"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: false, // traefik.port is not the same as discovery.port + exposedByDefault: true, + }, { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels("traefik.enable", "true", + "traefik.port", "80"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: true, // traefik.port is the same as discovery.port + exposedByDefault: true, + }, { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"))), + setLabels("traefik.enable", "true", + "traefik.port", "80"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: true, // No healthCheck + exposedByDefault: true, + }, { + mesosTask: task(statuses(status( + setState("TASK_RUNNING"), + setHealthy(false))), + setLabels("traefik.enable", "true", + "traefik.port", "80"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: false, // HealthCheck at false + exposedByDefault: true, + }, + } + + for _, c := range cases { + actual := mesosTaskFilter(c.mesosTask, c.exposedByDefault) + log.Errorf("Statuses : %v", c.mesosTask.Statuses) + log.Errorf("Label : %v", c.mesosTask.Labels) + log.Errorf("DiscoveryInfo : %v", c.mesosTask.DiscoveryInfo) + if actual != c.expected { + t.Fatalf("expected %v, got %v", c.expected, actual) + } + } +} + +func TestTaskRecords(t *testing.T) { + var task = state.Task{ + SlaveID: "s_id", + State: "TASK_RUNNING", + } + var framework = state.Framework{ + Tasks: []state.Task{task}, + } + var slave = state.Slave{ + ID: "s_id", + Hostname: "127.0.0.1", + } + var state = state.State{ + Slaves: []state.Slave{slave}, + Frameworks: []state.Framework{framework}, + } + + provider := &Mesos{ + Domain: "docker.localhost", + ExposedByDefault: true, + } + var p = provider.taskRecords(state) + if len(p) == 0 { + t.Fatal("taskRecord should return at least one task") + } + if p[0].SlaveIP != slave.Hostname { + t.Fatalf("The SlaveIP (%s) should be set with the slave hostname (%s)", p[0].SlaveID, slave.Hostname) + } +} + +func TestMesosLoadConfig(t *testing.T) { + cases := []struct { + applicationsError bool + tasksError bool + mesosTask state.Task + expected bool + exposedByDefault bool + expectedNil bool + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{} + for _, c := range cases { + provider := &Mesos{ + Domain: "docker.localhost", + ExposedByDefault: true, + } + actualConfig := provider.loadMesosConfig() + if c.expectedNil { + if actualConfig != nil { + t.Fatalf("Should have been nil, got %v", actualConfig) + } + } else { + // Compare backends + if !reflect.DeepEqual(actualConfig.Backends, c.expectedBackends) { + t.Fatalf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends) + } + if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) { + t.Fatalf("expected %#v, got %#v", c.expectedFrontends, actualConfig.Frontends) + } + } + } +} + +// test helpers + +type ( + taskOpt func(*state.Task) + statusOpt func(*state.Status) +) + +func task(opts ...taskOpt) state.Task { + var t state.Task + for _, opt := range opts { + opt(&t) + } + return t +} + +func statuses(st ...state.Status) taskOpt { + return func(t *state.Task) { + t.Statuses = append(t.Statuses, st...) + } +} + +func discovery(dp state.DiscoveryInfo) taskOpt { + return func(t *state.Task) { + t.DiscoveryInfo = dp + } +} + +func setLabels(kvs ...string) taskOpt { + return func(t *state.Task) { + if len(kvs)%2 != 0 { + panic("odd number") + } + + for i := 0; i < len(kvs); i += 2 { + var label = state.Label{Key: kvs[i], Value: kvs[i+1]} + log.Errorf("Label1.1 : %v", label) + t.Labels = append(t.Labels, label) + log.Errorf("Label1.2 : %v", t.Labels) + } + + } +} + +func status(opts ...statusOpt) state.Status { + var s state.Status + for _, opt := range opts { + opt(&s) + } + return s +} + +func setDiscoveryPort(proto string, port int, name string) state.DiscoveryInfo { + + dp := state.DiscoveryPort{ + Protocol: proto, + Number: port, + Name: name, + } + + discoveryPorts := []state.DiscoveryPort{dp} + + ports := state.Ports{ + DiscoveryPorts: discoveryPorts, + } + + return state.DiscoveryInfo{ + Ports: ports, + } +} + +func setDiscoveryPorts(proto1 string, port1 int, name1 string, proto2 string, port2 int, name2 string) state.DiscoveryInfo { + + dp1 := state.DiscoveryPort{ + Protocol: proto1, + Number: port1, + Name: name1, + } + + dp2 := state.DiscoveryPort{ + Protocol: proto2, + Number: port2, + Name: name2, + } + + discoveryPorts := []state.DiscoveryPort{dp1, dp2} + + ports := state.Ports{ + DiscoveryPorts: discoveryPorts, + } + + return state.DiscoveryInfo{ + Ports: ports, + } +} + +func setState(st string) statusOpt { + return func(s *state.Status) { + s.State = st + } +} +func setHealthy(b bool) statusOpt { + return func(s *state.Status) { + s.Healthy = &b + } +} diff --git a/server.go b/server.go index 238d64428..3411970f3 100644 --- a/server.go +++ b/server.go @@ -255,6 +255,9 @@ func (server *Server) configureProviders() { if server.globalConfiguration.Kubernetes != nil { server.providers = append(server.providers, server.globalConfiguration.Kubernetes) } + if server.globalConfiguration.Mesos != nil { + server.providers = append(server.providers, server.globalConfiguration.Mesos) + } } func (server *Server) startProviders() { diff --git a/templates/mesos.tmpl b/templates/mesos.tmpl new file mode 100644 index 000000000..a965deb8b --- /dev/null +++ b/templates/mesos.tmpl @@ -0,0 +1,18 @@ +{{$apps := .Applications}} +[backends]{{range .Tasks}} + [backends.backend{{getBackend . $apps}}.servers.server-{{getID .}}] + url = "{{getProtocol . $apps}}://{{getHost .}}:{{getPort . $apps}}" + weight = {{getWeight . $apps}} +{{end}} + +[frontends]{{range .Applications}} + [frontends.frontend-{{getFrontEndName .}}] + backend = "backend{{getFrontendBackend .}}" + passHostHeader = {{getPassHostHeader .}} + priority = {{getPriority .}} + entryPoints = [{{range getEntryPoints .}} + "{{.}}", + {{end}}] + [frontends.frontend-{{getFrontEndName .}}.routes.route-host{{getFrontEndName .}}] + rule = "{{getFrontendRule .}}" +{{end}} diff --git a/traefik.sample.toml b/traefik.sample.toml index 74474bc0c..55dd59cb5 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -331,6 +331,77 @@ # # dcosToken = "xxxxxx" + +################################################################ +# Mesos configuration backend +################################################################ + +# Enable Mesos configuration backend +# +# Optional +# +# [mesos] + +# Mesos server endpoint. +# You can also specify multiple endpoint for Mesos: +# endpoint = "192.168.35.40:5050,192.168.35.41:5050,192.168.35.42:5050" +# endpoint = "zk://192.168.35.20:2181,192.168.35.21:2181,192.168.35.22:2181/mesos" +# +# Required +# +# endpoint = "http://127.0.0.1:8080" + +# Enable watch Mesos changes +# +# Optional +# +# watch = true + +# Default domain used. +# Can be overridden by setting the "traefik.domain" label on an application. +# +# Required +# +# domain = "mesos.localhost" + +# Override default configuration template. For advanced users :) +# +# Optional +# +# filename = "mesos.tmpl" + +# Expose Mesos apps by default in traefik +# +# Optional +# Default: false +# +# ExposedByDefault = true + +# TLS client configuration. https://golang.org/pkg/crypto/tls/#Config +# +# Optional +# +# [mesos.TLS] +# InsecureSkipVerify = true + +# +# +# Optional +# +# ZkDetectionTimeout = 30 + +# +# +# Optional +# +# RefreshSeconds = 30 + +# +# +# Optional +# +# IPSources = "host" + ################################################################ # Kubernetes Ingress configuration backend ################################################################