diff --git a/configuration.go b/configuration.go index 717b73eb4..7013455c7 100644 --- a/configuration.go +++ b/configuration.go @@ -51,6 +51,7 @@ type GlobalConfiguration struct { Mesos *provider.Mesos `description:"Enable Mesos backend"` Eureka *provider.Eureka `description:"Enable Eureka backend"` ECS *provider.ECS `description:"Enable ECS backend"` + Rancher *provider.Rancher `description:"Enable Rancher backend"` } // DefaultEntryPoints holds default entry points @@ -400,6 +401,11 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { defaultECS.Cluster = "default" defaultECS.Constraints = types.Constraints{} + //default Rancher + var defaultRancher provider.Rancher + defaultRancher.Watch = true + defaultRancher.ExposedByDefault = true + defaultConfiguration := GlobalConfiguration{ Docker: &defaultDocker, File: &defaultFile, @@ -413,8 +419,13 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { Kubernetes: &defaultKubernetes, Mesos: &defaultMesos, ECS: &defaultECS, + Rancher: &defaultRancher, Retry: &Retry{}, } + + //default Rancher + //@TODO: ADD + return &TraefikConfiguration{ GlobalConfiguration: defaultConfiguration, } diff --git a/glide.lock b/glide.lock index 4539ab00f..f475a02ee 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: a0b0abed2162e490cbe75a6a36ebaaf39e748ee80e419e879e7253679a0bc134 -updated: 2017-02-05T18:09:09.856588042Z +hash: b2ac93355c3f551a75216a800337cee9321f6c9a04a18ab1fa8d8152e89b7595 +updated: 2017-02-05T23:00:24.927243212+01:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 @@ -300,6 +300,8 @@ imports: version: 44d81051d367757e1c7c6a5a86423ece9afcf63c - name: github.com/gorilla/context version: 1ea25387ff6f684839d82767c1733ff4d4d15d0a +- name: github.com/gorilla/websocket + version: c36f2fe5c330f0ac404b616b96c438b8616b1aaf - name: github.com/hashicorp/consul version: fce7d75609a04eeb9d4bf41c8dc592aac18fc97d subpackages: @@ -370,7 +372,7 @@ imports: - name: github.com/mvdan/xurls version: fa08908f19eca8c491d68c6bd8b4b44faea6daf8 - name: github.com/NYTimes/gziphandler - version: f6438dbf4a82c56684964b03956aa727b0d7816b + version: 6710af535839f57c687b62c4c23d649f9545d885 - name: github.com/ogier/pflag version: 45c278ab3607870051a2ea9040bb85fcb8557481 - name: github.com/opencontainers/runc @@ -386,6 +388,8 @@ imports: - ovh - name: github.com/pborman/uuid version: 5007efa264d92316c43112bc573e754bc889b7b1 +- name: github.com/pkg/errors + version: 248dadf4e9068a0b3e79f02ed0a610d935de5302 - name: github.com/pmezard/go-difflib version: d8ed2627bdf02c080bf22230dbb337003b7aba2d subpackages: @@ -414,6 +418,12 @@ imports: version: ab4b0d7ff424c462da486aef27f354cdeb29a319 subpackages: - src/egoscale +- name: github.com/rancher/go-rancher + version: 2c43ff300f3eafcbd7d0b89b10427fc630efdc1e + subpackages: + - client +- name: github.com/rcrowley/go-metrics + version: 1f30fe9094a513ce4c700b9a54458bbb0c96996c - name: github.com/ryanuber/go-glob version: 572520ed46dbddaed19ea3d9541bdd0494163693 - name: github.com/samuel/go-zookeeper @@ -739,8 +749,6 @@ testImports: version: 06479209bdc0d4135911688c18157bd39bd99c22 subpackages: - specs-go -- name: github.com/pkg/errors - version: 01fa4104b9c248c8945d14d9f128454d5b28d595 - name: github.com/vbatts/tar-split version: 6810cedb21b2c3d0b9bb8f9af12ff2dc7a2f14df subpackages: diff --git a/glide.yaml b/glide.yaml index a13917963..a4ec033e0 100644 --- a/glide.yaml +++ b/glide.yaml @@ -135,4 +135,6 @@ import: - package: github.com/gogo/protobuf version: v0.3 subpackages: - - proto \ No newline at end of file + - proto +- package: github.com/rancher/go-rancher + version: 2c43ff300f3eafcbd7d0b89b10427fc630efdc1e diff --git a/provider/rancher.go b/provider/rancher.go new file mode 100644 index 000000000..77072f0a9 --- /dev/null +++ b/provider/rancher.go @@ -0,0 +1,458 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "github.com/BurntSushi/ty/fun" + "github.com/cenk/backoff" + "github.com/containous/traefik/job" + "github.com/containous/traefik/log" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" + rancher "github.com/rancher/go-rancher/client" + "math" + "strconv" + "strings" + "text/template" + "time" +) + +const ( + // RancherDefaultWatchTime is the duration of the interval when polling rancher + RancherDefaultWatchTime = 15 * time.Second +) + +var _ Provider = (*Rancher)(nil) + +// Rancher holds configurations of the Rancher provider. +type Rancher struct { + BaseProvider `mapstructure:",squash"` + Endpoint string `description:"Rancher server HTTP(S) endpoint."` + AccessKey string `description:"Rancher server access key."` + SecretKey string `description:"Rancher server Secret Key."` + ExposedByDefault bool `description:"Expose Services by default"` + Domain string `description:"Default domain used"` +} + +type rancherData struct { + Name string + Labels map[string]string // List of labels set to container or service + Containers []string + Health string +} + +func (r rancherData) String() string { + return fmt.Sprintf("{name:%s, labels:%v, containers: %v, health: %s}", r.Name, r.Labels, r.Containers, r.Health) +} + +// Frontend Labels +func (provider *Rancher) getPassHostHeader(service rancherData) string { + if passHostHeader, err := getServiceLabel(service, "traefik.frontend.passHostHeader"); err == nil { + return passHostHeader + } + return "true" +} + +func (provider *Rancher) getPriority(service rancherData) string { + if priority, err := getServiceLabel(service, "traefik.frontend.priority"); err == nil { + return priority + } + return "0" +} + +func (provider *Rancher) getEntryPoints(service rancherData) []string { + if entryPoints, err := getServiceLabel(service, "traefik.frontend.entryPoints"); err == nil { + return strings.Split(entryPoints, ",") + } + return []string{} +} + +func (provider *Rancher) getFrontendRule(service rancherData) string { + if label, err := getServiceLabel(service, "traefik.frontend.rule"); err == nil { + return label + } + return "Host:" + strings.ToLower(strings.Replace(service.Name, "/", "_", -1)) + "." + provider.Domain +} + +func (provider *Rancher) getFrontendName(service rancherData) string { + // Replace '.' with '-' in quoted keys because of this issue https://github.com/BurntSushi/toml/issues/78 + return normalize(provider.getFrontendRule(service)) +} + +// Backend Labels +func (provider *Rancher) getLoadBalancerMethod(service rancherData) string { + if label, err := getServiceLabel(service, "traefik.backend.loadbalancer.method"); err == nil { + return label + } + return "wrr" +} + +func (provider *Rancher) hasLoadBalancerLabel(service rancherData) bool { + _, errMethod := getServiceLabel(service, "traefik.backend.loadbalancer.method") + _, errSticky := getServiceLabel(service, "traefik.backend.loadbalancer.sticky") + if errMethod != nil && errSticky != nil { + return false + } + return true +} + +func (provider *Rancher) hasCircuitBreakerLabel(service rancherData) bool { + if _, err := getServiceLabel(service, "traefik.backend.circuitbreaker.expression"); err != nil { + return false + } + return true +} + +func (provider *Rancher) getCircuitBreakerExpression(service rancherData) string { + if label, err := getServiceLabel(service, "traefik.backend.circuitbreaker.expression"); err == nil { + return label + } + return "NetworkErrorRatio() > 1" +} + +func (provider *Rancher) getSticky(service rancherData) string { + if _, err := getServiceLabel(service, "traefik.backend.loadbalancer.sticky"); err == nil { + return "true" + } + return "false" +} + +func (provider *Rancher) getBackend(service rancherData) string { + if label, err := getServiceLabel(service, "traefik.backend"); err == nil { + return normalize(label) + } + return normalize(service.Name) +} + +// Generall Application Stuff +func (provider *Rancher) getPort(service rancherData) string { + if label, err := getServiceLabel(service, "traefik.port"); err == nil { + return label + } + return "" +} + +func (provider *Rancher) getProtocol(service rancherData) string { + if label, err := getServiceLabel(service, "traefik.protocol"); err == nil { + return label + } + return "http" +} + +func (provider *Rancher) getWeight(service rancherData) string { + if label, err := getServiceLabel(service, "traefik.weight"); err == nil { + return label + } + return "0" +} + +func (provider *Rancher) getDomain(service rancherData) string { + if label, err := getServiceLabel(service, "traefik.domain"); err == nil { + return label + } + return provider.Domain +} + +func (provider *Rancher) hasMaxConnLabels(service rancherData) bool { + if _, err := getServiceLabel(service, "traefik.backend.maxconn.amount"); err != nil { + return false + } + if _, err := getServiceLabel(service, "traefik.backend.maxconn.extractorfunc"); err != nil { + return false + } + return true +} + +func (provider *Rancher) getMaxConnAmount(service rancherData) int64 { + if label, err := getServiceLabel(service, "traefik.backend.maxconn.amount"); err == nil { + i, errConv := strconv.ParseInt(label, 10, 64) + if errConv != nil { + log.Errorf("Unable to parse traefik.backend.maxconn.amount %s", label) + return math.MaxInt64 + } + return i + } + return math.MaxInt64 +} + +func (provider *Rancher) getMaxConnExtractorFunc(service rancherData) string { + if label, err := getServiceLabel(service, "traefik.backend.maxconn.extractorfunc"); err == nil { + return label + } + return "request.host" +} + +func getServiceLabel(service rancherData, label string) (string, error) { + for key, value := range service.Labels { + if key == label { + return value, nil + } + } + return "", errors.New("Label not found:" + label) +} + +func (provider *Rancher) createClient() (*rancher.RancherClient, error) { + return rancher.NewRancherClient(&rancher.ClientOpts{ + Url: provider.Endpoint, + AccessKey: provider.AccessKey, + SecretKey: provider.SecretKey, + }) +} + +// Provide allows the provider to provide configurations to traefik +// using the given configuration channel. +func (provider *Rancher) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { + + safe.Go(func() { + operation := func() error { + rancherClient, err := provider.createClient() + ctx := context.Background() + var environments = listRancherEnvironments(rancherClient) + var services = listRancherServices(rancherClient) + var container = listRancherContainer(rancherClient) + + var rancherData = parseRancherData(environments, services, container) + + if err != nil { + log.Errorf("Failed to create a client for rancher, error: %s", err) + return err + } + + configuration := provider.loadRancherConfig(rancherData) + configurationChan <- types.ConfigMessage{ + ProviderName: "rancher", + Configuration: configuration, + } + + if provider.Watch { + _, cancel := context.WithCancel(ctx) + ticker := time.NewTicker(RancherDefaultWatchTime) + pool.Go(func(stop chan bool) { + for { + select { + case <-ticker.C: + + log.Debugf("Refreshing new Data from Rancher API") + var environments = listRancherEnvironments(rancherClient) + var services = listRancherServices(rancherClient) + var container = listRancherContainer(rancherClient) + + rancherData := parseRancherData(environments, services, container) + + configuration := provider.loadRancherConfig(rancherData) + if configuration != nil { + configurationChan <- types.ConfigMessage{ + ProviderName: "rancher", + Configuration: configuration, + } + } + case <-stop: + ticker.Stop() + cancel() + return + } + } + }) + } + + return nil + } + notify := func(err error, time time.Duration) { + log.Errorf("Rancher connection error %+v, retrying in %s", err, time) + } + err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify) + if err != nil { + log.Errorf("Cannot connect to Rancher Endpoint %+v", err) + } + }) + + return nil +} + +func listRancherEnvironments(client *rancher.RancherClient) []*rancher.Environment { + + var environmentList = []*rancher.Environment{} + + environments, err := client.Environment.List(nil) + + if err != nil { + log.Errorf("Cannot get Rancher Environments %+v", err) + } + + for k := range environments.Data { + environmentList = append(environmentList, &environments.Data[k]) + } + + return environmentList +} + +func listRancherServices(client *rancher.RancherClient) []*rancher.Service { + + var servicesList = []*rancher.Service{} + + services, err := client.Service.List(nil) + + if err != nil { + log.Errorf("Cannot get Rancher Services %+v", err) + } + + for k := range services.Data { + servicesList = append(servicesList, &services.Data[k]) + } + + return servicesList +} + +func listRancherContainer(client *rancher.RancherClient) []*rancher.Container { + + containerList := []*rancher.Container{} + + container, err := client.Container.List(nil) + + log.Debugf("first container len: %i", len(container.Data)) + + if err != nil { + log.Errorf("Cannot get Rancher Services %+v", err) + } + + valid := true + + for valid { + for k := range container.Data { + containerList = append(containerList, &container.Data[k]) + } + + container, err = container.Next() + + if err != nil { + break + } + + if container == nil || len(container.Data) == 0 { + valid = false + } + } + + return containerList +} + +func parseRancherData(environments []*rancher.Environment, services []*rancher.Service, containers []*rancher.Container) []rancherData { + var rancherDataList []rancherData + + for _, environment := range environments { + + for _, service := range services { + if service.EnvironmentId != environment.Id { + continue + } + + rancherData := rancherData{ + Name: environment.Name + "/" + service.Name, + Health: service.HealthState, + Labels: make(map[string]string), + Containers: []string{}, + } + + for key, value := range service.LaunchConfig.Labels { + rancherData.Labels[key] = value.(string) + } + + for _, container := range containers { + for key, value := range container.Labels { + + if key == "io.rancher.stack_service.name" && value == rancherData.Name { + rancherData.Containers = append(rancherData.Containers, container.PrimaryIpAddress) + } + } + } + rancherDataList = append(rancherDataList, rancherData) + } + } + + return rancherDataList +} + +func (provider *Rancher) loadRancherConfig(services []rancherData) *types.Configuration { + + var RancherFuncMap = template.FuncMap{ + "getPort": provider.getPort, + "getBackend": provider.getBackend, + "getWeight": provider.getWeight, + "getDomain": provider.getDomain, + "getProtocol": provider.getProtocol, + "getPassHostHeader": provider.getPassHostHeader, + "getPriority": provider.getPriority, + "getEntryPoints": provider.getEntryPoints, + "getFrontendRule": provider.getFrontendRule, + "hasCircuitBreakerLabel": provider.hasCircuitBreakerLabel, + "getCircuitBreakerExpression": provider.getCircuitBreakerExpression, + "hasLoadBalancerLabel": provider.hasLoadBalancerLabel, + "getLoadBalancerMethod": provider.getLoadBalancerMethod, + "hasMaxConnLabels": provider.hasMaxConnLabels, + "getMaxConnAmount": provider.getMaxConnAmount, + "getMaxConnExtractorFunc": provider.getMaxConnExtractorFunc, + "getSticky": provider.getSticky, + } + + // filter services + filteredServices := fun.Filter(func(service rancherData) bool { + return provider.serviceFilter(service) + }, services).([]rancherData) + + frontends := map[string]rancherData{} + backends := map[string]rancherData{} + + for _, service := range filteredServices { + frontendName := provider.getFrontendName(service) + frontends[frontendName] = service + backendName := provider.getBackend(service) + backends[backendName] = service + } + + templateObjects := struct { + Frontends map[string]rancherData + Backends map[string]rancherData + Domain string + }{ + frontends, + backends, + provider.Domain, + } + + configuration, err := provider.getConfiguration("templates/rancher.tmpl", RancherFuncMap, templateObjects) + if err != nil { + log.Error(err) + } + return configuration + +} + +func (provider *Rancher) serviceFilter(service rancherData) bool { + + if service.Labels["traefik.port"] == "" { + log.Debugf("Filtering service %s without traefik.port label", service.Name) + return false + } + + if !isServiceEnabled(service, provider.ExposedByDefault) { + log.Debugf("Filtering disabled service %s", service.Name) + return false + } + + if service.Health != "" && service.Health != "healthy" { + log.Debugf("Filtering unhealthy or starting service %s", service.Name) + return false + } + + return true +} + +func isServiceEnabled(service rancherData, exposedByDefault bool) bool { + + if service.Labels["traefik.enable"] != "" { + var v = service.Labels["traefik.enable"] + return exposedByDefault && v != "false" || v == "true" + } + return exposedByDefault +} diff --git a/provider/rancher_test.go b/provider/rancher_test.go new file mode 100644 index 000000000..d0b920f2d --- /dev/null +++ b/provider/rancher_test.go @@ -0,0 +1,448 @@ +package provider + +import ( + "github.com/containous/traefik/types" + "reflect" + "strings" + "testing" +) + +func TestRancherGetFrontendName(t *testing.T) { + provider := &Rancher{ + Domain: "rancher.localhost", + } + + services := []struct { + service rancherData + expected string + }{ + { + service: rancherData{ + Name: "foo", + }, + expected: "Host-foo-rancher-localhost", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.frontend.rule": "Headers:User-Agent,bat/0.1.0", + }, + }, + + expected: "Headers-User-Agent-bat-0-1-0", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.frontend.rule": "Host:foo.bar", + }, + }, + + expected: "Host-foo-bar", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.frontend.rule": "Path:/test", + }, + }, + + expected: "Path-test", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.frontend.rule": "PathPrefix:/test2", + }, + }, + + expected: "PathPrefix-test2", + }, + } + + for _, e := range services { + actual := provider.getFrontendName(e.service) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestRancherGetFrontendRule(t *testing.T) { + provider := &Rancher{ + Domain: "rancher.localhost", + } + + services := []struct { + service rancherData + expected string + }{ + { + service: rancherData{ + Name: "foo", + }, + expected: "Host:foo.rancher.localhost", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.frontend.rule": "Host:foo.bar.com", + }, + }, + + expected: "Host:foo.bar.com", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.frontend.rule": "Path:/test", + }, + }, + + expected: "Path:/test", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.frontend.rule": "PathPrefix:/test2", + }, + }, + + expected: "PathPrefix:/test2", + }, + } + + for _, e := range services { + actual := provider.getFrontendRule(e.service) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestRancherGetBackend(t *testing.T) { + provider := &Rancher{ + Domain: "rancher.localhost", + } + + services := []struct { + service rancherData + expected string + }{ + { + service: rancherData{ + Name: "test-service", + }, + expected: "test-service", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.backend": "foobar", + }, + }, + + expected: "foobar", + }, + } + + for _, e := range services { + actual := provider.getBackend(e.service) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestRancherGetWeight(t *testing.T) { + provider := &Rancher{ + Domain: "rancher.localhost", + } + + services := []struct { + service rancherData + expected string + }{ + { + service: rancherData{ + Name: "test-service", + }, + expected: "0", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.weight": "5", + }, + }, + + expected: "5", + }, + } + + for _, e := range services { + actual := provider.getWeight(e.service) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestRancherGetPort(t *testing.T) { + provider := &Rancher{ + Domain: "rancher.localhost", + } + + services := []struct { + service rancherData + expected string + }{ + { + service: rancherData{ + Name: "test-service", + }, + expected: "", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.port": "1337", + }, + }, + + expected: "1337", + }, + } + + for _, e := range services { + actual := provider.getPort(e.service) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestRancherGetDomain(t *testing.T) { + provider := &Rancher{ + Domain: "rancher.localhost", + } + + services := []struct { + service rancherData + expected string + }{ + { + service: rancherData{ + Name: "test-service", + }, + expected: "rancher.localhost", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.domain": "foo.bar", + }, + }, + + expected: "foo.bar", + }, + } + + for _, e := range services { + actual := provider.getDomain(e.service) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestRancherGetProtocol(t *testing.T) { + provider := &Rancher{ + Domain: "rancher.localhost", + } + + services := []struct { + service rancherData + expected string + }{ + { + service: rancherData{ + Name: "test-service", + }, + expected: "http", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.protocol": "https", + }, + }, + + expected: "https", + }, + } + + for _, e := range services { + actual := provider.getProtocol(e.service) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestRancherGetPassHostHeader(t *testing.T) { + provider := &Rancher{ + Domain: "rancher.localhost", + } + + services := []struct { + service rancherData + expected string + }{ + { + service: rancherData{ + Name: "test-service", + }, + expected: "true", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "traefik.frontend.passHostHeader": "false", + }, + }, + + expected: "false", + }, + } + + for _, e := range services { + actual := provider.getPassHostHeader(e.service) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + +func TestRancherGetLabel(t *testing.T) { + services := []struct { + service rancherData + expected string + }{ + { + service: rancherData{ + Name: "test-service", + }, + expected: "Label not found", + }, + { + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + "foo": "bar", + }, + }, + + expected: "", + }, + } + + for _, e := range services { + label, err := getServiceLabel(e.service, "foo") + if e.expected != "" { + if err == nil || !strings.Contains(err.Error(), e.expected) { + t.Fatalf("expected an error with %q, got %v", e.expected, err) + } + } else { + if label != "bar" { + t.Fatalf("expected label 'bar', got %s", label) + } + } + } +} + +func TestRancherLoadRancherConfig(t *testing.T) { + cases := []struct { + services []rancherData + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + services: []rancherData{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + services: []rancherData{ + { + Name: "test-service", + Labels: map[string]string{ + "traefik.port": "80", + }, + Health: "healthy", + Containers: []string{"127.0.0.1"}, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test-service-rancher-localhost": { + Backend: "backend-test-service", + PassHostHeader: true, + EntryPoints: []string{}, + Priority: 0, + + Routes: map[string]types.Route{ + "route-frontend-Host-test-service-rancher-localhost": { + Rule: "Host:test-service.rancher.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test-service": { + Servers: map[string]types.Server{ + "server-0": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + } + + provider := &Rancher{ + Domain: "rancher.localhost", + ExposedByDefault: true, + } + + for _, c := range cases { + var rancherDataList []rancherData + for _, service := range c.services { + rancherDataList = append(rancherDataList, service) + } + + actualConfig := provider.loadRancherConfig(rancherDataList) + + // 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) + } + } +} diff --git a/server.go b/server.go index e9cd4decb..a2d3a406f 100644 --- a/server.go +++ b/server.go @@ -377,6 +377,9 @@ func (server *Server) configureProviders() { if server.globalConfiguration.ECS != nil { server.providers = append(server.providers, server.globalConfiguration.ECS) } + if server.globalConfiguration.Rancher != nil { + server.providers = append(server.providers, server.globalConfiguration.Rancher) + } } func (server *Server) startProviders() { diff --git a/templates/rancher.tmpl b/templates/rancher.tmpl new file mode 100644 index 000000000..15fb4571f --- /dev/null +++ b/templates/rancher.tmpl @@ -0,0 +1,38 @@ +{{$backendServers := .Backends}} +[backends]{{range $backendName, $backend := .Backends}} + {{if hasCircuitBreakerLabel $backend}} + [backends.backend-{{$backendName}}.circuitbreaker] + expression = "{{getCircuitBreakerExpression $backend}}" + {{end}} + + {{if hasLoadBalancerLabel $backend}} + [backends.backend-{{$backendName}}.loadbalancer] + method = "{{getLoadBalancerMethod $backend}}" + sticky = {{getSticky $backend}} + {{end}} + + {{if hasMaxConnLabels $backend}} + [backends.backend-{{$backendName}}.maxconn] + amount = {{getMaxConnAmount $backend}} + extractorfunc = "{{getMaxConnExtractorFunc $backend}}" + {{end}} + + {{range $index, $ip := $backend.Containers}} + [backends.backend-{{$backendName}}.servers.server-{{$index}}] + url = "{{getProtocol $backend}}://{{$ip}}:{{getPort $backend}}" + weight = {{getWeight $backend}} + {{end}} + +{{end}} + +[frontends]{{range $frontendName, $service := .Frontends}} + [frontends."frontend-{{$frontendName}}"] + backend = "backend-{{getBackend $service}}" + passHostHeader = {{getPassHostHeader $service}} + priority = {{getPriority $service}} + entryPoints = [{{range getEntryPoints $service}} + "{{.}}", + {{end}}] + [frontends."frontend-{{$frontendName}}".routes."route-frontend-{{$frontendName}}"] + rule = "{{getFrontendRule $service}}" +{{end}}