diff --git a/provider/mesos/config.go b/provider/mesos/config.go new file mode 100644 index 000000000..eacde5ec6 --- /dev/null +++ b/provider/mesos/config.go @@ -0,0 +1,304 @@ +package mesos + +import ( + "fmt" + "math" + "strconv" + "strings" + "text/template" + "time" + + "github.com/BurntSushi/ty/fun" + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/mesosphere/mesos-dns/records" + "github.com/mesosphere/mesos-dns/records/state" +) + +func (p *Provider) buildConfiguration() *types.Configuration { + var mesosFuncMap = template.FuncMap{ + "getBackend": getBackend, + "getPort": p.getPort, + "getHost": p.getHost, + "getWeight": getFuncApplicationStringValue(label.TraefikWeight, label.DefaultWeight), + "getDomain": getFuncStringValue(label.TraefikDomain, p.Domain), + "getProtocol": getFuncApplicationStringValue(label.TraefikProtocol, label.DefaultProtocol), + "getPassHostHeader": getFuncStringValue(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), + "getPriority": getFuncStringValue(label.TraefikFrontendPriority, label.DefaultFrontendPriority), + "getEntryPoints": getFuncSliceStringValue(label.TraefikFrontendEntryPoints), + "getFrontendRule": p.getFrontendRule, + "getFrontendBackend": getFrontendBackend, + "getID": getID, + "getFrontEndName": getFrontEndName, + } + + rg := records.NewRecordGenerator(time.Duration(p.StateTimeoutSecond) * time.Second) + st, err := rg.FindMaster(p.Masters...) + if err != nil { + log.Errorf("Failed to create a client for Mesos, error: %v", err) + return nil + } + tasks := taskRecords(st) + + // filter tasks + filteredTasks := fun.Filter(func(task state.Task) bool { + return taskFilter(task, p.ExposedByDefault) + }, tasks).([]state.Task) + + uniqueApps := make(map[string]state.Task) + for _, value := range filteredTasks { + if _, ok := uniqueApps[value.DiscoveryInfo.Name]; !ok { + uniqueApps[value.DiscoveryInfo.Name] = value + } + } + var filteredApps []state.Task + for _, value := range uniqueApps { + filteredApps = append(filteredApps, value) + } + + templateObjects := struct { + Applications []state.Task + Tasks []state.Task + Domain string + }{ + Applications: filteredApps, + Tasks: filteredTasks, + Domain: p.Domain, + } + + configuration, err := p.GetConfiguration("templates/mesos.tmpl", mesosFuncMap, templateObjects) + if err != nil { + log.Error(err) + } + return configuration +} + +func taskRecords(st state.State) []state.Task { + var tasks []state.Task + for _, f := range st.Frameworks { + for _, task := range f.Tasks { + for _, slave := range st.Slaves { + if task.SlaveID == slave.ID { + task.SlaveIP = slave.Hostname + } + } + + // only do running and discoverable tasks + if task.State == "TASK_RUNNING" { + tasks = append(tasks, task) + } + } + } + + return tasks +} + +func taskFilter(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 !isEnabled(task, exposedByDefaultFlag) { + log.Debugf("Filtering disabled Mesos task %s", task.DiscoveryInfo.Name) + return false + } + + // filter indeterminable task port + portIndexLabel := getStringValue(task, label.TraefikPortIndex, "") + portValueLabel := getStringValue(task, label.TraefikPort, "") + if portIndexLabel != "" && portValueLabel != "" { + log.Debugf("Filtering Mesos task %s specifying both %q' and %q labels", task.Name, label.TraefikPortIndex, label.TraefikPort) + return false + } + if portIndexLabel != "" { + index, err := strconv.Atoi(portIndexLabel) + if err != nil || index < 0 || index > len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1 { + log.Debugf("Filtering Mesos task %s with unexpected value for %q label", task.Name, label.TraefikPortIndex) + return false + } + } + if portValueLabel != "" { + port, err := strconv.Atoi(portValueLabel) + if err != nil { + log.Debugf("Filtering Mesos task %s with unexpected value for %q label", task.Name, label.TraefikPort) + 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 %q label", task.Name, label.TraefikPort) + 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 getID(task state.Task) string { + return provider.Normalize(task.ID) +} + +func getBackend(task state.Task, apps []state.Task) string { + application, err := getApplication(task, apps) + if err != nil { + log.Error(err) + return "" + } + return getFrontendBackend(application) +} + +func getFrontendBackend(task state.Task) string { + if value := getStringValue(task, label.TraefikBackend, ""); len(value) > 0 { + return value + } + return "-" + provider.Normalize(task.DiscoveryInfo.Name) +} + +func getFrontEndName(task state.Task) string { + return provider.Normalize(task.ID) +} + +func (p *Provider) getPort(task state.Task, applications []state.Task) string { + application, err := getApplication(task, applications) + if err != nil { + log.Error(err) + return "" + } + + plv := getIntValue(application, label.TraefikPortIndex, math.MinInt32, len(task.DiscoveryInfo.Ports.DiscoveryPorts)-1) + if plv >= 0 { + return strconv.Itoa(task.DiscoveryInfo.Ports.DiscoveryPorts[plv].Number) + } + + if pv := getStringValue(application, label.TraefikPort, ""); len(pv) > 0 { + return pv + } + + for _, port := range task.DiscoveryInfo.Ports.DiscoveryPorts { + return strconv.Itoa(port.Number) + } + return "" +} + +// 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 (p *Provider) getFrontendRule(task state.Task) string { + if v := getStringValue(task, label.TraefikFrontendRule, ""); len(v) > 0 { + return v + } + return "Host:" + strings.ToLower(strings.Replace(p.getSubDomain(task.DiscoveryInfo.Name), "_", "-", -1)) + "." + p.Domain +} + +func (p *Provider) getHost(task state.Task) string { + return task.IP(strings.Split(p.IPSources, ",")...) +} + +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) +} + +// Label functions + +func getFuncApplicationStringValue(labelName string, defaultValue string) func(task state.Task, applications []state.Task) string { + return func(task state.Task, applications []state.Task) string { + app, err := getApplication(task, applications) + if err == nil { + return getStringValue(app, labelName, defaultValue) + } + log.Error(err) + return defaultValue + } +} + +func getFuncStringValue(labelName string, defaultValue string) func(task state.Task) string { + return func(task state.Task) string { + return getStringValue(task, labelName, defaultValue) + } +} + +func getFuncSliceStringValue(labelName string) func(task state.Task) []string { + return func(task state.Task) []string { + return getSliceStringValue(task, labelName) + } +} + +func getStringValue(task state.Task, labelName string, defaultValue string) string { + for _, lbl := range task.Labels { + if lbl.Key == labelName { + return lbl.Value + } + } + return defaultValue +} + +func getBoolValue(task state.Task, labelName string, defaultValue bool) bool { + for _, lbl := range task.Labels { + if lbl.Key == labelName { + v, err := strconv.ParseBool(lbl.Value) + if err == nil { + return v + } + } + } + return defaultValue +} + +func getIntValue(task state.Task, labelName string, defaultValue int, maxValue int) int { + for _, lbl := range task.Labels { + if lbl.Key == labelName { + value, err := strconv.Atoi(lbl.Value) + if err == nil { + if value <= maxValue { + return value + } + log.Warnf("The value %q for %q exceed the max authorized value %q, falling back to %v.", lbl.Value, labelName, maxValue, defaultValue) + } else { + log.Warnf("Unable to parse %q: %q, falling back to %v. %v", labelName, lbl.Value, defaultValue, err) + } + } + } + return defaultValue +} + +func getSliceStringValue(task state.Task, labelName string) []string { + for _, lbl := range task.Labels { + if lbl.Key == labelName { + return label.SplitAndTrimString(lbl.Value, ",") + } + } + return nil +} + +func getApplication(task state.Task, apps []state.Task) (state.Task, error) { + for _, app := range apps { + if app.DiscoveryInfo.Name == task.DiscoveryInfo.Name { + return app, nil + } + } + return state.Task{}, fmt.Errorf("unable to get Mesos application from task %s", task.DiscoveryInfo.Name) +} + +func isEnabled(task state.Task, exposedByDefault bool) bool { + return getBoolValue(task, label.TraefikEnable, exposedByDefault) +} diff --git a/provider/mesos/config_test.go b/provider/mesos/config_test.go new file mode 100644 index 000000000..47762dfa4 --- /dev/null +++ b/provider/mesos/config_test.go @@ -0,0 +1,310 @@ +package mesos + +import ( + "reflect" + "strconv" + "testing" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/mesosphere/mesos-dns/records/state" + "github.com/stretchr/testify/assert" +) + +// FIXME fill this test!! +func TestBuildConfiguration(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 := &Provider{ + Domain: "docker.localhost", + ExposedByDefault: true, + } + actualConfig := provider.buildConfiguration() + 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) + } + } + } +} + +func TestTaskFilter(t *testing.T) { + testCases := []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(label.TraefikEnable, "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(label.TraefikEnable, "true"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: true, + exposedByDefault: false, + }, + { + mesosTask: task( + statuses( + status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels(label.TraefikEnable, "true"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: true, + exposedByDefault: true, + }, + { + mesosTask: task( + statuses( + status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels(label.TraefikEnable, "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(label.TraefikEnable, "true", + label.TraefikPortIndex, "1", + label.TraefikPort, "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(label.TraefikEnable, "true", + label.TraefikPortIndex, "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(label.TraefikEnable, "true"), + discovery(setDiscoveryPorts("TCP", 80, "WEB HTTP", "TCP", 443, "WEB HTTPS")), + ), + expected: true, // Default to first index + exposedByDefault: true, + }, + { + mesosTask: task( + statuses( + status( + setState("TASK_RUNNING"), + setHealthy(true))), + setLabels(label.TraefikEnable, "true", + label.TraefikPortIndex, "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(label.TraefikEnable, "true", + label.TraefikPortIndex, "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(label.TraefikEnable, "true", + label.TraefikPort, "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(label.TraefikEnable, "true", + label.TraefikPort, "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(label.TraefikEnable, "true", + label.TraefikPort, "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(label.TraefikEnable, "true", + label.TraefikPort, "80"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: true, // No healthCheck + exposedByDefault: true, + }, { + mesosTask: task( + statuses( + status( + setState("TASK_RUNNING"), + setHealthy(false))), + setLabels(label.TraefikEnable, "true", + label.TraefikPort, "80"), + discovery(setDiscoveryPort("TCP", 80, "WEB")), + ), + expected: false, // HealthCheck at false + exposedByDefault: true, + }, + } + + for index, test := range testCases { + t.Run(strconv.Itoa(index), func(t *testing.T) { + t.Parallel() + + actual := taskFilter(test.mesosTask, test.exposedByDefault) + if actual != test.expected { + t.Logf("Statuses : %v", test.mesosTask.Statuses) + t.Logf("Label : %v", test.mesosTask.Labels) + t.Logf("DiscoveryInfo : %v", test.mesosTask.DiscoveryInfo) + t.Fatalf("Expected %v, got %v", test.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 taskState = state.State{ + Slaves: []state.Slave{slave}, + Frameworks: []state.Framework{framework}, + } + + var p = taskRecords(taskState) + if len(p) == 0 { + t.Fatal("No 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 TestGetSubDomain(t *testing.T) { + providerGroups := &Provider{GroupsAsSubDomains: true} + providerNoGroups := &Provider{GroupsAsSubDomains: false} + + testCases := []struct { + path string + expected string + provider *Provider + }{ + {"/test", "test", providerNoGroups}, + {"/test", "test", providerGroups}, + {"/a/b/c/d", "d.c.b.a", providerGroups}, + {"/b/a/d/c", "c.d.a.b", providerGroups}, + {"/d/c/b/a", "a.b.c.d", providerGroups}, + {"/c/d/a/b", "b.a.d.c", providerGroups}, + {"/a/b/c/d", "a-b-c-d", providerNoGroups}, + {"/b/a/d/c", "b-a-d-c", providerNoGroups}, + {"/d/c/b/a", "d-c-b-a", providerNoGroups}, + {"/c/d/a/b", "c-d-a-b", providerNoGroups}, + } + + for _, test := range testCases { + test := test + t.Run("", func(t *testing.T) { + t.Parallel() + + actual := test.provider.getSubDomain(test.path) + + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/provider/mesos/mesos.go b/provider/mesos/mesos.go index 46f7d33f7..90ec9c140 100644 --- a/provider/mesos/mesos.go +++ b/provider/mesos/mesos.go @@ -1,14 +1,10 @@ package mesos import ( - "errors" "fmt" - "strconv" "strings" - "text/template" "time" - "github.com/BurntSushi/ty/fun" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/log" @@ -20,8 +16,6 @@ import ( _ "github.com/mesos/mesos-go/detector/zoo" "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" ) @@ -82,7 +76,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s for { select { case <-reload.C: - configuration := p.loadMesosConfig() + configuration := p.buildConfiguration() if configuration != nil { configurationChan <- types.ConfigMessage{ ProviderName: "mesos", @@ -98,7 +92,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s } log.Debugf("new masters detected: %v", masters) p.Masters = masters - configuration := p.loadMesosConfig() + configuration := p.buildConfiguration() if configuration != nil { configurationChan <- types.ConfigMessage{ ProviderName: "mesos", @@ -121,272 +115,6 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s return nil } -func (p *Provider) loadMesosConfig() *types.Configuration { - var mesosFuncMap = template.FuncMap{ - "getBackend": p.getBackend, - "getPort": p.getPort, - "getHost": p.getHost, - "getWeight": p.getWeight, - "getDomain": p.getDomain, - "getProtocol": p.getProtocol, - "getPassHostHeader": p.getPassHostHeader, - "getPriority": p.getPriority, - "getEntryPoints": p.getEntryPoints, - "getFrontendRule": p.getFrontendRule, - "getFrontendBackend": p.getFrontendBackend, - "getID": p.getID, - "getFrontEndName": p.getFrontEndName, - } - - t := records.NewRecordGenerator(time.Duration(p.StateTimeoutSecond) * time.Second) - sj, err := t.FindMaster(p.Masters...) - if err != nil { - log.Errorf("Failed to create a client for Mesos, error: %s", err) - return nil - } - tasks := p.taskRecords(sj) - - //filter tasks - filteredTasks := fun.Filter(func(task state.Task) bool { - return mesosTaskFilter(task, p.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, - p.Domain, - } - - configuration, err := p.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, types.LabelPortIndex) - portValueLabel := labels(task, types.LabelPort) - if portIndexLabel != "" && portValueLabel != "" { - log.Debugf("Filtering Mesos task %s specifying both traefik.portIndex and traefik.port labels", task.Name) - return false - } - if portIndexLabel != "" { - index, err := strconv.Atoi(labels(task, types.LabelPortIndex)) - 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, types.LabelPort)) - 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, types.LabelEnable) != "false" || labels(task, types.LabelEnable) == "true" -} - -func (p *Provider) 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 (p *Provider) 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 := p.getLabel(application, types.LabelPortIndex); err == nil { - if index, err := strconv.Atoi(portIndexLabel); err == nil { - return strconv.Itoa(task.DiscoveryInfo.Ports.DiscoveryPorts[index].Number) - } - } - if portValueLabel, err := p.getLabel(application, types.LabelPort); err == nil { - return portValueLabel - } - - for _, port := range task.DiscoveryInfo.Ports.DiscoveryPorts { - return strconv.Itoa(port.Number) - } - return "" -} - -func (p *Provider) 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 := p.getLabel(application, types.LabelWeight); err == nil { - return label - } - return "0" -} - -func (p *Provider) getDomain(task state.Task) string { - if label, err := p.getLabel(task, types.LabelDomain); err == nil { - return label - } - return p.Domain -} - -func (p *Provider) 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 := p.getLabel(application, types.LabelProtocol); err == nil { - return label - } - return "http" -} - -func (p *Provider) getPassHostHeader(task state.Task) string { - if passHostHeader, err := p.getLabel(task, types.LabelFrontendPassHostHeader); err == nil { - return passHostHeader - } - return "false" -} - -func (p *Provider) getPriority(task state.Task) string { - if priority, err := p.getLabel(task, types.LabelFrontendPriority); err == nil { - return priority - } - return "0" -} - -func (p *Provider) getEntryPoints(task state.Task) []string { - if entryPoints, err := p.getLabel(task, types.LabelFrontendEntryPoints); 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 (p *Provider) getFrontendRule(task state.Task) string { - if label, err := p.getLabel(task, types.LabelFrontendRule); err == nil { - return label - } - return "Host:" + strings.ToLower(strings.Replace(p.getSubDomain(task.DiscoveryInfo.Name), "_", "-", -1)) + "." + p.Domain -} - -func (p *Provider) 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 p.getFrontendBackend(application) -} - -func (p *Provider) getFrontendBackend(task state.Task) string { - if label, err := p.getLabel(task, types.LabelBackend); err == nil { - return label - } - return "-" + cleanupSpecialChars(task.DiscoveryInfo.Name) -} - -func (p *Provider) getHost(task state.Task) string { - return task.IP(strings.Split(p.IPSources, ",")...) -} - -func (p *Provider) getID(task state.Task) string { - return cleanupSpecialChars(task.ID) -} - -func (p *Provider) 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 != "" { @@ -401,43 +129,3 @@ func detectMasters(zk string, masters []string) <-chan []string { } return changed } - -func (p *Provider) taskRecords(sj state.State) []state.Task { - var tasks []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" { - tasks = append(tasks, task) - } - } - } - - return tasks -} - -// 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 (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) -} diff --git a/provider/mesos/mesos_helper_test.go b/provider/mesos/mesos_helper_test.go new file mode 100644 index 000000000..c9aac47d5 --- /dev/null +++ b/provider/mesos/mesos_helper_test.go @@ -0,0 +1,113 @@ +package mesos + +import ( + "github.com/containous/traefik/log" + "github.com/mesosphere/mesos-dns/records/state" +) + +// 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.Debugf("Label1.1 : %v", label) + t.Labels = append(t.Labels, label) + log.Debugf("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/provider/mesos/mesos_test.go b/provider/mesos/mesos_test.go deleted file mode 100644 index 95bb8a3e6..000000000 --- a/provider/mesos/mesos_test.go +++ /dev/null @@ -1,384 +0,0 @@ -package mesos - -import ( - "reflect" - "testing" - - "github.com/containous/traefik/log" - "github.com/containous/traefik/types" - "github.com/mesosphere/mesos-dns/records/state" -) - -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(types.LabelEnable, "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(types.LabelEnable, "true"), - discovery(setDiscoveryPort("TCP", 80, "WEB")), - ), - expected: true, - exposedByDefault: false, - }, - { - mesosTask: task(statuses(status( - setState("TASK_RUNNING"), - setHealthy(true))), - setLabels(types.LabelEnable, "true"), - discovery(setDiscoveryPort("TCP", 80, "WEB")), - ), - expected: true, - exposedByDefault: true, - }, - { - mesosTask: task(statuses(status( - setState("TASK_RUNNING"), - setHealthy(true))), - setLabels(types.LabelEnable, "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(types.LabelEnable, "true", - types.LabelPortIndex, "1", - types.LabelPort, "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(types.LabelEnable, "true", - types.LabelPortIndex, "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(types.LabelEnable, "true"), - discovery(setDiscoveryPorts("TCP", 80, "WEB HTTP", "TCP", 443, "WEB HTTPS")), - ), - expected: true, // Default to first index - exposedByDefault: true, - }, - { - mesosTask: task(statuses(status( - setState("TASK_RUNNING"), - setHealthy(true))), - setLabels(types.LabelEnable, "true", - types.LabelPortIndex, "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(types.LabelEnable, "true", - types.LabelPortIndex, "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(types.LabelEnable, "true", - types.LabelPort, "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(types.LabelEnable, "true", - types.LabelPort, "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(types.LabelEnable, "true", - types.LabelPort, "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(types.LabelEnable, "true", - types.LabelPort, "80"), - discovery(setDiscoveryPort("TCP", 80, "WEB")), - ), - expected: true, // No healthCheck - exposedByDefault: true, - }, { - mesosTask: task(statuses(status( - setState("TASK_RUNNING"), - setHealthy(false))), - setLabels(types.LabelEnable, "true", - types.LabelPort, "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 := &Provider{ - 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) { - // FIXME this test is dead? - 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 := &Provider{ - 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) - } - } - } -} - -func TestMesosGetSubDomain(t *testing.T) { - providerGroups := &Provider{GroupsAsSubDomains: true} - providerNoGroups := &Provider{GroupsAsSubDomains: false} - - apps := []struct { - path string - expected string - provider *Provider - }{ - {"/test", "test", providerNoGroups}, - {"/test", "test", providerGroups}, - {"/a/b/c/d", "d.c.b.a", providerGroups}, - {"/b/a/d/c", "c.d.a.b", providerGroups}, - {"/d/c/b/a", "a.b.c.d", providerGroups}, - {"/c/d/a/b", "b.a.d.c", providerGroups}, - {"/a/b/c/d", "a-b-c-d", providerNoGroups}, - {"/b/a/d/c", "b-a-d-c", providerNoGroups}, - {"/d/c/b/a", "d-c-b-a", providerNoGroups}, - {"/c/d/a/b", "c-d-a-b", providerNoGroups}, - } - - for _, a := range apps { - actual := a.provider.getSubDomain(a.path) - - if actual != a.expected { - t.Errorf("expected %q, got %q", a.expected, actual) - } - } -} - -// 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 - } -}