diff --git a/configuration.go b/configuration.go index 717b73eb4..98aa636f0 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 @@ -415,6 +416,10 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { ECS: &defaultECS, 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..de38d7fd0 --- /dev/null +++ b/provider/rancher.go @@ -0,0 +1,471 @@ +package provider + +import ( + rancher "github.com/rancher/go-rancher/client" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" + "github.com/containous/traefik/log" + "github.com/cenk/backoff" + "github.com/containous/traefik/job" + "time" + "github.com/BurntSushi/ty/fun" + //"context" + "errors" + "strings" + "strconv" + "math" + "fmt" + "text/template" +) + +var _ Provider = (*Rancher)(nil) + +// Rancher holds configurations of the Docker 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 "" +} + +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" +} + + +// Container Stuff +func (provider *Rancher) getIPAddress(container *rancher.Container) string { + ipAdress := container.PrimaryIpAddress; + + if ipAdress != ""{ + return ipAdress + } + return "" +} + + + +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, + }) +} + +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) + + fmt.Printf("Rancher Data #2 %s", &rancherData) + + if err != nil { + log.Errorf("Failed to create a client for docker, error: %s", err) + return err + } + + configuration := provider.loadRancherConfig(rancherData) + configurationChan <- types.ConfigMessage{ + ProviderName: "rancher", + Configuration: configuration, + } + + 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, environment := range environments.Data { + log.Debugf("Adding environment with id %s", environment.Id) + environmentList = append(environmentList, &environments.Data[k]) + } + + return environmentList +} + +/* +"io.rancher.stack.name" + */ +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, service := range services.Data { + log.Debugf("Adding service with id %s", service.Id) + servicesList = append(servicesList, &services.Data[k]) + } + + return servicesList +} + +func listRancherContainer(client *rancher.RancherClient)([]*rancher.Container){ + + var containerList = []*rancher.Container{} + + container, err := client.Container.List(nil) + + if err != nil { + log.Errorf("Cannot get Rancher Services %+v", err) + } + + for k, singleContainer := range container.Data { + log.Debugf("Adding container with id %s", singleContainer.Id) + containerList = append(containerList, &container.Data[k]) + } + + return containerList +} + +func parseRancherData(environments []*rancher.Environment, services []*rancher.Service, containers []*rancher.Container) []rancherData { + + log.Debugf("Starting to parse Rancher Data") + + var rancherDataList []rancherData + + for _, environment := range environments { + + log.Debugf("Iterating trough environment %s", environment.Name) + + for _, service := range services { + + log.Debugf("Iterating trough service %s with id %s for environment %s", service.Name, service.AccountId, environment.Id) + + if service.EnvironmentId != environment.Id { + log.Debugf("NO MATCH") + 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{ + "getIPAddress": provider.getIPAddress, + "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 + } + + fmt.Printf("Frontends %v", frontends) + fmt.Printf("Backends %v", backends) + + 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 + } + + /* + constraintTags := strings.Split(container.Labels["traefik.tags"], ",") + if ok, failingConstraint := provider.MatchConstraints(constraintTags); !ok { + if failingConstraint != nil { + log.Debugf("Container %v pruned by '%v' constraint", container.Name, failingConstraint.String()) + } + return false + } + */ + + if service.Health != "" && service.Health != "healthy" { + log.Debugf("Filtering unhealthy or starting service %s", service.Name) + return false + } + + log.Debugf("Service %s is enabled!", service.Name) + + return true +} + +func (provider *Rancher) containerFilter(container *rancher.Container, instanceIds []string) bool { + + //log.Debugf("Filtering Containers for InstanceIds %v ", instanceIds) + for _, instanceId := range instanceIds { + + //log.Debugf("Looking for instanceId %s on on container %s", instanceId, container.Id) + if container.Id == instanceId { + //log.Debugf("Found container with id %s", instanceId) + return true + } + } + + return false +} + +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 false +} 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}}