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). [![Circle CI](https://circleci.com/gh/gliderlabs/registrator.png?style=shield)](https://circleci.com/gh/gliderlabs/registrator) [![Docker Hub](https://img.shields.io/badge/docker-ready-blue.svg)](https://registry.hub.docker.com/u/gliderlabs/registrator/) +[![ImageLayers Size](https://img.shields.io/imagelayers/image-size/gliderlabs/registrator/latest.svg)](https://imagelayers.io/?images=gliderlabs%2Fregistrator:latest) [![IRC Channel](https://img.shields.io/badge/irc-%23gliderlabs-blue.svg)](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). [![Circle CI](https://circleci.com/gh/gliderlabs/registrator.png?style=shield)](https://circleci.com/gh/gliderlabs/registrator) [![Docker Hub](https://img.shields.io/badge/docker-ready-blue.svg)](https://registry.hub.docker.com/u/gliderlabs/registrator/) +[![ImageLayers Size](https://img.shields.io/imagelayers/image-size/gliderlabs/registrator/latest.svg)](https://imagelayers.io/?images=gliderlabs%2Fregistrator:latest) [![IRC Channel](https://img.shields.io/badge/irc-%23gliderlabs-blue.svg)](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://
: + consul-unix:// Consul is the recommended registry since it specifically models services for service discovery with health checks. @@ -32,6 +33,18 @@ SERVICE_80_CHECK_TIMEOUT=1s # optional, Consul default used otherwise It works for services on any port, not just 80. If its the only service, you can also use `SERVICE_CHECK_HTTP`. +### Consul TCP Check + +This feature is only available when using Consul 0.6 or newer. Containers +specifying these extra metadata in labels or environment will be used to +register an TCP health check with the service. + +```bash +SERVICE_443_CHECK_TCP=true +SERVICE_443_CHECK_INTERVAL=15s +SERVICE_443_CHECK_TIMEOUT=3s # optional, Consul default used otherwise +``` + ### Consul Script Check This feature is tricky because it lets you specify a script check to run from @@ -63,6 +76,7 @@ SERVICE_CHECK_TTL=30s ## Consul KV consulkv://
:/ + consulkv-unix://:/ This is a separate backend to use Consul's key-value store instead of its native service catalog. This behaves more like etcd since it has similar semantics, but @@ -81,7 +95,7 @@ Using the prefix from the Registry URI, service definitions are stored as: Etcd works similar to Consul KV, except supports service TTLs. It also currently doesn't support service attributes/tags. -If no address and port is specified, it will default to `127.0.0.1:4001`. +If no address and port is specified, it will default to `127.0.0.1:2379`. Using the prefix from the Registry URI, service definitions are stored as: @@ -94,7 +108,7 @@ Using the prefix from the Registry URI, service definitions are stored as: SkyDNS 2 uses etcd, so this backend writes service definitions in a format compatible with SkyDNS 2. The path may not be omitted and must be a valid DNS domain for SkyDNS. -If no address and port is specified, it will default to `127.0.0.1:4001`. +If no address and port is specified, it will default to `127.0.0.1:2379`. Using a Registry URI with the domain `cluster.local`, service definitions are stored as: @@ -104,3 +118,22 @@ SkyDNS requires the service ID to be a valid DNS hostname, so this backend requi override service ID to a valid DNS name. Example: $ docker run -d --name redis-1 -e SERVICE_ID=redis-1 -p 6379:6379 redis + +## Zookeeper Store + +The Zookeeper backend lets you publish ephemeral znodes into zookeeper. This mode is enabled by specifying a zookeeper path. The zookeeper backend supports publishing a json znode body complete with defined service attributes/tags as well as the service name and container id. Example URIs: + + $ registrator zookeeper://zookeeper.host/basepath + $ registrator zookeeper://192.168.1.100:9999/basepath + +Within the base path specified in the zookeeper URI, registrator will create the following path tree containing a JSON entry for the service: + + / = + +The JSON will contain all infromation about the published container service. As an example, the following container start: + + docker run -i -p 80 -e 'SERVICE_80_NAME=www' -t ubuntu:14.04 /bin/bash + +Will result in the zookeeper path and JSON znode body: + + /basepath/www/80 = {"Name":"www","IP":"192.168.1.123","PublicPort":49153,"PrivatePort":80,"ContainerID":"9124853ff0d1","Tags":[],"Attrs":{}} diff --git a/docs/user/quickstart.md b/docs/user/quickstart.md index 3f854203a..c0eafd3e7 100644 --- a/docs/user/quickstart.md +++ b/docs/user/quickstart.md @@ -16,7 +16,8 @@ container that will automatically get added to Consul. ## Before Starting We're going to need a host running Docker, which could just be a local -boot2docker VM, and a shell with the `docker` client pointed to that host. +[boot2docker](http://boot2docker.io/) VM, and a shell with the `docker` client +pointed to that host. We'll also need to have Consul running, which can just be running in a container. Let's run a single instance of Consul in server bootstrap mode: diff --git a/docs/user/run.md b/docs/user/run.md index 86f10a059..66824225f 100644 --- a/docs/user/run.md +++ b/docs/user/run.md @@ -31,15 +31,17 @@ hostname (`-h $HOSTNAME`) and using the `-ip` Registrator option below. ## Registrator Options -Option | Description ------- | ----------- -`-internal` | Use exposed ports instead of published ports -`-ip ` | Force IP address used for registering services -`-tags ` | Force comma-separated tags on all registered services -`-deregister ` | Deregister existed services "always" or "on-success". Default: always -`-ttl ` | TTL for services. Default: 0, no expiry (supported backends only) -`-ttl-refresh ` | Frequency service TTLs are refreshed (supported backends only) -`-resync ` | Frequency all services are resynchronized. Default: 0, never +Option | Since | Description +------ | ----- | ----------- +`-internal` | | Use exposed ports instead of published ports +`-ip ` | | Force IP address used for registering services +`-retry-attempts ` | v7 | Max retry attempts to establish a connection with the backend +`-retry-interval ` | v7 | Interval (in millisecond) between retry-attempts +`-tags ` | v5 | Force comma-separated tags on all registered services +`-deregister ` | v6 | Deregister existed services "always" or "on-success". Default: always +`-ttl ` | | TTL for services. Default: 0, no expiry (supported backends only) +`-ttl-refresh ` | | Frequency service TTLs are refreshed (supported backends only) +`-resync ` | v6 | Frequency all services are resynchronized. Default: 0, never If the `-internal` option is used, Registrator will register the docker0 internal IP and port instead of the host mapped ones. @@ -52,6 +54,8 @@ argument. For registry backends that support TTL expiry, Registrator can both set and refresh service TTLs with `-ttl` and `-ttl-refresh`. +If you want unlimited retry-attempts use `-retry-attempts -1`. + The `-resync` options controls how often Registrator will query Docker for all containers and reregister all services. This allows Registrator and the service registry to get back in sync if they fall out of sync. diff --git a/docs/user/services.md b/docs/user/services.md index 4c440bc2c..d7a187c1e 100644 --- a/docs/user/services.md +++ b/docs/user/services.md @@ -53,6 +53,9 @@ These can be implicitly set from the Dockerfile or explicitly set with `docker r You can also tell Registrator to ignore a container by setting a label or environment variable for `SERVICE_IGNORE`. +If you need to ignore individual service on some container, you can use +`SERVICE__IGNORE=true`. + ## Service Name Service names are what you use in service discovery lookups. By default, the @@ -70,7 +73,9 @@ internal exposed port to differentiate from each other. For example, an image `nginx-80` and `nginx-443`. You can override this default name with label or environment variable -`SERVICE_NAME` or `SERVICE_x_NAME`, where `x` is the internal exposed port. +`SERVICE_NAME` or `SERVICE_x_NAME`, where `x` is the internal exposed port. Note +that if a container has multiple exposed ports then setting `SERVICE_NAME` will +still result in multiple services named `SERVICE_NAME-`. ## IP and Port diff --git a/etcd/etcd.go b/etcd/etcd.go index eb6c61ef7..9b440d197 100644 --- a/etcd/etcd.go +++ b/etcd/etcd.go @@ -25,7 +25,7 @@ func (f *Factory) New(uri *url.URL) bridge.RegistryAdapter { if uri.Host != "" { urls = append(urls, "http://"+uri.Host) } else { - urls = append(urls, "http://127.0.0.1:4001") + urls = append(urls, "http://127.0.0.1:2379") } res, err := http.Get(urls[0] + "/version") @@ -52,6 +52,8 @@ type EtcdAdapter struct { } func (r *EtcdAdapter) Ping() error { + r.syncEtcdCluster() + var err error if r.client != nil { rr := etcd.NewRawRequest("GET", "version", nil, nil) @@ -67,7 +69,22 @@ func (r *EtcdAdapter) Ping() error { return nil } +func (r *EtcdAdapter) syncEtcdCluster() { + var result bool + if r.client != nil { + result = r.client.SyncCluster() + } else { + result = r.client2.SyncCluster() + } + + if !result { + log.Println("etcd: sync cluster was unsuccessful") + } +} + func (r *EtcdAdapter) Register(service *bridge.Service) error { + r.syncEtcdCluster() + path := r.path + "/" + service.Name + "/" + service.ID port := strconv.Itoa(service.Port) addr := net.JoinHostPort(service.IP, port) @@ -86,6 +103,8 @@ func (r *EtcdAdapter) Register(service *bridge.Service) error { } func (r *EtcdAdapter) Deregister(service *bridge.Service) error { + r.syncEtcdCluster() + path := r.path + "/" + service.Name + "/" + service.ID var err error @@ -104,3 +123,7 @@ func (r *EtcdAdapter) Deregister(service *bridge.Service) error { func (r *EtcdAdapter) Refresh(service *bridge.Service) error { return r.Register(service) } + +func (r *EtcdAdapter) Services() ([]*bridge.Service, error) { + return []*bridge.Service{}, nil +} diff --git a/mkdocs.yml b/mkdocs.yml index c4d784ab4..c6bd3f07e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,7 @@ site_url: https://gliderlabs.com/registrator repo_url: https://github.com/gliderlabs/registrator dev_addr: 0.0.0.0:8000 theme_dir: /pagebuilder/theme +google_analytics: ['UA-58928488-1', 'auto'] pages: - 'Readme': index.md - 'User Guide': diff --git a/modules.go b/modules.go index 833e5f999..4ac74c313 100644 --- a/modules.go +++ b/modules.go @@ -5,4 +5,5 @@ import ( _ "github.com/gliderlabs/registrator/consulkv" _ "github.com/gliderlabs/registrator/etcd" _ "github.com/gliderlabs/registrator/skydns2" + _ "github.com/gliderlabs/registrator/zookeeper" ) diff --git a/registrator.go b/registrator.go index 0d35eb69a..b76dc9441 100644 --- a/registrator.go +++ b/registrator.go @@ -6,14 +6,18 @@ import ( "fmt" "log" "os" + "strings" "time" dockerapi "github.com/fsouza/go-dockerclient" + "github.com/gliderlabs/pkg/usage" "github.com/gliderlabs/registrator/bridge" ) var Version string +var versionChecker = usage.NewChecker("registrator", Version) + var hostIp = flag.String("ip", "", "IP for ports mapped to the host") var internal = flag.Bool("internal", false, "Use internal ports instead of published ones") var refreshInterval = flag.Int("ttl-refresh", 0, "Frequency with which service TTLs are refreshed") @@ -21,6 +25,9 @@ var refreshTtl = flag.Int("ttl", 0, "TTL for services (default is no expiry)") var forceTags = flag.String("tags", "", "Append tags for all registered services") var resyncInterval = flag.Int("resync", 0, "Frequency with which services are resynchronized") var deregister = flag.String("deregister", "always", "Deregister exited services \"always\" or \"on-success\"") +var retryAttempts = flag.Int("retry-attempts", 0, "Max retry attempts to establish a connection with the backend. Use -1 for infinite retries") +var retryInterval = flag.Int("retry-interval", 2000, "Interval (in millisecond) between retry-attempts.") +var cleanup = flag.Bool("cleanup", false, "Remove dangling services") func getopt(name, def string) string { if env := os.Getenv(name); env != "" { @@ -37,38 +44,86 @@ func assert(err error) { func main() { if len(os.Args) == 2 && os.Args[1] == "--version" { - fmt.Println(Version) + versionChecker.PrintVersion() os.Exit(0) } log.Printf("Starting registrator %s ...", Version) + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " %s [options] \n\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + if flag.NArg() != 1 { + if flag.NArg() == 0 { + fmt.Fprint(os.Stderr, "Missing required argument for registry URI.\n\n") + } else { + fmt.Fprintln(os.Stderr, "Extra unparsed arguments:") + fmt.Fprintln(os.Stderr, " ", strings.Join(flag.Args()[1:], " ")) + fmt.Fprint(os.Stderr, "Options should come before the registry URI argument.\n\n") + } + flag.Usage() + os.Exit(2) + } + if *hostIp != "" { log.Println("Forcing host IP to", *hostIp) } + if (*refreshTtl == 0 && *refreshInterval > 0) || (*refreshTtl > 0 && *refreshInterval == 0) { assert(errors.New("-ttl and -ttl-refresh must be specified together or not at all")) } else if *refreshTtl > 0 && *refreshTtl <= *refreshInterval { assert(errors.New("-ttl must be greater than -ttl-refresh")) } - docker, err := dockerapi.NewClient(getopt("DOCKER_HOST", "unix:///tmp/docker.sock")) + if *retryInterval <= 0 { + assert(errors.New("-retry-interval must be greater than 0")) + } + + dockerHost := os.Getenv("DOCKER_HOST") + if dockerHost == "" { + os.Setenv("DOCKER_HOST", "unix:///tmp/docker.sock") + } + + docker, err := dockerapi.NewClientFromEnv() assert(err) if *deregister != "always" && *deregister != "on-success" { assert(errors.New("-deregister must be \"always\" or \"on-success\"")) } - b := bridge.New(docker, flag.Arg(0), bridge.Config{ + b, err := bridge.New(docker, flag.Arg(0), bridge.Config{ HostIp: *hostIp, Internal: *internal, ForceTags: *forceTags, RefreshTtl: *refreshTtl, RefreshInterval: *refreshInterval, DeregisterCheck: *deregister, + Cleanup: *cleanup, }) + assert(err) + + attempt := 0 + for *retryAttempts == -1 || attempt <= *retryAttempts { + log.Printf("Connecting to backend (%v/%v)", attempt, *retryAttempts) + + err = b.Ping() + if err == nil { + break + } + + if err != nil && attempt == *retryAttempts { + assert(err) + } + + time.Sleep(time.Duration(*retryInterval) * time.Millisecond) + attempt++ + } + // Start event listener before listing containers to avoid missing anything events := make(chan *dockerapi.APIEvents) assert(docker.AddEventListener(events)) @@ -117,8 +172,6 @@ func main() { go b.Add(msg.ID) case "die": go b.RemoveOnExit(msg.ID) - case "stop", "kill": - go b.Remove(msg.ID) } } diff --git a/skydns2/skydns2.go b/skydns2/skydns2.go index 90c1cebec..6f2cb2c4d 100644 --- a/skydns2/skydns2.go +++ b/skydns2/skydns2.go @@ -65,6 +65,10 @@ func (r *Skydns2Adapter) Refresh(service *bridge.Service) error { return r.Register(service) } +func (r *Skydns2Adapter) Services() ([]*bridge.Service, error) { + return []*bridge.Service{}, nil +} + func (r *Skydns2Adapter) servicePath(service *bridge.Service) string { return r.path + "/" + service.Name + "/" + service.ID } diff --git a/zookeeper/zookeeper.go b/zookeeper/zookeeper.go new file mode 100644 index 000000000..a2ac08d59 --- /dev/null +++ b/zookeeper/zookeeper.go @@ -0,0 +1,122 @@ +package zookeeper + +import ( + "encoding/json" + "log" + "net/url" + "strconv" + "time" + + "github.com/gliderlabs/registrator/bridge" + "github.com/samuel/go-zookeeper/zk" +) + +func init() { + bridge.Register(new(Factory), "zookeeper") +} + +type Factory struct{} + +func (f *Factory) New(uri *url.URL) bridge.RegistryAdapter { + c, _, err := zk.Connect([]string{uri.Host}, (time.Second * 10)) + if err != nil { + panic(err) + } + exists, _, err := c.Exists(uri.Path) + if err != nil { + log.Println("zookeeper: error checking if base path exists:", err) + } + if !exists { + c.Create(uri.Path, []byte{}, 0, zk.WorldACL(zk.PermAll)) + } + return &ZkAdapter{client: c, path: uri.Path} +} + +type ZkAdapter struct { + client *zk.Conn + path string +} + +type ZnodeBody struct { + Name string + IP string + PublicPort int + PrivatePort int + ContainerID string + Tags []string + Attrs map[string]string +} + +func (r *ZkAdapter) Register(service *bridge.Service) error { + privatePort, _ := strconv.Atoi(service.Origin.ExposedPort) + publicPortString := strconv.Itoa(service.Port) + acl := zk.WorldACL(zk.PermAll) + basePath := r.path + "/" + service.Name + if (r.path == "/") { + basePath = r.path + service.Name + } + exists, _, err := r.client.Exists(basePath) + if err != nil { + log.Println("zookeeper: error checking if exists: ", err) + } else { + if !exists { + _, err := r.client.Create(basePath, []byte{}, 0, acl) + if err != nil { + log.Println("zookeeper: failed to create base service node at path '" + basePath + "': ", err) + } + } // create base path for the service name if it missing + zbody := &ZnodeBody{Name: service.Name, IP: service.IP, PublicPort: service.Port, PrivatePort: privatePort, Tags: service.Tags, Attrs: service.Attrs, ContainerID: service.Origin.ContainerHostname} + body, err := json.Marshal(zbody) + if err != nil { + log.Println("zookeeper: failed to json encode service body: ", err) + } else { + path := basePath + "/" + service.IP + ":" + publicPortString + _, err = r.client.Create(path, body, 1, acl) + if err != nil { + log.Println("zookeeper: failed to register service at path '" + path + "': ", err) + } // create service path error check + } // json znode body creation check + } // service path exists error check + return err +} + +func (r *ZkAdapter) Ping() error { + _, _, err := r.client.Exists("/") + if err != nil { + log.Println("zookeeper: error on ping check for Exists(/): ", err) + return err + } + return nil +} + +func (r *ZkAdapter) Deregister(service *bridge.Service) error { + basePath := r.path + "/" + service.Name + if (r.path == "/") { + basePath = r.path + service.Name + } + publicPortString := strconv.Itoa(service.Port) + servicePortPath := basePath + "/" + service.IP + ":" + publicPortString + // Delete the service-port znode + err := r.client.Delete(servicePortPath, -1) // -1 means latest version number + if err != nil { + log.Println("zookeeper: failed to deregister service port entry: ", err) + } + // Check if all service-port znodes are removed. + children, _, err := r.client.Children(basePath) + if len(children) == 0 { + // Delete the service name znode + err := r.client.Delete(basePath, -1) + if err != nil { + log.Println("zookeeper: failed to delete service path: ", err) + } + } + return err +} + +func (r *ZkAdapter) Refresh(service *bridge.Service) error { + return r.Register(service) +} + +func (r *ZkAdapter) Services() ([]*bridge.Service, error) { + return []*bridge.Service{}, nil +}