diff --git a/README.md b/README.md index 74a832141..7bf30bf30 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), [Kubernetes](http://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Mesos](https://github.com/apache/mesos), [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), [Kubernetes](http://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Mesos](https://github.com/apache/mesos), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), [Eureka](https://github.com/Netflix/eureka), Rest API, file...) to manage its configuration automatically and dynamically. ## Overview diff --git a/configuration.go b/configuration.go index 4870b5b26..19130349a 100644 --- a/configuration.go +++ b/configuration.go @@ -48,6 +48,7 @@ type GlobalConfiguration struct { Boltdb *provider.BoltDb `description:"Enable Boltdb backend"` Kubernetes *provider.Kubernetes `description:"Enable Kubernetes backend"` Mesos *provider.Mesos `description:"Enable Mesos backend"` + Eureka *provider.Eureka `description:"Enable Eureka backend"` } // DefaultEntryPoints holds default entry points diff --git a/docs/toml.md b/docs/toml.md index 9cd894763..4ccdbb2a8 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -1226,4 +1226,40 @@ prefix = "/traefik" # filename = "boltdb.tmpl" ``` +## Eureka backend + +Træfɪk can be configured to use Eureka as a backend configuration: + + +```toml +################################################################ +# Eureka configuration backend +################################################################ + +# Enable Eureka configuration backend +# +# Optional +# +[eureka] + +# Eureka server endpoint. +# endpoint := "http://my.eureka.server/eureka" +# +# Required +# +endpoint = "http://my.eureka.server/eureka" + +# Override default configuration time between refresh +# +# Optional +# default 30s +delay = "1m" + +# Override default configuration template. For advanced users :) +# +# Optional +# +# filename = "eureka.tmpl" +``` + Please refer to the [Key Value storage structure](/user-guide/kv-config/#key-value-storage-structure) section to get documentation on traefik KV structure. diff --git a/glide.lock b/glide.lock index e860a9b02..e5daa50c8 100644 --- a/glide.lock +++ b/glide.lock @@ -1,8 +1,14 @@ -hash: 5a4ccfe1c0ee6b0c67a40ceb07a8510d61646e1551599331b626ccf1341f4048 -updated: 2016-11-15T19:22:10.412299841Z +hash: aae2fd761966717bcc30d8c03590cc28df8af49b36cc1d87689512cdfb44475e +updated: 2016-11-16T21:37:51.673291661+01:00 imports: - name: github.com/abbot/go-http-auth version: cb4372376e1e00e9f6ab9ec142e029302c9e7140 +- name: github.com/ArthurHlt/go-eureka-client + version: ba361cd0f9f571b4e871421423d2f02f5689c3d2 + subpackages: + - eureka +- name: github.com/ArthurHlt/gominlog + version: 068c01ce147ad68fca25ef3fa29ae5395ae273ab - name: github.com/boltdb/bolt version: f4c032d907f61f08dba2d719c58f108a1abb8e81 - name: github.com/BurntSushi/toml @@ -14,9 +20,9 @@ imports: - name: github.com/cenk/backoff version: 8edc80b07f38c27352fb186d971c628a6c32552b - name: github.com/codahale/hdrhistogram - version: 9208b142303c12d8899bae836fd524ac9338b4fd + version: f8ad88b59a584afeee9d334eff879b104439117b - name: github.com/codegangsta/cli - version: bf4a526f48af7badd25d2cb02d587e1b01be3b50 + version: 1efa31f08b9333f1bd4882d61f9d668a70cd902e - name: github.com/codegangsta/negroni version: 3f7ce7b928e14ff890b067e5bbbc80af73690a9c - name: github.com/containous/flaeg @@ -26,13 +32,17 @@ imports: - name: github.com/containous/staert version: 92329254783dc01174f03302d51d7cf2c9ff84cf - name: github.com/coreos/etcd - version: c400d05d0aa73e21e431c16145e558d624098018 + version: 1c9e0a0e33051fed6c05c141e6fcbfe5c7f2a899 subpackages: - - Godeps/_workspace/src/github.com/ugorji/go/codec - - Godeps/_workspace/src/golang.org/x/net/context - client - pkg/pathutil - pkg/types +- name: github.com/davecgh/go-spew + version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 + subpackages: + - spew +- name: github.com/daviddengcn/go-colortext + version: 3b18c8575a432453d41fdafb340099fff5bba2f7 - name: github.com/docker/distribution version: 99cb7c0946d2f5a38015443e515dc916295064d7 subpackages: @@ -155,9 +165,9 @@ imports: - name: github.com/gambol99/go-marathon version: a558128c87724cd7430060ef5aedf39f83937f55 - name: github.com/go-check/check - version: 11d3bc7aa68e238947792f30573146a3231fc0f1 + version: 4f90aeace3a26ad7021961c297b22c42160c7b25 - name: github.com/gogo/protobuf - version: 43ab7f0ec7b6d072e0368bd537ffefe74ed30198 + version: 99cb9b23110011cc45571c901ecae6f6f5e65cd3 subpackages: - proto - name: github.com/golang/glog @@ -167,7 +177,7 @@ imports: subpackages: - query - name: github.com/gorilla/context - version: 14f550f51af52180c2eefed15e5fd18d63c0a64a + version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 - name: github.com/hashicorp/consul version: d8e2fb7dd594163e25a89bc52c1a4613f5c5bfb8 subpackages: @@ -175,7 +185,7 @@ imports: - name: github.com/hashicorp/go-cleanhttp version: ad28ea4487f05916463e2423a55166280e8254b5 - name: github.com/hashicorp/serf - version: 598c54895cc5a7b1a24a398d635e8c0ea0959870 + version: b03bf85930b2349eb04b97c8fac437495296e3e7 subpackages: - coordinate - name: github.com/jarcoal/httpmock @@ -227,11 +237,15 @@ imports: - name: github.com/ogier/pflag version: 45c278ab3607870051a2ea9040bb85fcb8557481 - name: github.com/opencontainers/runc - version: ba1568de399395774ad84c2ace65937814c542ed + version: 02f8fa7863dd3f82909a73e2061897828460d52f subpackages: - libcontainer/user - name: github.com/parnurzeal/gorequest version: e30af16d4e485943aab0b0885ad6bdbb8c0d3dc7 +- name: github.com/pmezard/go-difflib + version: d8ed2627bdf02c080bf22230dbb337003b7aba2d + subpackages: + - difflib - name: github.com/ryanuber/go-glob version: 572520ed46dbddaed19ea3d9541bdd0494163693 - name: github.com/samuel/go-zookeeper @@ -247,7 +261,7 @@ imports: - name: github.com/stretchr/objx version: cbeaeb16a013161a98496fad62933b1d21786672 - name: github.com/stretchr/testify - version: b8dc1cecf15bdaf1988d9e87aa7cd98d899a06d6 + version: 976c720a22c8eb4eb6a0b4348ad85ad12491a506 subpackages: - assert - mock @@ -255,6 +269,10 @@ imports: version: 152b5d051953fdb6e45f14b6826962aadc032324 - name: github.com/tv42/zbase32 version: 03389da7e0bf9844767f82690f4d68fc097a1306 +- name: github.com/ugorji/go + version: b94837a2404ab90efe9289e77a70694c355739cb + subpackages: + - codec - name: github.com/unrolled/render version: 526faf80cd4b305bb8134abea8d20d5ced74faa6 - name: github.com/vdemeester/docker-events @@ -278,7 +296,7 @@ imports: - name: github.com/vulcand/route version: cb89d787ddbb1c5849a7ac9f79004c1fd12a4a32 - name: github.com/vulcand/vulcand - version: 42492a3a85e294bdbdd1bcabb8c12769a81ea284 + version: bed092e10989250b48bdb6aa3b0557b207f05c80 subpackages: - conntracker - plugin @@ -289,26 +307,26 @@ imports: subpackages: - acme - name: golang.org/x/crypto - version: 4ed45ec682102c643324fae5dff8dab085b6c300 + version: d81fdb778bf2c40a91b24519d60cdc5767318829 subpackages: - bcrypt - blowfish - ocsp - name: golang.org/x/net - version: 6460565bec1e8891e29ff478184c71b9e443ac36 + version: b400c2eff1badec7022a8c8f5bea058b6315eed7 subpackages: - context - proxy - publicsuffix - name: golang.org/x/sys - version: eb2c74142fd19a79b3f237334c7384d5167b1b46 + version: 62bee037599929a6e9146f29d10dd5208c43507d subpackages: - unix - windows - name: gopkg.in/fsnotify.v1 version: 944cff21b3baf3ced9a880365682152ba577d348 - name: gopkg.in/mgo.v2 - version: 29cc868a5ca65f401ff318143f9408d02f4799cc + version: 22287bab4379e1fbf6002fb4eb769888f3fb224c subpackages: - bson - name: gopkg.in/square/go-jose.v1 @@ -326,7 +344,7 @@ testImports: - name: github.com/flynn/go-shlex version: 3f9db97f856818214da2e1057f8ad84803971cff - name: github.com/gorilla/mux - version: e444e69cbd2e2e3e0749a2f3c717cec491552bbf + version: 9fa818a44c2bf1396a17f9d5a3c0f6dd39d2ff8e - name: github.com/libkermit/docker-check version: cbe0ef03b3d23070eac4d00ba8828f2cc7f7e5a3 - name: github.com/spf13/pflag diff --git a/glide.yaml b/glide.yaml index 718012c2b..33ae718fc 100644 --- a/glide.yaml +++ b/glide.yaml @@ -99,3 +99,6 @@ import: - package: github.com/docker/leadership - package: github.com/satori/go.uuid version: ^1.1.0 +- package: github.com/ArthurHlt/go-eureka-client + subpackages: + - eureka diff --git a/provider/eureka.go b/provider/eureka.go new file mode 100644 index 000000000..7ec2a4bfd --- /dev/null +++ b/provider/eureka.go @@ -0,0 +1,145 @@ +package provider + +import ( + "github.com/ArthurHlt/go-eureka-client/eureka" + log "github.com/Sirupsen/logrus" + "github.com/cenk/backoff" + "github.com/containous/traefik/job" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" + "io/ioutil" + "strconv" + "strings" + "text/template" + "time" +) + +// Eureka holds configuration of the Eureka provider. +type Eureka struct { + BaseProvider `mapstructure:",squash"` + Endpoint string + Delay string +} + +// Provide allows the provider to provide configurations to traefik +// using the given configuration channel. +func (provider *Eureka) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, _ []types.Constraint) error { + + operation := func() error { + configuration, err := provider.buildConfiguration() + if err != nil { + log.Errorf("Failed to build configuration for Eureka, error: %s", err) + return err + } + + configurationChan <- types.ConfigMessage{ + ProviderName: "eureka", + Configuration: configuration, + } + + var delay time.Duration + if len(provider.Delay) > 0 { + var err error + delay, err = time.ParseDuration(provider.Delay) + if err != nil { + log.Errorf("Failed to parse delay for Eureka, error: %s", err) + return err + } + } else { + delay = time.Second * 30 + } + + ticker := time.NewTicker(delay) + go func() { + for t := range ticker.C { + + log.Debug("Refreshing Eureka " + t.String()) + + configuration, err := provider.buildConfiguration() + if err != nil { + log.Errorf("Failed to refresh Eureka configuration, error: %s", err) + return + } + + configurationChan <- types.ConfigMessage{ + ProviderName: "eureka", + Configuration: configuration, + } + } + }() + return nil + } + + notify := func(err error, time time.Duration) { + log.Errorf("Eureka 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 Eureka server %+v", err) + return err + } + return nil +} + +// Build the configuration from Eureka server +func (provider *Eureka) buildConfiguration() (*types.Configuration, error) { + var EurekaFuncMap = template.FuncMap{ + "replace": replace, + "tolower": strings.ToLower, + "getPort": provider.getPort, + "getProtocol": provider.getProtocol, + "getWeight": provider.getWeight, + "getInstanceID": provider.getInstanceID, + } + + eureka.GetLogger().SetOutput(ioutil.Discard) + + client := eureka.NewClient([]string{ + provider.Endpoint, + }) + + applications, err := client.GetApplications() + if err != nil { + return nil, err + } + + templateObjects := struct { + Applications []eureka.Application + }{ + applications.Applications, + } + + configuration, err := provider.getConfiguration("templates/eureka.tmpl", EurekaFuncMap, templateObjects) + if err != nil { + log.Error(err) + } + return configuration, nil +} + +func (provider *Eureka) getPort(instance eureka.InstanceInfo) string { + if instance.SecurePort.Enabled { + return strconv.Itoa(instance.SecurePort.Port) + } + return strconv.Itoa(instance.Port.Port) +} + +func (provider *Eureka) getProtocol(instance eureka.InstanceInfo) string { + if instance.SecurePort.Enabled { + return "https" + } + return "http" +} + +func (provider *Eureka) getWeight(instance eureka.InstanceInfo) string { + if val, ok := instance.Metadata.Map["traefik.weight"]; ok { + return val + } + return "0" +} + +func (provider *Eureka) getInstanceID(instance eureka.InstanceInfo) string { + if val, ok := instance.Metadata.Map["traefik.backend.id"]; ok { + return val + } + return strings.Replace(instance.IpAddr, ".", "-", -1) + "-" + provider.getPort(instance) +} diff --git a/provider/eureka_test.go b/provider/eureka_test.go new file mode 100644 index 000000000..407c997af --- /dev/null +++ b/provider/eureka_test.go @@ -0,0 +1,170 @@ +package provider + +import ( + "github.com/ArthurHlt/go-eureka-client/eureka" + "testing" +) + +func TestEurekaGetPort(t *testing.T) { + cases := []struct { + expectedPort string + instanceInfo eureka.InstanceInfo + }{ + { + expectedPort: "80", + instanceInfo: eureka.InstanceInfo{ + SecurePort: &eureka.Port{ + Port: 443, Enabled: false, + }, + Port: &eureka.Port{ + Port: 80, Enabled: true, + }, + }, + }, + { + expectedPort: "443", + instanceInfo: eureka.InstanceInfo{ + SecurePort: &eureka.Port{ + Port: 443, Enabled: true, + }, + Port: &eureka.Port{ + Port: 80, Enabled: false, + }, + }, + }, + } + + eurekaProvider := &Eureka{} + for _, c := range cases { + port := eurekaProvider.getPort(c.instanceInfo) + if port != c.expectedPort { + t.Fatalf("Should have been %s, got %s", c.expectedPort, port) + } + } +} + +func TestEurekaGetProtocol(t *testing.T) { + cases := []struct { + expectedProtocol string + instanceInfo eureka.InstanceInfo + }{ + { + expectedProtocol: "http", + instanceInfo: eureka.InstanceInfo{ + SecurePort: &eureka.Port{ + Port: 443, Enabled: false, + }, + Port: &eureka.Port{ + Port: 80, Enabled: true, + }, + }, + }, + { + expectedProtocol: "https", + instanceInfo: eureka.InstanceInfo{ + SecurePort: &eureka.Port{ + Port: 443, Enabled: true, + }, + Port: &eureka.Port{ + Port: 80, Enabled: false, + }, + }, + }, + } + + eurekaProvider := &Eureka{} + for _, c := range cases { + protocol := eurekaProvider.getProtocol(c.instanceInfo) + if protocol != c.expectedProtocol { + t.Fatalf("Should have been %s, got %s", c.expectedProtocol, protocol) + } + } +} + +func TestEurekaGetWeight(t *testing.T) { + cases := []struct { + expectedWeight string + instanceInfo eureka.InstanceInfo + }{ + { + expectedWeight: "0", + instanceInfo: eureka.InstanceInfo{ + Port: &eureka.Port{ + Port: 80, Enabled: true, + }, + Metadata: &eureka.MetaData{ + Map: map[string]string{}, + }, + }, + }, + { + expectedWeight: "10", + instanceInfo: eureka.InstanceInfo{ + Port: &eureka.Port{ + Port: 80, Enabled: true, + }, + Metadata: &eureka.MetaData{ + Map: map[string]string{ + "traefik.weight": "10", + }, + }, + }, + }, + } + + eurekaProvider := &Eureka{} + for _, c := range cases { + weight := eurekaProvider.getWeight(c.instanceInfo) + if weight != c.expectedWeight { + t.Fatalf("Should have been %s, got %s", c.expectedWeight, weight) + } + } +} + +func TestEurekaGetInstanceId(t *testing.T) { + cases := []struct { + expectedID string + instanceInfo eureka.InstanceInfo + }{ + { + expectedID: "MyInstanceId", + instanceInfo: eureka.InstanceInfo{ + IpAddr: "10.11.12.13", + SecurePort: &eureka.Port{ + Port: 443, Enabled: false, + }, + Port: &eureka.Port{ + Port: 80, Enabled: true, + }, + Metadata: &eureka.MetaData{ + Map: map[string]string{ + "traefik.backend.id": "MyInstanceId", + }, + }, + }, + }, + { + expectedID: "10-11-12-13-80", + instanceInfo: eureka.InstanceInfo{ + IpAddr: "10.11.12.13", + SecurePort: &eureka.Port{ + Port: 443, Enabled: false, + }, + Port: &eureka.Port{ + Port: 80, Enabled: true, + }, + Metadata: &eureka.MetaData{ + Map: map[string]string{}, + }, + }, + }, + } + + eurekaProvider := &Eureka{} + for _, c := range cases { + id := eurekaProvider.getInstanceID(c.instanceInfo) + if id != c.expectedID { + t.Fatalf("Should have been %s, got %s", c.expectedID, id) + } + } +} diff --git a/server.go b/server.go index 83f97864b..01528f5dd 100644 --- a/server.go +++ b/server.go @@ -349,6 +349,9 @@ func (server *Server) configureProviders() { if server.globalConfiguration.Mesos != nil { server.providers = append(server.providers, server.globalConfiguration.Mesos) } + if server.globalConfiguration.Eureka != nil { + server.providers = append(server.providers, server.globalConfiguration.Eureka) + } } func (server *Server) startProviders() { diff --git a/templates/eureka.tmpl b/templates/eureka.tmpl new file mode 100644 index 000000000..10ad8e613 --- /dev/null +++ b/templates/eureka.tmpl @@ -0,0 +1,15 @@ +[backends]{{range .Applications}} + {{ $app := .}} + {{range .Instances}} + [backends.backend{{$app.Name}}.servers.server-{{ getInstanceID . }}] + url = "{{ getProtocol . }}://{{ .IpAddr }}:{{ getPort . }}" + weight = {{ getWeight . }} +{{end}}{{end}} + +[frontends]{{range .Applications}} + [frontends.frontend{{.Name}}] + backend = "backend{{.Name}}" + entryPoints = ["http"] + [frontends.frontend{{.Name }}.routes.route-host{{.Name}}] + rule = "Host:http://{{ .Name | tolower }}" +{{end}}