diff --git a/CHANGELOG.md b/CHANGELOG.md
index 307089ed9..4d541ef2f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,33 @@ All notable changes to this project will be documented in this file.
### Changed
+## [v7] - 2016-03-05
+### Fixed
+- Providing a SERVICE_NAME for a container with multiple ports exposed would cause services to overwrite each other
+- dd3ab2e Fix specific port names not overriding port suffix
+
+### Added
+- bridge.Ping - calls adapter.Ping
+- Consul TCP Health Check
+- Support for Consul unix sockets
+- Basic Zookeper backend
+- Support for Docker multi host networking
+- Default to tcp for PortType if not provided
+- Sync etcd cluster on service registration
+- Support hostip for overlay network
+- Cleanup dangling services
+- Startup backend service connection retry
+
+### Removed
+
+### Changed
+- Upgraded base image to alpine:3.2 and go 1.4
+- bridge.New returns an error instead of calling log.Fatal
+- bridge.New will not attempt to ping an adapter.
+- Specifying a SERVICE_NAME for containers exposing multiple ports will now result in a named service per port. #194
+- Etcd uses port 2379 instead of 4001 #340
+- Setup Docker client from environment
+- Use exit status to determine if container was killed
## [v6] - 2015-08-07
### Fixed
@@ -17,6 +44,7 @@ All notable changes to this project will be documented in this file.
- Panic from invalid skydns2 URI.
### Added
+- Basic zookeeper adapter
- Optional periodic resyncing of services from containers
- More error logging for registries
- Support for services on containers with `--net=host`
@@ -54,6 +82,7 @@ All notable changes to this project will be documented in this file.
- Dropped Godeps for now
-[unreleased]: https://github.com/gliderlabs/registrator/compare/v6...HEAD
+[unreleased]: https://github.com/gliderlabs/registrator/compare/v7...HEAD
+[v7]: https://github.com/gliderlabs/registrator/compare/v6...v7
[v6]: https://github.com/gliderlabs/registrator/compare/v5...v6
[v5]: https://github.com/gliderlabs/registrator/compare/v0.4.0...v5
diff --git a/Dockerfile b/Dockerfile
index b793d38a7..238b04426 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM gliderlabs/alpine:3.1
+FROM gliderlabs/alpine:3.2
ENTRYPOINT ["/bin/registrator"]
COPY . /go/src/github.com/gliderlabs/registrator
diff --git a/Dockerfile.dev b/Dockerfile.dev
index 1d7934626..dff9429a3 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -1,4 +1,4 @@
-FROM gliderlabs/alpine:3.1
+FROM gliderlabs/alpine:3.2
CMD ["/bin/registrator"]
ENV GOPATH /go
diff --git a/Makefile b/Makefile
index 1f01ed9c5..42445777f 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,12 @@
NAME=registrator
VERSION=$(shell cat VERSION)
+DEV_RUN_OPTS ?= consul:
dev:
docker build -f Dockerfile.dev -t $(NAME):dev .
docker run --rm \
-v /var/run/docker.sock:/tmp/docker.sock \
- $(NAME):dev /bin/registrator consul:
+ $(NAME):dev /bin/registrator $(DEV_RUN_OPTS)
build:
mkdir -p build
@@ -21,6 +22,7 @@ release:
glu hubtag gliderlabs/$(NAME) $(VERSION)
docs:
+ boot2docker ssh "sync; sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'" || true
docker run --rm -it -p 8000:8000 -v $(PWD):/work gliderlabs/pagebuilder mkdocs serve
circleci:
diff --git a/README.md b/README.md
index 1c98effda..f748087e5 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@ Service registry bridge for Docker, sponsored by [Weave](http://weave.works).
[](https://circleci.com/gh/gliderlabs/registrator)
[](https://registry.hub.docker.com/u/gliderlabs/registrator/)
+[](https://imagelayers.io/?images=gliderlabs%2Fregistrator:latest)
[](https://kiwiirc.com/client/irc.freenode.net/#gliderlabs)
@@ -13,12 +14,17 @@ supports pluggable service registries, which currently includes
[Consul](http://www.consul.io/), [etcd](https://github.com/coreos/etcd) and
[SkyDNS 2](https://github.com/skynetservices/skydns/).
+Full documentation available at http://gliderlabs.com/registrator
+
## Getting Registrator
Get the latest release, master, or any version of Registrator via [Docker Hub](https://registry.hub.docker.com/u/gliderlabs/registrator/):
$ docker pull gliderlabs/registrator:latest
+Latest tag always points to the latest release. There is also a `:master` tag
+and version tags to pin to specific releases.
+
## Using Registrator
The quickest way to see Registrator in action is our
@@ -42,12 +48,11 @@ discussing in [Slack](http://glider-slackin.herokuapp.com/).
Also check out our Developer Guide on [Contributing
Backends](https://gliderlabs.com/registrator/latest/dev/backends) and [Staging
-Releases](https://gliderlabs.com/registrator/latest/dev/releases.).
+Releases](https://gliderlabs.com/registrator/latest/dev/releases).
## Sponsors and Thanks
-Ongoing support of this project is made possible by [Weave](http://weave.works),
-the Docker SDN. Big thanks to Michael Crosby for
+Ongoing support of this project is made possible by [Weave](http://weave.works), the easiest way to connect, observe and control your containers. Big thanks to Michael Crosby for
[skydock](https://github.com/crosbymichael/skydock) and the Consul mailing list
for inspiration.
diff --git a/SPONSORS b/SPONSORS
index 47c141492..c70a20d8b 100644
--- a/SPONSORS
+++ b/SPONSORS
@@ -1,2 +1,2 @@
-DigitalOcean http://digitalocean.com
+DigitalOcean http://digitalocean.com
Weaveworks http://weave.works
diff --git a/VERSION b/VERSION
index 9c0be88a7..02a819f21 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-v6
+v7
diff --git a/bridge/bridge.go b/bridge/bridge.go
index 4901c0901..673605df6 100644
--- a/bridge/bridge.go
+++ b/bridge/bridge.go
@@ -1,11 +1,13 @@
package bridge
import (
+ "errors"
"log"
"net"
"net/url"
"os"
"path"
+ "regexp"
"strconv"
"strings"
"sync"
@@ -13,6 +15,8 @@ import (
dockerapi "github.com/fsouza/go-dockerclient"
)
+var serviceIDPattern = regexp.MustCompile(`^(.+?):([a-zA-Z0-9][a-zA-Z0-9_.-]+):[0-9]+(?::udp)?$`)
+
type Bridge struct {
sync.Mutex
registry RegistryAdapter
@@ -22,28 +26,28 @@ type Bridge struct {
config Config
}
-func New(docker *dockerapi.Client, adapterUri string, config Config) *Bridge {
+func New(docker *dockerapi.Client, adapterUri string, config Config) (*Bridge, error) {
uri, err := url.Parse(adapterUri)
if err != nil {
- log.Fatal("Bad adapter URI:", adapterUri)
+ return nil, errors.New("bad adapter uri: " + adapterUri)
}
factory, found := AdapterFactories.Lookup(uri.Scheme)
if !found {
- log.Fatal("Unrecognized adapter:", adapterUri)
- }
- adapter := factory.New(uri)
- err = adapter.Ping()
- if err != nil {
- log.Fatalf("%s: %s", uri.Scheme, err)
+ return nil, errors.New("unrecognized adapter: " + adapterUri)
}
+
log.Println("Using", uri.Scheme, "adapter:", uri)
return &Bridge{
docker: docker,
config: config,
- registry: adapter,
+ registry: factory.New(uri),
services: make(map[string][]*Service),
deadContainers: make(map[string]*DeadContainer),
- }
+ }, nil
+}
+
+func (b *Bridge) Ping() error {
+ return b.registry.Ping()
}
func (b *Bridge) Add(containerId string) {
@@ -57,7 +61,7 @@ func (b *Bridge) Remove(containerId string) {
}
func (b *Bridge) RemoveOnExit(containerId string) {
- b.remove(containerId, b.config.DeregisterCheck == "always" || b.didExitCleanly(containerId))
+ b.remove(containerId, b.shouldRemove(containerId))
}
func (b *Bridge) Refresh() {
@@ -97,8 +101,7 @@ func (b *Bridge) Sync(quiet bool) {
log.Printf("Syncing services on %d containers", len(containers))
- // NOTE: This assumes reregistering will do the right thing, i.e. nothing.
- // NOTE: This will NOT remove services.
+ // NOTE: This assumes reregistering will do the right thing, i.e. nothing..
for _, listing := range containers {
services := b.services[listing.ID]
if services == nil {
@@ -112,6 +115,47 @@ func (b *Bridge) Sync(quiet bool) {
}
}
}
+
+ // Clean up services that were registered previously, but aren't
+ // acknowledged within registrator
+ if b.config.Cleanup {
+ log.Println("Cleaning up dangling services")
+
+ extServices, err := b.registry.Services()
+ if err != nil {
+ log.Println("cleanup failed:", err)
+ return
+ }
+
+ Outer:
+ for _, extService := range extServices {
+ matches := serviceIDPattern.FindStringSubmatch(extService.ID)
+ if len(matches) != 3 {
+ // There's no way this was registered by us, so leave it
+ continue
+ }
+ serviceHostname := matches[1]
+ if serviceHostname != Hostname {
+ // ignore because registered on a different host
+ continue
+ }
+ serviceContainerName := matches[2]
+ for _, listing := range b.services {
+ for _, service := range listing {
+ if service.Name == extService.Name && serviceContainerName == service.Origin.container.Name[1:] {
+ continue Outer
+ }
+ }
+ }
+ log.Println("dangling:", extService.ID)
+ err := b.registry.Deregister(extService)
+ if err != nil {
+ log.Println("deregister failed:", extService.ID, err)
+ continue
+ }
+ log.Println(extService.ID, "removed")
+ }
+ }
}
func (b *Bridge) add(containerId string, quiet bool) {
@@ -176,20 +220,16 @@ func (b *Bridge) add(containerId string, quiet bool) {
func (b *Bridge) newService(port ServicePort, isgroup bool) *Service {
container := port.container
defaultName := strings.Split(path.Base(container.Config.Image), ":")[0]
- if isgroup {
- defaultName = defaultName + "-" + port.ExposedPort
- }
// not sure about this logic. kind of want to remove it.
- hostname, err := os.Hostname()
- if err != nil {
+ hostname := Hostname
+ if hostname == "" {
hostname = port.HostIP
- } else {
- if port.HostIP == "0.0.0.0" {
- ip, err := net.ResolveIPAddr("ip", hostname)
- if err == nil {
- port.HostIP = ip.String()
- }
+ }
+ if port.HostIP == "0.0.0.0" {
+ ip, err := net.ResolveIPAddr("ip", hostname)
+ if err == nil {
+ port.HostIP = ip.String()
}
}
@@ -197,7 +237,7 @@ func (b *Bridge) newService(port ServicePort, isgroup bool) *Service {
port.HostIP = b.config.HostIp
}
- metadata := serviceMetaData(container.Config, port.ExposedPort)
+ metadata, metadataFromPort := serviceMetaData(container.Config, port.ExposedPort)
ignore := mapDefault(metadata, "ignore", "")
if ignore != "" {
@@ -208,6 +248,9 @@ func (b *Bridge) newService(port ServicePort, isgroup bool) *Service {
service.Origin = port
service.ID = hostname + ":" + container.Name[1:] + ":" + port.ExposedPort
service.Name = mapDefault(metadata, "name", defaultName)
+ if isgroup && !metadataFromPort["name"] {
+ service.Name += "-" + port.ExposedPort
+ }
var p int
if b.config.Internal == true {
service.IP = port.ExposedIP
@@ -268,7 +311,13 @@ func (b *Bridge) remove(containerId string, deregister bool) {
delete(b.services, containerId)
}
-func (b *Bridge) didExitCleanly(containerId string) bool {
+// bit set on ExitCode if it represents an exit via a signal
+var dockerSignaledBit = 128
+
+func (b *Bridge) shouldRemove(containerId string) bool {
+ if b.config.DeregisterCheck == "always" {
+ return true
+ }
container, err := b.docker.InspectContainer(containerId)
if _, ok := err.(*dockerapi.NoSuchContainer); ok {
// the container has already been removed from Docker
@@ -276,9 +325,27 @@ func (b *Bridge) didExitCleanly(containerId string) bool {
// so its exit code is not accessible
log.Printf("registrator: container %v was removed, could not fetch exit code", containerId[:12])
return true
- } else if err != nil {
+ }
+
+ switch {
+ case err != nil:
log.Printf("registrator: error fetching status for container %v on \"die\" event: %v\n", containerId[:12], err)
return false
+ case container.State.Running:
+ log.Printf("registrator: not removing container %v, still running", containerId[:12])
+ return false
+ case container.State.ExitCode == 0:
+ return true
+ case container.State.ExitCode&dockerSignaledBit == dockerSignaledBit:
+ return true
}
- return !container.State.Running && container.State.ExitCode == 0
+ return false
+}
+
+var Hostname string
+
+func init() {
+ // It's ok for Hostname to ultimately be an empty string
+ // An empty string will fall back to trying to make a best guess
+ Hostname, _ = os.Hostname()
}
diff --git a/bridge/bridge_test.go b/bridge/bridge_test.go
new file mode 100644
index 000000000..7aa67e82e
--- /dev/null
+++ b/bridge/bridge_test.go
@@ -0,0 +1,23 @@
+package bridge
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewError(t *testing.T) {
+ bridge, err := New(nil, "", Config{})
+ assert.Nil(t, bridge)
+ assert.Error(t, err)
+}
+
+func TestNewValid(t *testing.T) {
+ Register(new(fakeFactory), "fake")
+ // Note: the following is valid for New() since it does not
+ // actually connect to docker.
+ bridge, err := New(nil, "fake://", Config{})
+
+ assert.NotNil(t, bridge)
+ assert.NoError(t, err)
+}
diff --git a/bridge/extpoints.go b/bridge/extpoints.go
index bf82346e2..dd46dcf31 100644
--- a/bridge/extpoints.go
+++ b/bridge/extpoints.go
@@ -139,4 +139,3 @@ func (ep *adapterFactoryExt) All() map[string]AdapterFactory {
}
return all
}
-
diff --git a/bridge/types.go b/bridge/types.go
index 6560f4683..b1611127e 100644
--- a/bridge/types.go
+++ b/bridge/types.go
@@ -16,6 +16,7 @@ type RegistryAdapter interface {
Register(service *Service) error
Deregister(service *Service) error
Refresh(service *Service) error
+ Services() ([]*Service, error)
}
type Config struct {
@@ -25,6 +26,7 @@ type Config struct {
RefreshTtl int
RefreshInterval int
DeregisterCheck string
+ Cleanup bool
}
type Service struct {
@@ -52,5 +54,6 @@ type ServicePort struct {
PortType string
ContainerHostname string
ContainerID string
+ ContainerName string
container *dockerapi.Container
}
diff --git a/bridge/types_test.go b/bridge/types_test.go
new file mode 100644
index 000000000..fe2265e8e
--- /dev/null
+++ b/bridge/types_test.go
@@ -0,0 +1,25 @@
+package bridge
+
+import "net/url"
+
+type fakeFactory struct{}
+
+func (f *fakeFactory) New(uri *url.URL) RegistryAdapter {
+
+ return &fakeAdapter{}
+}
+
+type fakeAdapter struct{}
+
+func (f *fakeAdapter) Ping() error {
+ return nil
+}
+func (f *fakeAdapter) Register(service *Service) error {
+ return nil
+}
+func (f *fakeAdapter) Deregister(service *Service) error {
+ return nil
+}
+func (f *fakeAdapter) Refresh(service *Service) error {
+ return nil
+}
diff --git a/bridge/util.go b/bridge/util.go
index b151e533c..bcca0508e 100644
--- a/bridge/util.go
+++ b/bridge/util.go
@@ -30,16 +30,20 @@ func combineTags(tagParts ...string) []string {
return tags
}
-func serviceMetaData(config *dockerapi.Config, port string) map[string]string {
+func serviceMetaData(config *dockerapi.Config, port string) (map[string]string, map[string]bool) {
meta := config.Env
for k, v := range config.Labels {
- meta = append(meta, k + "=" + v)
+ meta = append(meta, k+"="+v)
}
metadata := make(map[string]string)
+ metadataFromPort := make(map[string]bool)
for _, kv := range meta {
kvp := strings.SplitN(kv, "=", 2)
if strings.HasPrefix(kvp[0], "SERVICE_") && len(kvp) > 1 {
key := strings.ToLower(strings.TrimPrefix(kvp[0], "SERVICE_"))
+ if metadataFromPort[key] {
+ continue
+ }
portkey := strings.SplitN(key, "_", 2)
_, err := strconv.Atoi(portkey[0])
if err == nil && len(portkey) > 1 {
@@ -47,16 +51,17 @@ func serviceMetaData(config *dockerapi.Config, port string) map[string]string {
continue
}
metadata[portkey[1]] = kvp[1]
+ metadataFromPort[portkey[1]] = true
} else {
metadata[key] = kvp[1]
}
}
}
- return metadata
+ return metadata, metadataFromPort
}
func servicePort(container *dockerapi.Container, port dockerapi.Port, published []dockerapi.PortBinding) ServicePort {
- var hp, hip string
+ var hp, hip, ep, ept, eip, nm string
if len(published) > 0 {
hp = published[0].HostPort
hip = published[0].HostIP
@@ -64,13 +69,37 @@ func servicePort(container *dockerapi.Container, port dockerapi.Port, published
if hip == "" {
hip = "0.0.0.0"
}
- p := strings.Split(string(port), "/")
+
+ //for overlay networks
+ //detect if container use overlay network, than set HostIP into NetworkSettings.Network[string].IPAddress
+ //better to use registrator with -internal flag
+ nm = container.HostConfig.NetworkMode
+ if nm != "bridge" || nm != "default" || nm != "host" {
+ hip = container.NetworkSettings.Networks[nm].IPAddress
+ }
+
+ exposedPort := strings.Split(string(port), "/")
+ ep = exposedPort[0]
+ if len(exposedPort) == 2 {
+ ept = exposedPort[1]
+ } else {
+ ept = "tcp" // default
+ }
+
+ // Nir: support docker NetworkSettings
+ eip = container.NetworkSettings.IPAddress
+ if eip == "" {
+ for _, network := range container.NetworkSettings.Networks {
+ eip = network.IPAddress
+ }
+ }
+
return ServicePort{
HostPort: hp,
HostIP: hip,
- ExposedPort: p[0],
- ExposedIP: container.NetworkSettings.IPAddress,
- PortType: p[1],
+ ExposedPort: ep,
+ ExposedIP: eip,
+ PortType: ept,
ContainerID: container.ID,
ContainerHostname: container.Config.Hostname,
container: container,
diff --git a/consul/consul.go b/consul/consul.go
index 8c1bc5eac..b681b89ec 100644
--- a/consul/consul.go
+++ b/consul/consul.go
@@ -13,7 +13,9 @@ import (
const DefaultInterval = "10s"
func init() {
- bridge.Register(new(Factory), "consul")
+ f := new(Factory)
+ bridge.Register(f, "consul")
+ bridge.Register(f, "consul-unix")
}
func (r *ConsulAdapter) interpolateService(script string, service *bridge.Service) string {
@@ -26,7 +28,9 @@ type Factory struct{}
func (f *Factory) New(uri *url.URL) bridge.RegistryAdapter {
config := consulapi.DefaultConfig()
- if uri.Host != "" {
+ if uri.Scheme == "consul-unix" {
+ config.Address = strings.TrimPrefix(uri.String(), "consul-")
+ } else if uri.Host != "" {
config.Address = uri.Host
}
client, err := consulapi.NewClient(config)
@@ -76,10 +80,15 @@ func (r *ConsulAdapter) buildCheck(service *bridge.Service) *consulapi.AgentServ
check.Script = r.interpolateService(script, service)
} else if ttl := service.Attrs["check_ttl"]; ttl != "" {
check.TTL = ttl
+ } else if tcp := service.Attrs["check_tcp"]; tcp != "" {
+ check.TCP = fmt.Sprintf("%s:%d", service.IP, service.Port)
+ if timeout := service.Attrs["check_timeout"]; timeout != "" {
+ check.Timeout = timeout
+ }
} else {
return nil
}
- if check.Script != "" || check.HTTP != "" {
+ if check.Script != "" || check.HTTP != "" || check.TCP != "" {
if interval := service.Attrs["check_interval"]; interval != "" {
check.Interval = interval
} else {
@@ -96,3 +105,24 @@ func (r *ConsulAdapter) Deregister(service *bridge.Service) error {
func (r *ConsulAdapter) Refresh(service *bridge.Service) error {
return nil
}
+
+func (r *ConsulAdapter) Services() ([]*bridge.Service, error) {
+ services, err := r.client.Agent().Services()
+ if err != nil {
+ return []*bridge.Service{}, err
+ }
+ out := make([]*bridge.Service, len(services))
+ i := 0
+ for _, v := range services {
+ s := &bridge.Service{
+ ID: v.ID,
+ Name: v.Service,
+ Port: v.Port,
+ Tags: v.Tags,
+ IP: v.Address,
+ }
+ out[i] = s
+ i++
+ }
+ return out, nil
+}
diff --git a/consulkv/consulkv.go b/consulkv/consulkv.go
index 6947a7666..7d5cc12b3 100644
--- a/consulkv/consulkv.go
+++ b/consulkv/consulkv.go
@@ -5,27 +5,34 @@ import (
"net"
"net/url"
"strconv"
+ "strings"
"github.com/gliderlabs/registrator/bridge"
consulapi "github.com/hashicorp/consul/api"
)
func init() {
- bridge.Register(new(Factory), "consulkv")
+ f := new(Factory)
+ bridge.Register(f, "consulkv")
+ bridge.Register(f, "consulkv-unix")
}
type Factory struct{}
func (f *Factory) New(uri *url.URL) bridge.RegistryAdapter {
config := consulapi.DefaultConfig()
- if uri.Host != "" {
+ path := uri.Path
+ if uri.Scheme == "consulkv-unix" {
+ spl := strings.SplitN(uri.Path, ":", 2)
+ config.Address, path = "unix://"+spl[0], spl[1]
+ } else if uri.Host != "" {
config.Address = uri.Host
}
client, err := consulapi.NewClient(config)
if err != nil {
log.Fatal("consulkv: ", uri.Scheme)
}
- return &ConsulKVAdapter{client: client, path: uri.Path}
+ return &ConsulKVAdapter{client: client, path: path}
}
type ConsulKVAdapter struct {
@@ -46,9 +53,11 @@ func (r *ConsulKVAdapter) Ping() error {
}
func (r *ConsulKVAdapter) Register(service *bridge.Service) error {
+ log.Println("Register")
path := r.path[1:] + "/" + service.Name + "/" + service.ID
port := strconv.Itoa(service.Port)
addr := net.JoinHostPort(service.IP, port)
+ log.Printf("path: %s", path)
_, err := r.client.KV().Put(&consulapi.KVPair{Key: path, Value: []byte(addr)}, nil)
if err != nil {
log.Println("consulkv: failed to register service:", err)
@@ -68,3 +77,7 @@ func (r *ConsulKVAdapter) Deregister(service *bridge.Service) error {
func (r *ConsulKVAdapter) Refresh(service *bridge.Service) error {
return nil
}
+
+func (r *ConsulKVAdapter) Services() ([]*bridge.Service, error) {
+ return []*bridge.Service{}, nil
+}
diff --git a/docs/index.md b/docs/index.md
index 56b7ccc70..757c11e92 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -4,6 +4,7 @@ Service registry bridge for Docker, sponsored by [Weave](http://weave.works).
[](https://circleci.com/gh/gliderlabs/registrator)
[](https://registry.hub.docker.com/u/gliderlabs/registrator/)
+[](https://imagelayers.io/?images=gliderlabs%2Fregistrator:latest)
[](https://kiwiirc.com/client/irc.freenode.net/#gliderlabs)
@@ -19,6 +20,9 @@ Get the latest release, master, or any version of Registrator via [Docker Hub](h
$ docker pull gliderlabs/registrator:latest
+Latest tag always points to the latest release. There is also a `:master` tag
+and version tags to pin to specific releases.
+
## Using Registrator
The quickest way to see Registrator in action is our
@@ -44,8 +48,7 @@ and [Staging Releases](dev/releases.md).
## Sponsors and Thanks
-Ongoing support of this project is made possible by [Weave](http://weave.works),
-the Docker SDN. Big thanks to Michael Crosby for
+Ongoing support of this project is made possible by [Weave](http://weave.works), the easiest way to connect, observe and control your containers. Big thanks to Michael Crosby for
[skydock](https://github.com/crosbymichael/skydock) and the Consul mailing list
for inspiration.
diff --git a/docs/user/backends.md b/docs/user/backends.md
index 07a737b05..e7c037d72 100644
--- a/docs/user/backends.md
+++ b/docs/user/backends.md
@@ -9,6 +9,7 @@ See also [Contributing Backends](../dev/backends.md).
## Consul
consul://