diff --git a/.gitignore b/.gitignore index 303a52878..2393c5b95 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ dist gen.go .idea log -*.iml \ No newline at end of file +*.iml +./traefik \ No newline at end of file diff --git a/configuration.go b/configuration.go index 0796eb2cb..f1c6813b5 100644 --- a/configuration.go +++ b/configuration.go @@ -12,6 +12,7 @@ type GlobalConfiguration struct { File *FileProvider Web *WebProvider Marathon *MarathonProvider + Consul *ConsulProvider } func NewGlobalConfiguration() *GlobalConfiguration { diff --git a/consul.go b/consul.go new file mode 100644 index 000000000..c6b085d82 --- /dev/null +++ b/consul.go @@ -0,0 +1,155 @@ +package main + +import ( + "github.com/hashicorp/consul/api" + "text/template" + "bytes" + "github.com/BurntSushi/toml" + "strings" + "github.com/BurntSushi/ty/fun" + "net/http" +) + + +type Key struct { + Value string +} + +type ConsulProvider struct { + Watch bool + Endpoint string + Prefix string + Filename string + consulClient *api.Client +} +var kvClient *api.KV + +var ConsulFuncMap = template.FuncMap{ + "List": func(keys ...string) []string { + joinedKeys := strings.Join(keys, "") + keysPairs, _, err := kvClient.Keys(joinedKeys, "/", nil) + if err != nil { + log.Error("Error getting keys ", joinedKeys, err) + return nil + } + keysPairs = fun.Filter(func(key string) bool { + if (key == joinedKeys) { + return false + } + return true + }, keysPairs).([]string) + return keysPairs + }, + "Get": func(keys ...string) string { + joinedKeys := strings.Join(keys, "") + keyPair, _, err := kvClient.Get(joinedKeys, nil) + if err != nil { + log.Error("Error getting key ", joinedKeys, err) + return "" + } + return string(keyPair.Value) + }, + "Last": func(key string) string { + splittedKey := strings.Split(key, "/") + return splittedKey[len(splittedKey) -2] + }, +} + +func NewConsulProvider() *ConsulProvider { + consulProvider := new(ConsulProvider) + // default values + consulProvider.Watch = true + consulProvider.Prefix = "traefik" + + return consulProvider +} + +func (provider *ConsulProvider) Provide(configurationChan chan <- *Configuration) { + config := &api.Config{ + Address: provider.Endpoint, + Scheme: "http", + HttpClient: http.DefaultClient, + } + consulClient, _ := api.NewClient(config) + provider.consulClient = consulClient + if provider.Watch { + var waitIndex uint64 + keypairs, meta, err := consulClient.KV().Keys("", "", nil) + if keypairs == nil && err == nil { + log.Error("Key was not found.") + } + waitIndex = meta.LastIndex + go func() { + for { + opts := api.QueryOptions{ + WaitIndex: waitIndex, + } + keypairs, meta, err := consulClient.KV().Keys("", "", &opts) + if keypairs == nil && err == nil { + log.Error("Key was not found.") + } + waitIndex = meta.LastIndex + configuration := provider.loadConsulConfig() + if configuration != nil { + configurationChan <- configuration + } + } + }() + } + configuration := provider.loadConsulConfig() + configurationChan <- configuration +} + +func (provider *ConsulProvider) loadConsulConfig() *Configuration { + configuration := new(Configuration) + services := []*api.CatalogService{} + kvClient = provider.consulClient.KV() + + servicesName, _, _ := provider.consulClient.Catalog().Services(nil) + for serviceName, _ := range servicesName { + catalogServices, _, _ := provider.consulClient.Catalog().Service(serviceName, "", nil) + for _, catalogService := range catalogServices { + services= append(services, catalogService) + } + } + + templateObjects := struct { + Services []*api.CatalogService + }{ + services, + } + + tmpl := template.New(provider.Filename).Funcs(ConsulFuncMap) + if len(provider.Filename) > 0 { + _, err := tmpl.ParseFiles(provider.Filename) + if err != nil { + log.Error("Error reading file", err) + return nil + } + } else { + buf, err := Asset("providerTemplates/consul.tmpl") + if err != nil { + log.Error("Error reading file", err) + } + _, err = tmpl.Parse(string(buf)) + if err != nil { + log.Error("Error reading file", err) + return nil + } + } + + var buffer bytes.Buffer + + err := tmpl.Execute(&buffer, templateObjects) + if err != nil { + log.Error("Error with consul template:", err) + return nil + } + + if _, err := toml.Decode(buffer.String(), configuration); err != nil { + log.Error("Error creating consul configuration:", err) + return nil + } + + return configuration +} diff --git a/docker.go b/docker.go index 422e5d9e6..919fc3eb5 100644 --- a/docker.go +++ b/docker.go @@ -7,7 +7,6 @@ import ( "github.com/BurntSushi/ty/fun" "github.com/cenkalti/backoff" "github.com/fsouza/go-dockerclient" - "github.com/leekchan/gtf" "strconv" "strings" "text/template" @@ -73,8 +72,8 @@ func (provider *DockerProvider) Provide(configurationChan chan<- *Configuration) log.Fatalf("Docker connection error %+v", err) } log.Debug("Docker connection established") - dockerEvents := make(chan *docker.APIEvents) if provider.Watch { + dockerEvents := make(chan *docker.APIEvents) dockerClient.AddEventListener(dockerEvents) log.Debug("Docker listening") go func() { @@ -152,7 +151,6 @@ func (provider *DockerProvider) loadDockerConfig(dockerClient *docker.Client) *C hosts, provider.Domain, } - gtf.Inject(DockerFuncMap) tmpl := template.New(provider.Filename).Funcs(DockerFuncMap) if len(provider.Filename) > 0 { _, err := tmpl.ParseFiles(provider.Filename) diff --git a/marathon.go b/marathon.go index ca9323083..b9b13608b 100644 --- a/marathon.go +++ b/marathon.go @@ -5,7 +5,6 @@ import ( "github.com/BurntSushi/toml" "github.com/BurntSushi/ty/fun" "github.com/gambol99/go-marathon" - "github.com/leekchan/gtf" "strconv" "strings" "text/template" @@ -148,8 +147,7 @@ func (provider *MarathonProvider) loadMarathonConfig() *Configuration { provider.Domain, } - gtf.Inject(MarathonFuncMap) - tmpl := template.New(provider.Filename).Funcs(DockerFuncMap) + tmpl := template.New(provider.Filename).Funcs(MarathonFuncMap) if len(provider.Filename) > 0 { _, err := tmpl.ParseFiles(provider.Filename) if err != nil { diff --git a/providerTemplates/consul.tmpl b/providerTemplates/consul.tmpl new file mode 100644 index 000000000..c38b23c0a --- /dev/null +++ b/providerTemplates/consul.tmpl @@ -0,0 +1,24 @@ +{{$frontends := "frontends/" | List }} +{{$backends := "backends/" | List }} + +{{range $backends}} +{{$backend := .}} +{{$servers := "servers/" | List $backend }} +{{range $servers}} +[backends.{{Last $backend}}.servers.{{Last .}}] + url = "{{Get . "/url"}}" + weight = {{Get . "/weight"}} +{{end}} +{{end}} + +[frontends]{{range $frontends}} + {{$frontend := Last .}} + [frontends.{{$frontend}}] + backend = "{{Get . "/backend"}}" + {{$routes := "routes/" | List .}} + {{range $routes}} + [frontends.{{$frontend}}.routes.{{Last .}}] + rule = "{{Get . "/rule"}}" + value = "{{Get . "/value"}}" + {{end}} +{{end}} \ No newline at end of file diff --git a/tests/compose-consul.yml b/tests/compose-consul.yml new file mode 100644 index 000000000..83f30b7a9 --- /dev/null +++ b/tests/compose-consul.yml @@ -0,0 +1,21 @@ +consul: + image: progrium/consul + command: -server -bootstrap -advertise 12.0.0.254 -log-level debug -ui-dir /ui + ports: + - "8400:8400" + - "8500:8500" + - "8600:53/udp" + expose: + - "8300" + - "8301" + - "8301/udp" + - "8302" + - "8302/udp" + +registrator: + image: gliderlabs/registrator:master + command: -internal consulkv://consul:8500/traefik + volumes: + - /var/run/docker.sock:/tmp/docker.sock + links: + - consul \ No newline at end of file diff --git a/tests/compose-marathon.yml b/tests/compose-marathon.yml new file mode 100644 index 000000000..91685e963 --- /dev/null +++ b/tests/compose-marathon.yml @@ -0,0 +1,42 @@ +zk: + image: bobrik/zookeeper + net: host + environment: + ZK_CONFIG: tickTime=2000,initLimit=10,syncLimit=5,maxClientCnxns=128,forceSync=no,clientPort=2181 + ZK_ID: 1 + +master: + image: mesosphere/mesos-master:0.23.0-1.0.ubuntu1404 + net: host + environment: + MESOS_ZK: zk://127.0.0.1:2181/mesos + MESOS_HOSTNAME: 127.0.0.1 + MESOS_IP: 127.0.0.1 + MESOS_QUORUM: 1 + MESOS_CLUSTER: docker-compose + MESOS_WORK_DIR: /var/lib/mesos + +slave: + image: mesosphere/mesos-slave:0.23.0-1.0.ubuntu1404 + net: host + pid: host + privileged: true + environment: + MESOS_MASTER: zk://127.0.0.1:2181/mesos + MESOS_HOSTNAME: 127.0.0.1 + MESOS_IP: 127.0.0.1 + MESOS_CONTAINERIZERS: docker,mesos + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup + - /usr/bin/docker:/usr/bin/docker:ro + - /usr/lib/x86_64-linux-gnu/libapparmor.so.1:/usr/lib/x86_64-linux-gnu/libapparmor.so.1:ro + - /var/run/docker.sock:/var/run/docker.sock + +marathon: + image: mesosphere/marathon:v0.9.2 + net: host + environment: + MARATHON_MASTER: zk://127.0.0.1:2181/mesos + MARATHON_ZK: zk://127.0.0.1:2181/marathon + MARATHON_HOSTNAME: 127.0.0.1 + command: --event_subscriber http_callback diff --git a/traefik.go b/traefik.go index 414fefba2..66a2820e7 100644 --- a/traefik.go +++ b/traefik.go @@ -41,7 +41,7 @@ func main() { fmtlog.SetFlags(fmtlog.Lshortfile | fmtlog.LstdFlags) var srv *graceful.Server var configurationRouter *mux.Router - var configurationChan = make(chan *Configuration) + var configurationChan = make(chan *Configuration, 10) defer close(configurationChan) var providers = []Provider{} var format = logging.MustStringFormatter("%{color}%{time:15:04:05.000} %{shortfile:20.20s} %{level:8.8s} %{id:03x} ▶%{color:reset} %{message}") @@ -122,7 +122,9 @@ func main() { if gloablConfiguration.Web != nil { providers = append(providers, gloablConfiguration.Web) } - // providers = append(providers, NewConsulProvider()) + if gloablConfiguration.Consul != nil { + providers = append(providers, gloablConfiguration.Consul) + } // start providers for _, provider := range providers { diff --git a/traefik.sample.toml b/traefik.sample.toml index 16adb1603..652489b8e 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -172,6 +172,41 @@ # filename = "marathon.tmpl" +################################################################ +# Consul KV configuration backend +################################################################ + +# Enable Consul KV configuration backend +# +# Optional +# +# [consul] + +# Consul server endpoint +# +# Required +# +# endpoint = "http://127.0.0.1:8500" + +# Enable watch Consul changes +# +# Optional +# +# watch = true + +# Prefix used for KV store. +# +# Optional +# +# prefix = "traefik" + +# Override default configuration template. For advanced users :) +# +# Optional +# +# filename = "consul.tmpl" + + ################################################################ # Sample rules diff --git a/traefik.toml b/traefik.toml index 3de4d6006..1950edbe5 100644 --- a/traefik.toml +++ b/traefik.toml @@ -41,7 +41,7 @@ port = ":8081" # Optional # Default: "ERROR" # -# logLevel = "ERROR" +logLevel = "DEBUG" # SSL certificate and key used # @@ -75,7 +75,7 @@ address = ":8082" # # Optional # -# [file] +[file] # Rules file # If defined, traefik will load rules from this file, @@ -89,7 +89,7 @@ address = ":8082" # # Optional # -# watch = true +watch = true ################################################################ @@ -100,26 +100,26 @@ address = ":8082" # # Optional # -[docker] +# [docker] # Docker server endpoint. Can be a tcp or a unix socket endpoint. # # Required # -endpoint = "unix:///var/run/docker.sock" +# endpoint = "unix:///var/run/docker.sock" # Enable watch docker changes # # Optional # -watch = true +# watch = true # Default domain used. # Can be overridden by setting the "traefik.domain" label on a container. # # Required # -domain = "docker.localhost" +# domain = "docker.localhost" # Override default configuration template. For advanced users :) # @@ -172,34 +172,68 @@ domain = "docker.localhost" # filename = "marathon.tmpl" +################################################################ +# Consul KV configuration backend +################################################################ + +# Enable Consul KV configuration backend +# +# Optional +# +# [consul] + +# Consul server endpoint +# +# Required +# +# endpoint = "http://127.0.0.1:8500" + +# Enable watch Consul changes +# +# Optional +# +# watch = true + +# Prefix used for KV store. +# +# Optional +# +# prefix = "traefik" + +# Override default configuration template. For advanced users :) +# +# Optional +# +# filename = "consul.tmpl" + ################################################################ # Sample rules ################################################################ -# [backends] -# [backends.backend1] -# [backends.backend1.servers.server1] -# url = "http://172.17.0.2:80" -# weight = 10 -# [backends.backend1.servers.server2] -# url = "http://172.17.0.3:80" -# weight = 1 -# [backends.backend2] -# [backends.backend2.servers.server1] -# url = "http://172.17.0.4:80" -# weight = 1 -# [backends.backend2.servers.server2] -# url = "http://172.17.0.5:80" -# weight = 2 -# -# [frontends] -# [frontends.frontend1] -# backend = "backend2" -# [frontends.frontend1.routes.test_1] -# category = "Host" -# value = "test.localhost" -# [frontends.frontend2] -# backend = "backend1" -# [frontends.frontend2.routes.test_2] -# category = "Path" -# value = "/test" +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "http://172.17.0.2:80" + weight = 10 + [backends.backend1.servers.server2] + url = "http://172.17.0.3:80" + weight = 1 + [backends.backend2] + [backends.backend2.servers.server1] + url = "http://172.17.0.83:80" + weight = 3 + [backends.backend2.servers.server2] + url = "http://172.17.0.5:80" + weight = 10 + +[frontends] + [frontends.frontend1] + backend = "backend2" + [frontends.frontend1.routes.test_1] + rule = "Host" + value = "test.localhost" + [frontends.frontend2] + backend = "backend1" + [frontends.frontend2.routes.test_2] + rule = "Path" + value = "/test"