├── .circleci └── config.yml ├── .dockerignore ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile.dev ├── ISSUE_TEMPLATE.md ├── LICENSE ├── Makefile ├── README.md ├── SPONSORS ├── VERSION ├── bridge ├── bridge.go ├── bridge_test.go ├── extpoints.go ├── types.go ├── types_test.go ├── util.go └── util_test.go ├── consul └── consul.go ├── consulkv └── consulkv.go ├── docs ├── dev │ ├── backends.md │ └── releases.md ├── index.md └── user │ ├── backends.md │ ├── faq.md │ ├── quickstart.md │ ├── run.md │ └── services.md ├── etcd └── etcd.go ├── go.mod ├── go.sum ├── mkdocs.yml ├── modules.go ├── registrator.go ├── skydns2 └── skydns2.go └── zookeeper └── zookeeper.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: gliderlabs/ci:build-2 6 | command: ["/bin/bash"] 7 | working_directory: /go/src/github.com/gliderlabs/registrator 8 | steps: 9 | - checkout 10 | - setup_remote_docker 11 | - run: 12 | command: make circleci 13 | - run: 14 | name: Build 15 | command: make build 16 | - store_artifacts: 17 | path: build 18 | - deploy: 19 | name: Deploy website 20 | command: | 21 | if is-branch "master"; then 22 | eval $(docker run gliderlabs/pagebuilder circleci-cmd) 23 | fi 24 | - deploy: 25 | name: Deploy beta channel 26 | command: | 27 | if is-branch "release"; then 28 | make release 29 | fi 30 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gvm_local 3 | build 4 | release 5 | vendor 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gvm_local 2 | build 3 | release 4 | vendor 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [v7.4.0]() - 2021-09-22 5 | ### Fixed 6 | - Minor code styling changes 7 | 8 | ### Added 9 | - Support Go template in -tags flag 10 | - Custom Go template functions: 11 | - `strSlice` 12 | - `sIndex` 13 | - `mIndex` 14 | - `toUpper` 15 | - `toLower` 16 | - `replace` 17 | - `join` 18 | - `split` 19 | - `splitIndex` 20 | - `matchFirstElement` 21 | - `matchAllElements` 22 | - `httpGet` 23 | - `jsonParse` 24 | - Prebuilt Docker images available on [DockerHub](https://hub.docker.com/r/psyhomb/registrator/tags) 25 | 26 | ### Removed 27 | 28 | ### Changed 29 | - [Migrated to Go modules for managing dependencies](https://go.dev/blog/migrating-to-go-modules) 30 | - Upgraded Docker base images: 31 | - builder: `golang:1.9.4-alpine3.7` => `golang:1.17.1-alpine3.14` 32 | - runtime: `alpine:3.7` => `alpine:3.14` 33 | 34 | ## [v7] - 2016-03-05 35 | ### Fixed 36 | - Providing a SERVICE_NAME for a container with multiple ports exposed would cause services to overwrite each other 37 | - dd3ab2e Fix specific port names not overriding port suffix 38 | 39 | ### Added 40 | - bridge.Ping - calls adapter.Ping 41 | - Consul TCP Health Check 42 | - Support for Consul unix sockets 43 | - Basic Zookeper backend 44 | - Support for Docker multi host networking 45 | - Default to tcp for PortType if not provided 46 | - Sync etcd cluster on service registration 47 | - Support hostip for overlay network 48 | - Cleanup dangling services 49 | - Startup backend service connection retry 50 | 51 | ### Removed 52 | 53 | ### Changed 54 | - Upgraded base image to alpine:3.2 and go 1.4 55 | - bridge.New returns an error instead of calling log.Fatal 56 | - bridge.New will not attempt to ping an adapter. 57 | - Specifying a SERVICE_NAME for containers exposing multiple ports will now result in a named service per port. #194 58 | - Etcd uses port 2379 instead of 4001 #340 59 | - Setup Docker client from environment 60 | - Use exit status to determine if container was killed 61 | 62 | ## [v6] - 2015-08-07 63 | ### Fixed 64 | - Support for etcd v0 and v2 65 | - Panic from invalid skydns2 URI. 66 | 67 | ### Added 68 | - Basic zookeeper adapter 69 | - Optional periodic resyncing of services from containers 70 | - More error logging for registries 71 | - Support for services on containers with `--net=host` 72 | - Added `extensions.go` file for adding/disabling components 73 | - Interpolate SERVICE_PORT and SERVICE_IP in SERVICE_X_CHECK_SCRIPT 74 | - Ability to force IP for a service in Consul 75 | - Implemented initial ping for every service registry 76 | - Option to only deregister containers cleanly shutdown #113 77 | - Added support for label metadata along with environment variables 78 | 79 | ### Removed 80 | 81 | ### Changed 82 | - Overall refactoring and cleanup 83 | - Decoupled registries into subpackages using extpoints 84 | - Replaced check-http script with Consul's native HTTP checks 85 | 86 | 87 | ## [v5] - 2015-02-18 88 | ### Added 89 | - Automated, PR-driven release process 90 | - Development Dockerfile and make task 91 | - CircleCI config with artifacts for every build 92 | - `--version` flag to see version 93 | 94 | ### Changed 95 | - Base container is now Alpine 96 | - Built entirely in Docker 97 | - Moved to gliderlabs organization 98 | - New versioning scheme 99 | - Release artifact now saved container image 100 | 101 | ### Removed 102 | - Dropped unnecessary layers in Dockerfile 103 | - Dropped Godeps for now 104 | 105 | 106 | [unreleased]: https://github.com/gliderlabs/registrator/compare/v7...HEAD 107 | [v7]: https://github.com/gliderlabs/registrator/compare/v6...v7 108 | [v6]: https://github.com/gliderlabs/registrator/compare/v5...v6 109 | [v5]: https://github.com/gliderlabs/registrator/compare/v0.4.0...v5 110 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17.1-alpine3.14 AS builder 2 | WORKDIR /go/src/github.com/gliderlabs/registrator/ 3 | COPY . . 4 | RUN \ 5 | apk add --no-cache git \ 6 | && CGO_ENABLED=0 GOOS=linux go build \ 7 | -a -installsuffix cgo \ 8 | -ldflags "-X main.Version=$(cat VERSION)" \ 9 | -o bin/registrator \ 10 | . 11 | 12 | FROM alpine:3.14 13 | RUN apk add --no-cache ca-certificates 14 | COPY --from=builder /go/src/github.com/gliderlabs/registrator/bin/registrator /bin/registrator 15 | 16 | ENTRYPOINT ["/bin/registrator"] 17 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | CMD ["/bin/registrator"] 3 | 4 | COPY . /go/src/github.com/gliderlabs/registrator 5 | RUN apk --no-cache add -t build-deps build-base go git curl \ 6 | && apk --no-cache add ca-certificates \ 7 | && export GOPATH=/go && mkdir -p /go/bin && export PATH=$PATH:/go/bin \ 8 | && curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh \ 9 | && cd /go/src/github.com/gliderlabs/registrator \ 10 | && export GOPATH=/go \ 11 | && git config --global http.https://gopkg.in.followRedirects true \ 12 | && dep ensure \ 13 | && go build -ldflags "-X main.Version=dev" -o /bin/registrator 14 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - What version of docker are you running? 2 | - What version of registrator are you running? 3 | - Did you build a custom version of registrator? If so, what is that image? 4 | - What is the exact command you are running registrator with? 5 | - What is the exact command you are running your container with? 6 | - A log capture of all the docker events before, during, and after the issue. 7 | - If relevant, `Dockerfile` for application that is having issues. 8 | 9 | Description of the problem: 10 | 11 | How reproducible: 12 | 13 | Steps to Reproduce: 14 | 15 | Actual Results: 16 | 17 | Expected Results: 18 | 19 | Additional info: 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Glider Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=registrator 2 | VERSION=$(shell cat VERSION) 3 | DEV_RUN_OPTS ?= consul: 4 | 5 | dev: 6 | docker build -f Dockerfile.dev -t $(NAME):dev . 7 | docker run --rm \ 8 | -v /var/run/docker.sock:/tmp/docker.sock \ 9 | $(NAME):dev /bin/registrator $(DEV_RUN_OPTS) 10 | 11 | build: 12 | mkdir -p build 13 | docker build -t $(NAME):$(VERSION) . 14 | docker save $(NAME):$(VERSION) | gzip -9 > build/$(NAME)_$(VERSION).tgz 15 | 16 | release: 17 | rm -rf release && mkdir release 18 | go get github.com/progrium/gh-release/... 19 | cp build/* release 20 | gh-release create gliderlabs/$(NAME) $(VERSION) \ 21 | $(shell git rev-parse --abbrev-ref HEAD) $(VERSION) 22 | 23 | docs: 24 | boot2docker ssh "sync; sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'" || true 25 | docker run --rm -it -p 8000:8000 -v $(PWD):/work gliderlabs/pagebuilder mkdocs serve 26 | 27 | circleci: 28 | rm ~/.gitconfig 29 | ifneq ($(CIRCLE_BRANCH), release) 30 | echo build-$$CIRCLE_BUILD_NUM > VERSION 31 | endif 32 | 33 | .PHONY: build release docs 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Registrator 2 | 3 | Service registry bridge for Docker. 4 | 5 | [![Circle CI](https://circleci.com/gh/gliderlabs/registrator.png?style=shield)](https://circleci.com/gh/gliderlabs/registrator) 6 | [![Docker pulls](https://img.shields.io/docker/pulls/gliderlabs/registrator.svg)](https://hub.docker.com/r/gliderlabs/registrator/) 7 | [![IRC Channel](https://img.shields.io/badge/irc-%23gliderlabs-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/#gliderlabs) 8 |

9 | 10 | Registrator automatically registers and deregisters services for any Docker 11 | container by inspecting containers as they come online. Registrator 12 | supports pluggable service registries, which currently includes 13 | [Consul](http://www.consul.io/), [etcd](https://github.com/coreos/etcd) and 14 | [SkyDNS 2](https://github.com/skynetservices/skydns/). 15 | 16 | Full documentation available at http://gliderlabs.com/registrator 17 | 18 | ## Getting Registrator 19 | 20 | Get the latest release, master, or any version of Registrator via [Docker Hub](https://registry.hub.docker.com/u/gliderlabs/registrator/): 21 | 22 | $ docker pull gliderlabs/registrator:latest 23 | 24 | Latest tag always points to the latest release. There is also a `:master` tag 25 | and version tags to pin to specific releases. 26 | 27 | ## Using Registrator 28 | 29 | The quickest way to see Registrator in action is our 30 | [Quickstart](https://gliderlabs.com/registrator/latest/user/quickstart) 31 | tutorial. Otherwise, jump to the [Run 32 | Reference](https://gliderlabs.com/registrator/latest/user/run) in the User 33 | Guide. Typically, running Registrator looks like this: 34 | 35 | $ docker run -d \ 36 | --name=registrator \ 37 | --net=host \ 38 | --volume=/var/run/docker.sock:/tmp/docker.sock \ 39 | gliderlabs/registrator:latest \ 40 | consul://localhost:8500 41 | 42 | ## CLI Options 43 | ``` 44 | Usage of /bin/registrator: 45 | /bin/registrator [options] 46 | 47 | -cleanup=false: Remove dangling services 48 | -deregister="always": Deregister exited services "always" or "on-success" 49 | -explicit=false: Only register containers which have SERVICE_NAME label set 50 | -internal=false: Use internal ports instead of published ones 51 | -ip="": IP for ports mapped to the host 52 | -resync=0: Frequency with which services are resynchronized 53 | -retry-attempts=0: Max retry attempts to establish a connection with the backend. Use -1 for infinite retries 54 | -retry-interval=2000: Interval (in millisecond) between retry-attempts. 55 | -tags="": Append tags for all registered services (supports Go template) 56 | -ttl=0: TTL for services (default is no expiry) 57 | -ttl-refresh=0: Frequency with which service TTLs are refreshed 58 | ``` 59 | 60 | ## Contributing 61 | 62 | Pull requests are welcome! We recommend getting feedback before starting by 63 | opening a [GitHub issue](https://github.com/gliderlabs/registrator/issues) or 64 | discussing in [Slack](http://glider-slackin.herokuapp.com/). 65 | 66 | Also check out our Developer Guide on [Contributing 67 | Backends](https://gliderlabs.com/registrator/latest/dev/backends) and [Staging 68 | Releases](https://gliderlabs.com/registrator/latest/dev/releases). 69 | 70 | ## Sponsors and Thanks 71 | 72 | Big thanks to Weave for sponsoring, Michael Crosby for 73 | [skydock](https://github.com/crosbymichael/skydock), and the Consul mailing list 74 | for inspiration. 75 | 76 | For a full list of sponsors, see 77 | [SPONSORS](https://github.com/gliderlabs/registrator/blob/master/SPONSORS). 78 | 79 | ## License 80 | 81 | MIT 82 | 83 | 84 | -------------------------------------------------------------------------------- /SPONSORS: -------------------------------------------------------------------------------- 1 | DigitalOcean http://digitalocean.com 2 | Weaveworks http://weave.works 3 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v7.4.0 2 | -------------------------------------------------------------------------------- /bridge/bridge.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "path" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "sync" 17 | "text/template" 18 | "time" 19 | 20 | jsonp "github.com/buger/jsonparser" 21 | dockerapi "github.com/fsouza/go-dockerclient" 22 | ) 23 | 24 | var serviceIDPattern = regexp.MustCompile(`^(.+?):([a-zA-Z0-9][a-zA-Z0-9_.-]+):[0-9]+(?::udp)?$`) 25 | 26 | type Bridge struct { 27 | sync.Mutex 28 | registry RegistryAdapter 29 | docker *dockerapi.Client 30 | services map[string][]*Service 31 | deadContainers map[string]*DeadContainer 32 | config Config 33 | } 34 | 35 | func New(docker *dockerapi.Client, adapterUri string, config Config) (*Bridge, error) { 36 | uri, err := url.Parse(adapterUri) 37 | if err != nil { 38 | return nil, errors.New("bad adapter uri: " + adapterUri) 39 | } 40 | factory, found := AdapterFactories.Lookup(uri.Scheme) 41 | if !found { 42 | return nil, errors.New("unrecognized adapter: " + adapterUri) 43 | } 44 | 45 | log.Println("Using", uri.Scheme, "adapter:", uri) 46 | return &Bridge{ 47 | docker: docker, 48 | config: config, 49 | registry: factory.New(uri), 50 | services: make(map[string][]*Service), 51 | deadContainers: make(map[string]*DeadContainer), 52 | }, nil 53 | } 54 | 55 | func (b *Bridge) Ping() error { 56 | return b.registry.Ping() 57 | } 58 | 59 | func (b *Bridge) Add(containerId string) { 60 | b.Lock() 61 | defer b.Unlock() 62 | b.add(containerId, false) 63 | } 64 | 65 | func (b *Bridge) Remove(containerId string) { 66 | b.remove(containerId, true) 67 | } 68 | 69 | func (b *Bridge) RemoveOnExit(containerId string) { 70 | b.remove(containerId, b.shouldRemove(containerId)) 71 | } 72 | 73 | func (b *Bridge) Refresh() { 74 | b.Lock() 75 | defer b.Unlock() 76 | 77 | for containerId, deadContainer := range b.deadContainers { 78 | deadContainer.TTL -= b.config.RefreshInterval 79 | if deadContainer.TTL <= 0 { 80 | delete(b.deadContainers, containerId) 81 | } 82 | } 83 | 84 | for containerId, services := range b.services { 85 | for _, service := range services { 86 | err := b.registry.Refresh(service) 87 | if err != nil { 88 | log.Println("refresh failed:", service.ID, err) 89 | continue 90 | } 91 | log.Println("refreshed:", containerId[:12], service.ID) 92 | } 93 | } 94 | } 95 | 96 | func (b *Bridge) Sync(quiet bool) { 97 | b.Lock() 98 | defer b.Unlock() 99 | 100 | containers, err := b.docker.ListContainers(dockerapi.ListContainersOptions{}) 101 | if err != nil && quiet { 102 | log.Println("error listing containers, skipping sync") 103 | return 104 | } else if err != nil && !quiet { 105 | log.Fatal(err) 106 | } 107 | 108 | log.Printf("Syncing services on %d containers", len(containers)) 109 | 110 | // NOTE: This assumes reregistering will do the right thing, i.e. nothing.. 111 | for _, listing := range containers { 112 | services := b.services[listing.ID] 113 | if services == nil { 114 | b.add(listing.ID, quiet) 115 | } else { 116 | for _, service := range services { 117 | err := b.registry.Register(service) 118 | if err != nil { 119 | log.Println("sync register failed:", service, err) 120 | } 121 | } 122 | } 123 | } 124 | 125 | // Clean up services that were registered previously, but aren't 126 | // acknowledged within registrator 127 | if b.config.Cleanup { 128 | // Remove services if its corresponding container is not running 129 | log.Println("Listing non-exited containers") 130 | filters := map[string][]string{"status": {"created", "restarting", "running", "paused"}} 131 | nonExitedContainers, err := b.docker.ListContainers(dockerapi.ListContainersOptions{Filters: filters}) 132 | if err != nil { 133 | log.Println("error listing nonExitedContainers, skipping sync", err) 134 | return 135 | } 136 | for listingId := range b.services { 137 | found := false 138 | for _, container := range nonExitedContainers { 139 | if listingId == container.ID { 140 | found = true 141 | break 142 | } 143 | } 144 | // This is a container that does not exist 145 | if !found { 146 | log.Printf("stale: Removing service %s because it does not exist", listingId) 147 | go b.RemoveOnExit(listingId) 148 | } 149 | } 150 | 151 | log.Println("Cleaning up dangling services") 152 | extServices, err := b.registry.Services() 153 | if err != nil { 154 | log.Println("cleanup failed:", err) 155 | return 156 | } 157 | 158 | Outer: 159 | for _, extService := range extServices { 160 | matches := serviceIDPattern.FindStringSubmatch(extService.ID) 161 | if len(matches) != 3 { 162 | // There's no way this was registered by us, so leave it 163 | continue 164 | } 165 | serviceHostname := matches[1] 166 | if serviceHostname != Hostname { 167 | // ignore because registered on a different host 168 | continue 169 | } 170 | serviceContainerName := matches[2] 171 | for _, listing := range b.services { 172 | for _, service := range listing { 173 | if service.Name == extService.Name && serviceContainerName == service.Origin.container.Name[1:] { 174 | continue Outer 175 | } 176 | } 177 | } 178 | log.Println("dangling:", extService.ID) 179 | err := b.registry.Deregister(extService) 180 | if err != nil { 181 | log.Println("deregister failed:", extService.ID, err) 182 | continue 183 | } 184 | log.Println(extService.ID, "removed") 185 | } 186 | } 187 | } 188 | 189 | func (b *Bridge) add(containerId string, quiet bool) { 190 | if d := b.deadContainers[containerId]; d != nil { 191 | b.services[containerId] = d.Services 192 | delete(b.deadContainers, containerId) 193 | } 194 | 195 | if b.services[containerId] != nil { 196 | log.Println("container, ", containerId[:12], ", already exists, ignoring") 197 | // Alternatively, remove and readd or resubmit. 198 | return 199 | } 200 | 201 | container, err := b.docker.InspectContainer(containerId) 202 | if err != nil { 203 | log.Println("unable to inspect container:", containerId[:12], err) 204 | return 205 | } 206 | 207 | ports := make(map[string]ServicePort) 208 | 209 | // Extract configured host port mappings, relevant when using --net=host 210 | for port := range container.Config.ExposedPorts { 211 | published := []dockerapi.PortBinding{{HostIP: "0.0.0.0", HostPort: port.Port()}} 212 | ports[string(port)] = servicePort(container, port, published) 213 | } 214 | 215 | // Extract runtime port mappings, relevant when using --net=bridge 216 | for port, published := range container.NetworkSettings.Ports { 217 | ports[string(port)] = servicePort(container, port, published) 218 | } 219 | 220 | if len(ports) == 0 && !quiet { 221 | log.Println("ignored:", container.ID[:12], "no published ports") 222 | return 223 | } 224 | 225 | servicePorts := make(map[string]ServicePort) 226 | for key, port := range ports { 227 | if !b.config.Internal && port.HostPort == "" { 228 | if !quiet { 229 | log.Println("ignored:", container.ID[:12], "port", port.ExposedPort, "not published on host") 230 | } 231 | continue 232 | } 233 | servicePorts[key] = port 234 | } 235 | 236 | isGroup := len(servicePorts) > 1 237 | for _, port := range servicePorts { 238 | service := b.newService(port, isGroup) 239 | if service == nil { 240 | if !quiet { 241 | log.Println("ignored:", container.ID[:12], "service on port", port.ExposedPort) 242 | } 243 | continue 244 | } 245 | err := b.registry.Register(service) 246 | if err != nil { 247 | log.Println("register failed:", service, err) 248 | continue 249 | } 250 | b.services[container.ID] = append(b.services[container.ID], service) 251 | log.Println("added:", container.ID[:12], service.ID) 252 | } 253 | } 254 | 255 | func (b *Bridge) newService(port ServicePort, isgroup bool) *Service { 256 | container := port.container 257 | defaultName := strings.Split(path.Base(container.Config.Image), ":")[0] 258 | 259 | // not sure about this logic. kind of want to remove it. 260 | hostname := Hostname 261 | if hostname == "" { 262 | hostname = port.HostIP 263 | } 264 | if port.HostIP == "0.0.0.0" { 265 | ip, err := net.ResolveIPAddr("ip", hostname) 266 | if err == nil { 267 | port.HostIP = ip.String() 268 | } 269 | } 270 | 271 | if b.config.HostIp != "" { 272 | port.HostIP = b.config.HostIp 273 | } 274 | 275 | metadata, metadataFromPort := serviceMetaData(container.Config, port.ExposedPort) 276 | 277 | ignore := mapDefault(metadata, "ignore", "") 278 | if ignore != "" { 279 | return nil 280 | } 281 | 282 | serviceName := mapDefault(metadata, "name", "") 283 | if serviceName == "" { 284 | if b.config.Explicit { 285 | return nil 286 | } 287 | serviceName = defaultName 288 | } 289 | 290 | service := new(Service) 291 | service.Origin = port 292 | service.ID = hostname + ":" + container.Name[1:] + ":" + port.ExposedPort 293 | service.Name = serviceName 294 | if isgroup && !metadataFromPort["name"] { 295 | service.Name += "-" + port.ExposedPort 296 | } 297 | var p int 298 | 299 | if b.config.Internal { 300 | service.IP = port.ExposedIP 301 | p, _ = strconv.Atoi(port.ExposedPort) 302 | } else { 303 | service.IP = port.HostIP 304 | p, _ = strconv.Atoi(port.HostPort) 305 | } 306 | service.Port = p 307 | 308 | if b.config.UseIpFromLabel != "" { 309 | containerIp := container.Config.Labels[b.config.UseIpFromLabel] 310 | if containerIp != "" { 311 | slashIndex := strings.LastIndex(containerIp, "/") 312 | if slashIndex > -1 { 313 | service.IP = containerIp[:slashIndex] 314 | } else { 315 | service.IP = containerIp 316 | } 317 | log.Println("using container IP " + service.IP + " from label '" + 318 | b.config.UseIpFromLabel + "'") 319 | } else { 320 | log.Println("Label '" + b.config.UseIpFromLabel + 321 | "' not found in container configuration") 322 | } 323 | } 324 | 325 | // NetworkMode can point to another container (kuberenetes pods) 326 | networkMode := container.HostConfig.NetworkMode 327 | if networkMode != "" { 328 | if strings.HasPrefix(networkMode, "container:") { 329 | networkContainerId := strings.Split(networkMode, ":")[1] 330 | log.Println(service.Name + ": detected container NetworkMode, linked to: " + networkContainerId[:12]) 331 | networkContainer, err := b.docker.InspectContainer(networkContainerId) 332 | if err != nil { 333 | log.Println("unable to inspect network container:", networkContainerId[:12], err) 334 | } else { 335 | service.IP = networkContainer.NetworkSettings.IPAddress 336 | log.Println(service.Name + ": using network container IP " + service.IP) 337 | } 338 | } 339 | } 340 | 341 | // Use container inspect data to populate tags list 342 | // https://github.com/fsouza/go-dockerclient/blob/master/container.go#L441-L483 343 | ForceTags := b.config.ForceTags 344 | if len(ForceTags) != 0 { 345 | // Template functions 346 | fm := template.FuncMap{ 347 | // Template function name: strSlice 348 | // Description: Slice string from start to end (same as s[start:end] where s represents string). 349 | // 350 | // Usage: strSlice s start end 351 | // 352 | // Example: strSlice .ID 0 12 353 | // { 354 | // "Id": "e20f9c1a76565d62ae24a3bb877b17b862b6eab94f4e95a0e07ccf25087aaf4f" 355 | // } 356 | // Output: "e20f9c1a7656" 357 | // 358 | "strSlice": func(v string, i ...int) string { 359 | if len(i) == 1 { 360 | if len(v) >= i[0] { 361 | return v[i[0]:] 362 | } 363 | } 364 | 365 | if len(i) == 2 { 366 | if len(v) >= i[0] && len(v) >= i[1] { 367 | if i[0] == 0 { 368 | return v[:i[1]] 369 | } 370 | if i[1] < i[0] { 371 | return v[i[0]:] 372 | } 373 | return v[i[0]:i[1]] 374 | } 375 | } 376 | 377 | return v 378 | }, 379 | // Template function name: sIndex 380 | // Description: Return element from slice or array s by specifiying index i (same as s[i] where s represents slice or array - index i can also take negative values to extract elements in reverse order). 381 | // 382 | // Usage: sIndex i s 383 | // 384 | // Example: sIndex 0 .Config.Env 385 | // { 386 | // "Config": { 387 | // "Env": [ 388 | // "ENVIRONMENT=test", 389 | // "SERVICE_8105_NAME=foo", 390 | // "HOME=/home/foobar", 391 | // "SERVICE_9404_NAME=bar" 392 | // ] 393 | // } 394 | // } 395 | // Output: "ENVIRONMENT=test" 396 | // 397 | "sIndex": func(i int, s []string) string { 398 | if i < 0 { 399 | i = i * -1 400 | if i >= len(s) { 401 | return s[0] 402 | } 403 | return s[len(s)-i] 404 | } 405 | 406 | if i >= len(s) { 407 | return s[len(s)-1] 408 | } 409 | 410 | return s[i] 411 | }, 412 | // Template function name: mIndex 413 | // Description: Return value for key k stored in the map m (same as m["k"]). 414 | // 415 | // Usage: mIndex k m 416 | // 417 | // Example: mIndex "com.amazonaws.ecs.task-arn" .Config.Labels 418 | // { 419 | // "Config": { 420 | // "Labels": { 421 | // "com.amazonaws.ecs.task-arn": "arn:aws:ecs:region:xxxxxxxxxxxx:task/368f4403-0ee4-4f4c-b7a5-be50c57db5cf" 422 | // } 423 | // } 424 | // } 425 | // Output: "arn:aws:ecs:region:xxxxxxxxxxxx:task/368f4403-0ee4-4f4c-b7a5-be50c57db5cf" 426 | // 427 | "mIndex": func(k string, m map[string]string) string { 428 | return m[k] 429 | }, 430 | // Template function name: toUpper 431 | // Description: Return s with all letters mapped to their upper case. 432 | // 433 | // Usage: toUpper s 434 | // 435 | // Example: toUpper "foo" 436 | // Output: "FOO" 437 | // 438 | "toUpper": func(v string) string { 439 | return strings.ToUpper(v) 440 | }, 441 | // Template function name: toLower 442 | // Description: Return s with all letters mapped to their lower case. 443 | // 444 | // Usage: toLower s 445 | // 446 | // Example: toLower "FoO" 447 | // Output: "foo" 448 | // 449 | "toLower": func(v string) string { 450 | return strings.ToLower(v) 451 | }, 452 | // Template function name: replace 453 | // Description: Replace all (-1) or first n occurrences of "old" with "new" found in the designated string s. 454 | // 455 | // Usage: replace n old new s 456 | // 457 | // Example: replace -1 "=" "" "=foo=" 458 | // Output: "foo" 459 | // 460 | "replace": func(n int, old, new, v string) string { 461 | return strings.Replace(v, old, new, n) 462 | }, 463 | // Template function name: join 464 | // Description: Create a single string from all the elements found in the slice s where sep will be used as separator. 465 | // 466 | // Usage: join sep s 467 | // 468 | // Example: join "," .Config.Env 469 | // { 470 | // "Config": { 471 | // "Env": [ 472 | // "ENVIRONMENT=test", 473 | // "SERVICE_8105_NAME=foo", 474 | // "HOME=/home/foobar", 475 | // "SERVICE_9404_NAME=bar" 476 | // ] 477 | // } 478 | // } 479 | // Output: "ENVIRONMENT=test,SERVICE_8105_NAME=foo,HOME=/home/foobar,SERVICE_9404_NAME=bar" 480 | // 481 | "join": func(sep string, s []string) string { 482 | return strings.Join(s, sep) 483 | }, 484 | // Template function name: split 485 | // Description: Split string s into all substrings separated by sep and return a slice of the substrings between those separators. 486 | // 487 | // Usage: split sep s 488 | // 489 | // Example: split "," "/proc/bus,/proc/fs,/proc/irq" 490 | // Output: [/proc/bus /proc/fs /proc/irq] 491 | // 492 | "split": func(sep, v string) []string { 493 | return strings.Split(v, sep) 494 | }, 495 | // Template function name: splitIndex 496 | // Description: split and sIndex function combined, index i can also take negative values to extract elements in reverse order. 497 | // Same result can be achieved if using pipeline with both functions: {{ split sep s | sIndex i }} 498 | // 499 | // Usage: splitIndex i sep s 500 | // 501 | // Example: splitIndex -1 "/" "arn:aws:ecs:region:xxxxxxxxxxxx:task/368f4403-0ee4-4f4c-b7a5-be50c57db5cf" 502 | // Output: "368f4403-0ee4-4f4c-b7a5-be50c57db5cf" 503 | // 504 | "splitIndex": func(i int, sep, v string) string { 505 | l := strings.Split(v, sep) 506 | 507 | if i < 0 { 508 | i = i * -1 509 | if i >= len(l) { 510 | return l[0] 511 | } 512 | return l[len(l)-i] 513 | } 514 | 515 | if i >= len(l) { 516 | return l[len(l)-1] 517 | } 518 | 519 | return l[i] 520 | }, 521 | // Template function name: matchFirstElement 522 | // Description: Iterate through slice s and return first element that match regex expression. 523 | // 524 | // Usage: matchFirstElement regex s 525 | // 526 | // Example: matchFirstElement "^SERVICE_" .Config.Env 527 | // { 528 | // "Config": { 529 | // "Env": [ 530 | // "ENVIRONMENT=test", 531 | // "SERVICE_8105_NAME=foo", 532 | // "HOME=/home/foobar", 533 | // "SERVICE_9404_NAME=bar" 534 | // ] 535 | // } 536 | // } 537 | // Output: "SERVICE_8105_NAME=foo" 538 | // 539 | "matchFirstElement": func(r string, s []string) string { 540 | var m string 541 | 542 | re := regexp.MustCompile(r) 543 | for _, e := range s { 544 | if re.MatchString(e) { 545 | m = e 546 | break 547 | } 548 | } 549 | 550 | return m 551 | }, 552 | // Template function name: matchAllElements 553 | // Description: Iterate through slice s and return slice of all elements that match regex expression. 554 | // 555 | // Usage: matchAllElements regex s 556 | // 557 | // Example: matchAllElements "^SERVICE_" .Config.Env 558 | // { 559 | // "Config": { 560 | // "Env": [ 561 | // "ENVIRONMENT=test", 562 | // "SERVICE_8105_NAME=foo", 563 | // "HOME=/home/foobar", 564 | // "SERVICE_9404_NAME=bar" 565 | // ] 566 | // } 567 | // } 568 | // Output: [SERVICE_8105_NAME=foo SERVICE_9404_NAME=bar] 569 | // 570 | "matchAllElements": func(r string, s []string) []string { 571 | var m []string 572 | 573 | re := regexp.MustCompile(r) 574 | for _, e := range s { 575 | if re.MatchString(e) { 576 | m = append(m, e) 577 | } 578 | } 579 | 580 | return m 581 | }, 582 | // Template function name: httpGet 583 | // Description: Fetch an object from URL. 584 | // 585 | // Usage: httpGet url 586 | // 587 | // Example: httpGet "https://ajpi.me/all" 588 | // Output: []byte (e.g. JSON object) 589 | // 590 | "httpGet": func(url string) []byte { 591 | // HTTP client configuration 592 | c := &http.Client{ 593 | Timeout: 10 * time.Second, 594 | } 595 | 596 | res, err := c.Get(url) 597 | if err != nil { 598 | log.Printf("httpGet template function encountered an error while executing HTTP request: %v", err) 599 | return []byte("") 600 | } 601 | 602 | body, err := ioutil.ReadAll(res.Body) 603 | res.Body.Close() 604 | if err != nil { 605 | log.Printf("httpGet template function encountered an error while reading HTTP body payload: %v", err) 606 | return []byte("") 607 | } 608 | 609 | return body 610 | }, 611 | // Template function name: jsonParse 612 | // Description: Extract value from JSON object by specifying exact path (nested objects). Keys in path has to be separated with double colon sign. 613 | // 614 | // Usage: jsonParse b key1::key2::key3::keyN 615 | // 616 | // Example: jsonParse b "Additional::Country" 617 | // { 618 | // "Additional": { 619 | // "Country": "United States" 620 | // } 621 | // } 622 | // Output: "United States" 623 | // 624 | "jsonParse": func(b []byte, k string) string { 625 | var ( 626 | keys []string 627 | js []byte 628 | err error 629 | ) 630 | 631 | keys = strings.Split(k, "::") 632 | 633 | js, _, _, err = jsonp.Get(b, keys...) 634 | if err != nil { 635 | log.Printf("jsonParse template function encountered an error while parsing JSON object %v: %v", keys, err) 636 | } 637 | 638 | return string(js) 639 | }, 640 | } 641 | 642 | tmpl, err := template.New("tags").Funcs(fm).Parse(ForceTags) 643 | if err != nil { 644 | log.Fatalf("%s template parsing failed with error: %s", ForceTags, err) 645 | } 646 | 647 | var b bytes.Buffer 648 | err = tmpl.Execute(&b, container) 649 | if err != nil { 650 | log.Fatalf("%s template execution failed with error: %s", ForceTags, err) 651 | } 652 | 653 | ForceTags = b.String() 654 | } 655 | 656 | if port.PortType == "udp" { 657 | service.Tags = combineTags( 658 | mapDefault(metadata, "tags", ""), ForceTags, "udp") 659 | service.ID = service.ID + ":udp" 660 | } else { 661 | service.Tags = combineTags( 662 | mapDefault(metadata, "tags", ""), ForceTags) 663 | } 664 | 665 | id := mapDefault(metadata, "id", "") 666 | if id != "" { 667 | service.ID = id 668 | } 669 | 670 | delete(metadata, "id") 671 | delete(metadata, "tags") 672 | delete(metadata, "name") 673 | service.Attrs = metadata 674 | service.TTL = b.config.RefreshTtl 675 | 676 | return service 677 | } 678 | 679 | func (b *Bridge) remove(containerId string, deregister bool) { 680 | b.Lock() 681 | defer b.Unlock() 682 | 683 | if deregister { 684 | deregisterAll := func(services []*Service) { 685 | for _, service := range services { 686 | err := b.registry.Deregister(service) 687 | if err != nil { 688 | log.Println("deregister failed:", service.ID, err) 689 | continue 690 | } 691 | log.Println("removed:", containerId[:12], service.ID) 692 | } 693 | } 694 | deregisterAll(b.services[containerId]) 695 | if d := b.deadContainers[containerId]; d != nil { 696 | deregisterAll(d.Services) 697 | delete(b.deadContainers, containerId) 698 | } 699 | } else if b.config.RefreshTtl != 0 && b.services[containerId] != nil { 700 | // need to stop the refreshing, but can't delete it yet 701 | b.deadContainers[containerId] = &DeadContainer{b.config.RefreshTtl, b.services[containerId]} 702 | } 703 | delete(b.services, containerId) 704 | } 705 | 706 | // bit set on ExitCode if it represents an exit via a signal 707 | var dockerSignaledBit = 128 708 | 709 | func (b *Bridge) shouldRemove(containerId string) bool { 710 | if b.config.DeregisterCheck == "always" { 711 | return true 712 | } 713 | container, err := b.docker.InspectContainer(containerId) 714 | if _, ok := err.(*dockerapi.NoSuchContainer); ok { 715 | // the container has already been removed from Docker 716 | // e.g. probabably run with "--rm" to remove immediately 717 | // so its exit code is not accessible 718 | log.Printf("registrator: container %v was removed, could not fetch exit code", containerId[:12]) 719 | return true 720 | } 721 | 722 | switch { 723 | case err != nil: 724 | log.Printf("registrator: error fetching status for container %v on \"die\" event: %v\n", containerId[:12], err) 725 | return false 726 | case container.State.Running: 727 | log.Printf("registrator: not removing container %v, still running", containerId[:12]) 728 | return false 729 | case container.State.ExitCode == 0: 730 | return true 731 | case container.State.ExitCode&dockerSignaledBit == dockerSignaledBit: 732 | return true 733 | } 734 | return false 735 | } 736 | 737 | var Hostname string 738 | 739 | func init() { 740 | // It's ok for Hostname to ultimately be an empty string 741 | // An empty string will fall back to trying to make a best guess 742 | Hostname, _ = os.Hostname() 743 | } 744 | -------------------------------------------------------------------------------- /bridge/bridge_test.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewError(t *testing.T) { 10 | bridge, err := New(nil, "", Config{}) 11 | assert.Nil(t, bridge) 12 | assert.Error(t, err) 13 | } 14 | 15 | func TestNewValid(t *testing.T) { 16 | Register(new(fakeFactory), "fake") 17 | // Note: the following is valid for New() since it does not 18 | // actually connect to docker. 19 | bridge, err := New(nil, "fake://", Config{}) 20 | 21 | assert.NotNil(t, bridge) 22 | assert.NoError(t, err) 23 | } 24 | -------------------------------------------------------------------------------- /bridge/extpoints.go: -------------------------------------------------------------------------------- 1 | // generated by go-extpoints -- DO NOT EDIT 2 | package bridge 3 | 4 | import ( 5 | "reflect" 6 | "sync" 7 | ) 8 | 9 | var registry = struct { 10 | sync.Mutex 11 | extpoints map[string]*extensionPoint 12 | }{ 13 | extpoints: make(map[string]*extensionPoint), 14 | } 15 | 16 | type extensionPoint struct { 17 | sync.Mutex 18 | iface reflect.Type 19 | components map[string]interface{} 20 | } 21 | 22 | func newExtensionPoint(iface interface{}) *extensionPoint { 23 | ep := &extensionPoint{ 24 | iface: reflect.TypeOf(iface).Elem(), 25 | components: make(map[string]interface{}), 26 | } 27 | registry.Lock() 28 | defer registry.Unlock() 29 | registry.extpoints[ep.iface.Name()] = ep 30 | return ep 31 | } 32 | 33 | func (ep *extensionPoint) lookup(name string) (ext interface{}, ok bool) { 34 | ep.Lock() 35 | defer ep.Unlock() 36 | ext, ok = ep.components[name] 37 | return 38 | } 39 | 40 | func (ep *extensionPoint) all() map[string]interface{} { 41 | ep.Lock() 42 | defer ep.Unlock() 43 | all := make(map[string]interface{}) 44 | for k, v := range ep.components { 45 | all[k] = v 46 | } 47 | return all 48 | } 49 | 50 | func (ep *extensionPoint) register(component interface{}, name string) bool { 51 | ep.Lock() 52 | defer ep.Unlock() 53 | if name == "" { 54 | name = reflect.TypeOf(component).Elem().Name() 55 | } 56 | _, exists := ep.components[name] 57 | if exists { 58 | return false 59 | } 60 | ep.components[name] = component 61 | return true 62 | } 63 | 64 | func (ep *extensionPoint) unregister(name string) bool { 65 | ep.Lock() 66 | defer ep.Unlock() 67 | _, exists := ep.components[name] 68 | if !exists { 69 | return false 70 | } 71 | delete(ep.components, name) 72 | return true 73 | } 74 | 75 | func implements(component interface{}) []string { 76 | var ifaces []string 77 | for name, ep := range registry.extpoints { 78 | if reflect.TypeOf(component).Implements(ep.iface) { 79 | ifaces = append(ifaces, name) 80 | } 81 | } 82 | return ifaces 83 | } 84 | 85 | func Register(component interface{}, name string) []string { 86 | registry.Lock() 87 | defer registry.Unlock() 88 | var ifaces []string 89 | for _, iface := range implements(component) { 90 | if ok := registry.extpoints[iface].register(component, name); ok { 91 | ifaces = append(ifaces, iface) 92 | } 93 | } 94 | return ifaces 95 | } 96 | 97 | func Unregister(name string) []string { 98 | registry.Lock() 99 | defer registry.Unlock() 100 | var ifaces []string 101 | for iface, extpoint := range registry.extpoints { 102 | if ok := extpoint.unregister(name); ok { 103 | ifaces = append(ifaces, iface) 104 | } 105 | } 106 | return ifaces 107 | } 108 | 109 | // AdapterFactory 110 | 111 | var AdapterFactories = &adapterFactoryExt{ 112 | newExtensionPoint(new(AdapterFactory)), 113 | } 114 | 115 | type adapterFactoryExt struct { 116 | *extensionPoint 117 | } 118 | 119 | func (ep *adapterFactoryExt) Unregister(name string) bool { 120 | return ep.unregister(name) 121 | } 122 | 123 | func (ep *adapterFactoryExt) Register(component AdapterFactory, name string) bool { 124 | return ep.register(component, name) 125 | } 126 | 127 | func (ep *adapterFactoryExt) Lookup(name string) (AdapterFactory, bool) { 128 | ext, ok := ep.lookup(name) 129 | if !ok { 130 | return nil, ok 131 | } 132 | return ext.(AdapterFactory), ok 133 | } 134 | 135 | func (ep *adapterFactoryExt) All() map[string]AdapterFactory { 136 | all := make(map[string]AdapterFactory) 137 | for k, v := range ep.all() { 138 | all[k] = v.(AdapterFactory) 139 | } 140 | return all 141 | } 142 | -------------------------------------------------------------------------------- /bridge/types.go: -------------------------------------------------------------------------------- 1 | //go:generate go-extpoints . AdapterFactory 2 | package bridge 3 | 4 | import ( 5 | "net/url" 6 | 7 | dockerapi "github.com/fsouza/go-dockerclient" 8 | ) 9 | 10 | type AdapterFactory interface { 11 | New(uri *url.URL) RegistryAdapter 12 | } 13 | 14 | type RegistryAdapter interface { 15 | Ping() error 16 | Register(service *Service) error 17 | Deregister(service *Service) error 18 | Refresh(service *Service) error 19 | Services() ([]*Service, error) 20 | } 21 | 22 | type Config struct { 23 | HostIp string 24 | Internal bool 25 | Explicit bool 26 | UseIpFromLabel string 27 | ForceTags string 28 | RefreshTtl int 29 | RefreshInterval int 30 | DeregisterCheck string 31 | Cleanup bool 32 | } 33 | 34 | type Service struct { 35 | ID string 36 | Name string 37 | Port int 38 | IP string 39 | Tags []string 40 | Attrs map[string]string 41 | TTL int 42 | 43 | Origin ServicePort 44 | } 45 | 46 | type DeadContainer struct { 47 | TTL int 48 | Services []*Service 49 | } 50 | 51 | type ServicePort struct { 52 | HostPort string 53 | HostIP string 54 | ExposedPort string 55 | ExposedIP string 56 | PortType string 57 | ContainerHostname string 58 | ContainerID string 59 | ContainerName string 60 | container *dockerapi.Container 61 | } 62 | -------------------------------------------------------------------------------- /bridge/types_test.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import "net/url" 4 | 5 | type fakeFactory struct{} 6 | 7 | func (f *fakeFactory) New(uri *url.URL) RegistryAdapter { 8 | 9 | return &fakeAdapter{} 10 | } 11 | 12 | type fakeAdapter struct{} 13 | 14 | func (f *fakeAdapter) Ping() error { 15 | return nil 16 | } 17 | func (f *fakeAdapter) Register(service *Service) error { 18 | return nil 19 | } 20 | func (f *fakeAdapter) Deregister(service *Service) error { 21 | return nil 22 | } 23 | func (f *fakeAdapter) Refresh(service *Service) error { 24 | return nil 25 | } 26 | func (f *fakeAdapter) Services() ([]*Service, error) { 27 | return nil, nil 28 | } 29 | -------------------------------------------------------------------------------- /bridge/util.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/cenkalti/backoff" 8 | dockerapi "github.com/fsouza/go-dockerclient" 9 | ) 10 | 11 | func retry(fn func() error) error { 12 | return backoff.Retry(fn, backoff.NewExponentialBackOff()) 13 | } 14 | 15 | func mapDefault(m map[string]string, key, default_ string) string { 16 | v, ok := m[key] 17 | if !ok || v == "" { 18 | return default_ 19 | } 20 | return v 21 | } 22 | 23 | // Golang regexp module does not support /(?!\\),/ syntax for spliting by not escaped comma 24 | // Then this function is reproducing it 25 | func recParseEscapedComma(str string) []string { 26 | if len(str) == 0 { 27 | return []string{} 28 | } else if str[0] == ',' { 29 | return recParseEscapedComma(str[1:]) 30 | } 31 | 32 | offset := 0 33 | for len(str[offset:]) > 0 { 34 | index := strings.Index(str[offset:], ",") 35 | 36 | if index == -1 { 37 | break 38 | } else if str[offset+index-1:offset+index] != "\\" { 39 | return append(recParseEscapedComma(str[offset+index+1:]), str[:offset+index]) 40 | } 41 | 42 | str = str[:offset+index-1] + str[offset+index:] 43 | offset += index 44 | } 45 | 46 | return []string{str} 47 | } 48 | 49 | func combineTags(tagParts ...string) []string { 50 | tags := make([]string, 0) 51 | for _, element := range tagParts { 52 | tags = append(tags, recParseEscapedComma(element)...) 53 | } 54 | return tags 55 | } 56 | 57 | func serviceMetaData(config *dockerapi.Config, port string) (map[string]string, map[string]bool) { 58 | meta := config.Env 59 | for k, v := range config.Labels { 60 | meta = append(meta, k+"="+v) 61 | } 62 | metadata := make(map[string]string) 63 | metadataFromPort := make(map[string]bool) 64 | for _, kv := range meta { 65 | kvp := strings.SplitN(kv, "=", 2) 66 | if strings.HasPrefix(kvp[0], "SERVICE_") && len(kvp) > 1 { 67 | key := strings.ToLower(strings.TrimPrefix(kvp[0], "SERVICE_")) 68 | if metadataFromPort[key] { 69 | continue 70 | } 71 | portkey := strings.SplitN(key, "_", 2) 72 | _, err := strconv.Atoi(portkey[0]) 73 | if err == nil && len(portkey) > 1 { 74 | if portkey[0] != port { 75 | continue 76 | } 77 | metadata[portkey[1]] = kvp[1] 78 | metadataFromPort[portkey[1]] = true 79 | } else { 80 | metadata[key] = kvp[1] 81 | } 82 | } 83 | } 84 | return metadata, metadataFromPort 85 | } 86 | 87 | func servicePort(container *dockerapi.Container, port dockerapi.Port, published []dockerapi.PortBinding) ServicePort { 88 | var hp, hip, ep, ept, eip, nm string 89 | if len(published) > 0 { 90 | hp = published[0].HostPort 91 | hip = published[0].HostIP 92 | } 93 | if hip == "" { 94 | hip = "0.0.0.0" 95 | } 96 | 97 | //for overlay networks 98 | //detect if container use overlay network, than set HostIP into NetworkSettings.Network[string].IPAddress 99 | //better to use registrator with -internal flag 100 | nm = container.HostConfig.NetworkMode 101 | if nm != "bridge" && nm != "default" && nm != "host" { 102 | hip = container.NetworkSettings.Networks[nm].IPAddress 103 | } 104 | 105 | exposedPort := strings.Split(string(port), "/") 106 | ep = exposedPort[0] 107 | if len(exposedPort) == 2 { 108 | ept = exposedPort[1] 109 | } else { 110 | ept = "tcp" // default 111 | } 112 | 113 | // Nir: support docker NetworkSettings 114 | eip = container.NetworkSettings.IPAddress 115 | if eip == "" { 116 | for _, network := range container.NetworkSettings.Networks { 117 | eip = network.IPAddress 118 | } 119 | } 120 | 121 | return ServicePort{ 122 | HostPort: hp, 123 | HostIP: hip, 124 | ExposedPort: ep, 125 | ExposedIP: eip, 126 | PortType: ept, 127 | ContainerID: container.ID, 128 | ContainerHostname: container.Config.Hostname, 129 | container: container, 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /bridge/util_test.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestEscapedComma(t *testing.T) { 11 | cases := []struct { 12 | Tag string 13 | Expected []string 14 | }{ 15 | { 16 | Tag: "", 17 | Expected: []string{}, 18 | }, 19 | { 20 | Tag: "foobar", 21 | Expected: []string{"foobar"}, 22 | }, 23 | { 24 | Tag: "foo,bar", 25 | Expected: []string{"foo", "bar"}, 26 | }, 27 | { 28 | Tag: "foo\\,bar", 29 | Expected: []string{"foo,bar"}, 30 | }, 31 | { 32 | Tag: "foo,bar\\,baz", 33 | Expected: []string{"foo", "bar,baz"}, 34 | }, 35 | { 36 | Tag: "\\,foobar\\,", 37 | Expected: []string{",foobar,"}, 38 | }, 39 | { 40 | Tag: ",,,,foo,,,bar,,,", 41 | Expected: []string{"foo", "bar"}, 42 | }, 43 | { 44 | Tag: ",,,,", 45 | Expected: []string{}, 46 | }, 47 | { 48 | Tag: ",,\\,,", 49 | Expected: []string{","}, 50 | }, 51 | } 52 | 53 | for _, c := range cases { 54 | results := recParseEscapedComma(c.Tag) 55 | sort.Strings(c.Expected) 56 | sort.Strings(results) 57 | assert.EqualValues(t, c.Expected, results) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /consul/consul.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/url" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/gliderlabs/registrator/bridge" 12 | consulapi "github.com/hashicorp/consul/api" 13 | "github.com/hashicorp/go-cleanhttp" 14 | ) 15 | 16 | const DefaultInterval = "10s" 17 | 18 | func init() { 19 | f := new(Factory) 20 | bridge.Register(f, "consul") 21 | bridge.Register(f, "consul-tls") 22 | bridge.Register(f, "consul-unix") 23 | } 24 | 25 | func (r *ConsulAdapter) interpolateService(script string, service *bridge.Service) string { 26 | withIp := strings.Replace(script, "$SERVICE_IP", service.IP, -1) 27 | withPort := strings.Replace(withIp, "$SERVICE_PORT", strconv.Itoa(service.Port), -1) 28 | return withPort 29 | } 30 | 31 | type Factory struct{} 32 | 33 | func (f *Factory) New(uri *url.URL) bridge.RegistryAdapter { 34 | config := consulapi.DefaultConfig() 35 | if uri.Scheme == "consul-unix" { 36 | config.Address = strings.TrimPrefix(uri.String(), "consul-") 37 | } else if uri.Scheme == "consul-tls" { 38 | tlsConfigDesc := &consulapi.TLSConfig{ 39 | Address: uri.Host, 40 | CAFile: os.Getenv("CONSUL_CACERT"), 41 | CertFile: os.Getenv("CONSUL_CLIENT_CERT"), 42 | KeyFile: os.Getenv("CONSUL_CLIENT_KEY"), 43 | InsecureSkipVerify: false, 44 | } 45 | tlsConfig, err := consulapi.SetupTLSConfig(tlsConfigDesc) 46 | if err != nil { 47 | log.Fatal("Cannot set up Consul TLSConfig", err) 48 | } 49 | config.Scheme = "https" 50 | transport := cleanhttp.DefaultPooledTransport() 51 | transport.TLSClientConfig = tlsConfig 52 | config.Transport = transport 53 | config.Address = uri.Host 54 | } else if uri.Host != "" { 55 | config.Address = uri.Host 56 | } 57 | client, err := consulapi.NewClient(config) 58 | if err != nil { 59 | log.Fatal("consul: ", uri.Scheme) 60 | } 61 | return &ConsulAdapter{client: client} 62 | } 63 | 64 | type ConsulAdapter struct { 65 | client *consulapi.Client 66 | } 67 | 68 | // Ping will try to connect to consul by attempting to retrieve the current leader. 69 | func (r *ConsulAdapter) Ping() error { 70 | status := r.client.Status() 71 | leader, err := status.Leader() 72 | if err != nil { 73 | return err 74 | } 75 | log.Println("consul: current leader ", leader) 76 | 77 | return nil 78 | } 79 | 80 | func (r *ConsulAdapter) Register(service *bridge.Service) error { 81 | registration := new(consulapi.AgentServiceRegistration) 82 | registration.ID = service.ID 83 | registration.Name = service.Name 84 | registration.Port = service.Port 85 | registration.Tags = service.Tags 86 | registration.Address = service.IP 87 | registration.Check = r.buildCheck(service) 88 | registration.Meta = service.Attrs 89 | return r.client.Agent().ServiceRegister(registration) 90 | } 91 | 92 | func (r *ConsulAdapter) buildCheck(service *bridge.Service) *consulapi.AgentServiceCheck { 93 | check := new(consulapi.AgentServiceCheck) 94 | if status := service.Attrs["check_initial_status"]; status != "" { 95 | check.Status = status 96 | } 97 | if path := service.Attrs["check_http"]; path != "" { 98 | check.HTTP = fmt.Sprintf("http://%s:%d%s", service.IP, service.Port, path) 99 | if timeout := service.Attrs["check_timeout"]; timeout != "" { 100 | check.Timeout = timeout 101 | } 102 | if method := service.Attrs["check_http_method"]; method != "" { 103 | check.Method = method 104 | } 105 | } else if path := service.Attrs["check_https"]; path != "" { 106 | check.HTTP = fmt.Sprintf("https://%s:%d%s", service.IP, service.Port, path) 107 | if timeout := service.Attrs["check_timeout"]; timeout != "" { 108 | check.Timeout = timeout 109 | } 110 | if method := service.Attrs["check_https_method"]; method != "" { 111 | check.Method = method 112 | } 113 | } else if cmd := service.Attrs["check_cmd"]; cmd != "" { 114 | check.Args = []string{"check-cmd", service.Origin.ContainerID[:12], service.Origin.ExposedPort, cmd} 115 | } else if script := service.Attrs["check_script"]; script != "" { 116 | check.Args = []string{r.interpolateService(script, service)} 117 | } else if ttl := service.Attrs["check_ttl"]; ttl != "" { 118 | check.TTL = ttl 119 | } else if tcp := service.Attrs["check_tcp"]; tcp != "" { 120 | check.TCP = fmt.Sprintf("%s:%d", service.IP, service.Port) 121 | if timeout := service.Attrs["check_timeout"]; timeout != "" { 122 | check.Timeout = timeout 123 | } 124 | } else if grpc := service.Attrs["check_grpc"]; grpc != "" { 125 | check.GRPC = fmt.Sprintf("%s:%d", service.IP, service.Port) 126 | if timeout := service.Attrs["check_timeout"]; timeout != "" { 127 | check.Timeout = timeout 128 | } 129 | if useTLS := service.Attrs["check_grpc_use_tls"]; useTLS != "" { 130 | check.GRPCUseTLS = true 131 | if tlsSkipVerify := service.Attrs["check_tls_skip_verify"]; tlsSkipVerify != "" { 132 | check.TLSSkipVerify = true 133 | } 134 | } 135 | } else { 136 | return nil 137 | } 138 | if len(check.Args) != 0 || check.HTTP != "" || check.TCP != "" || check.GRPC != "" { 139 | if interval := service.Attrs["check_interval"]; interval != "" { 140 | check.Interval = interval 141 | } else { 142 | check.Interval = DefaultInterval 143 | } 144 | } 145 | if deregister_after := service.Attrs["check_deregister_after"]; deregister_after != "" { 146 | check.DeregisterCriticalServiceAfter = deregister_after 147 | } 148 | return check 149 | } 150 | 151 | func (r *ConsulAdapter) Deregister(service *bridge.Service) error { 152 | return r.client.Agent().ServiceDeregister(service.ID) 153 | } 154 | 155 | func (r *ConsulAdapter) Refresh(service *bridge.Service) error { 156 | return nil 157 | } 158 | 159 | func (r *ConsulAdapter) Services() ([]*bridge.Service, error) { 160 | services, err := r.client.Agent().Services() 161 | if err != nil { 162 | return []*bridge.Service{}, err 163 | } 164 | out := make([]*bridge.Service, len(services)) 165 | i := 0 166 | for _, v := range services { 167 | s := &bridge.Service{ 168 | ID: v.ID, 169 | Name: v.Service, 170 | Port: v.Port, 171 | Tags: v.Tags, 172 | IP: v.Address, 173 | } 174 | out[i] = s 175 | i++ 176 | } 177 | return out, nil 178 | } 179 | -------------------------------------------------------------------------------- /consulkv/consulkv.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "net/url" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/gliderlabs/registrator/bridge" 11 | consulapi "github.com/hashicorp/consul/api" 12 | ) 13 | 14 | func init() { 15 | f := new(Factory) 16 | bridge.Register(f, "consulkv") 17 | bridge.Register(f, "consulkv-unix") 18 | } 19 | 20 | type Factory struct{} 21 | 22 | func (f *Factory) New(uri *url.URL) bridge.RegistryAdapter { 23 | config := consulapi.DefaultConfig() 24 | path := uri.Path 25 | if uri.Scheme == "consulkv-unix" { 26 | spl := strings.SplitN(uri.Path, ":", 2) 27 | config.Address, path = "unix://"+spl[0], spl[1] 28 | } else if uri.Host != "" { 29 | config.Address = uri.Host 30 | } 31 | client, err := consulapi.NewClient(config) 32 | if err != nil { 33 | log.Fatal("consulkv: ", uri.Scheme) 34 | } 35 | return &ConsulKVAdapter{client: client, path: path} 36 | } 37 | 38 | type ConsulKVAdapter struct { 39 | client *consulapi.Client 40 | path string 41 | } 42 | 43 | // Ping will try to connect to consul by attempting to retrieve the current leader. 44 | func (r *ConsulKVAdapter) Ping() error { 45 | status := r.client.Status() 46 | leader, err := status.Leader() 47 | if err != nil { 48 | return err 49 | } 50 | log.Println("consulkv: current leader ", leader) 51 | 52 | return nil 53 | } 54 | 55 | func (r *ConsulKVAdapter) Register(service *bridge.Service) error { 56 | log.Println("Register") 57 | path := r.path[1:] + "/" + service.Name + "/" + service.ID 58 | port := strconv.Itoa(service.Port) 59 | addr := net.JoinHostPort(service.IP, port) 60 | log.Printf("path: %s", path) 61 | _, err := r.client.KV().Put(&consulapi.KVPair{Key: path, Value: []byte(addr)}, nil) 62 | if err != nil { 63 | log.Println("consulkv: failed to register service:", err) 64 | } 65 | return err 66 | } 67 | 68 | func (r *ConsulKVAdapter) Deregister(service *bridge.Service) error { 69 | path := r.path[1:] + "/" + service.Name + "/" + service.ID 70 | _, err := r.client.KV().Delete(path, nil) 71 | if err != nil { 72 | log.Println("consulkv: failed to deregister service:", err) 73 | } 74 | return err 75 | } 76 | 77 | func (r *ConsulKVAdapter) Refresh(service *bridge.Service) error { 78 | return nil 79 | } 80 | 81 | func (r *ConsulKVAdapter) Services() ([]*bridge.Service, error) { 82 | return []*bridge.Service{}, nil 83 | } 84 | -------------------------------------------------------------------------------- /docs/dev/backends.md: -------------------------------------------------------------------------------- 1 | # Contributing Backends 2 | 3 | As you can see by either the Consul or etcd source files, writing a new registry backend is easy. Just follow the example set by those two. It boils down to writing an object that implements this interface: 4 | ``` 5 | type RegistryAdapter interface { 6 | Ping() error 7 | Register(service *Service) error 8 | Deregister(service *Service) error 9 | Refresh(service *Service) error 10 | } 11 | ``` 12 | The `Service` struct looks like this: 13 | ``` 14 | type Service struct { 15 | ID string 16 | Name string 17 | Port int 18 | IP string 19 | Tags []string 20 | Attrs map[string]string 21 | TTL int 22 | ... 23 | } 24 | ``` 25 | Then add a factory which accepts a uri and returns the registry adapter, and register that factory with the bridge like `bridge.Register(new(Factory), "")`. 26 | -------------------------------------------------------------------------------- /docs/dev/releases.md: -------------------------------------------------------------------------------- 1 | # Staging Releases 2 | 3 | Don't wait for maintainers to cut a release! You can stage a release at any time 4 | using GitHub. Just open a PR against the release branch from master. If merged, 5 | a new release will automatically be cut. 6 | 7 | Please be sure to bump the version and update CHANGELOG.md and include your 8 | changelog text in the PR body. 9 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Registrator 2 | 3 | Service registry bridge for Docker, sponsored by [Weave](http://weave.works). 4 | 5 | [![Circle CI](https://circleci.com/gh/gliderlabs/registrator.png?style=shield)](https://circleci.com/gh/gliderlabs/registrator) 6 | [![Docker Hub](https://img.shields.io/badge/docker-ready-blue.svg)](https://registry.hub.docker.com/u/gliderlabs/registrator/) 7 | [![ImageLayers Size](https://img.shields.io/imagelayers/image-size/gliderlabs/registrator/latest.svg)](https://imagelayers.io/?images=gliderlabs%2Fregistrator:latest) 8 | [![IRC Channel](https://img.shields.io/badge/irc-%23gliderlabs-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/#gliderlabs) 9 |

10 | 11 | Registrator automatically registers and deregisters services for any Docker 12 | container by inspecting containers as they come online. Registrator 13 | supports pluggable service registries, which currently includes 14 | [Consul](http://www.consul.io/), [etcd](https://github.com/coreos/etcd) and 15 | [SkyDNS 2](https://github.com/skynetservices/skydns/). 16 | 17 | ## Getting Registrator 18 | 19 | Get the latest release, master, or any version of Registrator via [Docker Hub](https://registry.hub.docker.com/u/gliderlabs/registrator/): 20 | 21 | $ docker pull gliderlabs/registrator:latest 22 | 23 | Latest tag always points to the latest release. There is also a `:master` tag 24 | and version tags to pin to specific releases. 25 | 26 | ## Using Registrator 27 | 28 | The quickest way to see Registrator in action is our 29 | [Quickstart](user/quickstart.md) tutorial. Otherwise, jump to the [Run 30 | Reference](user/run.md) in the User Guide. Typically, running Registrator 31 | looks like this: 32 | 33 | $ docker run -d \ 34 | --name=registrator \ 35 | --net=host \ 36 | --volume=/var/run/docker.sock:/tmp/docker.sock \ 37 | gliderlabs/registrator:latest \ 38 | consul://localhost:8500 39 | 40 | ## Contributing 41 | 42 | Pull requests are welcome! We recommend getting feedback before starting by 43 | opening a [GitHub issue](https://github.com/gliderlabs/registrator/issues) or 44 | discussing in [Slack](http://glider-slackin.herokuapp.com/). 45 | 46 | Also check out our Developer Guide on [Contributing Backends](dev/backends.md) 47 | and [Staging Releases](dev/releases.md). 48 | 49 | ## Sponsors and Thanks 50 | 51 | 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 52 | [skydock](https://github.com/crosbymichael/skydock) and the Consul mailing list 53 | for inspiration. 54 | 55 | For a full list of sponsors, see 56 | [SPONSORS](https://github.com/gliderlabs/registrator/blob/master/SPONSORS). 57 | 58 | ## License 59 | 60 | MIT 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/user/backends.md: -------------------------------------------------------------------------------- 1 | # Backend Reference 2 | 3 | Registrator supports a number of backing registries. In order for Registrator to 4 | be useful, you need to be running one of these. Below are the Registry URIs to 5 | use for supported backends and documentation specific to their features. 6 | 7 | See also [Contributing Backends](../dev/backends.md). 8 | 9 | ## Consul 10 | 11 | consul://
: 12 | consul-unix:// 13 | consul-tls://
: 14 | 15 | Consul is the recommended registry since it specifically models services for 16 | service discovery with health checks. 17 | 18 | If no address and port is specified, it will default to `127.0.0.1:8500`. 19 | 20 | Consul supports tags but no arbitrary service attributes. 21 | 22 | When using the `consul-tls` scheme, registrator communicates with Consul through TLS. You must set the following environment variables: 23 | * `CONSUL_CACERT` : CA file location 24 | * `CONSUL_CLIENT_CERT` : Certificate file location 25 | * `CONSUL_CLIENT_KEY` : Key location 26 | 27 | For more information on the Consul check parameters below, see the [API documentation](https://www.consul.io/api/agent/check.html#register-check). 28 | 29 | ### Consul HTTP Check 30 | 31 | This feature is only available when using Consul 0.5 or newer. Containers 32 | specifying these extra metadata in labels or environment will be used to 33 | register an HTTP health check with the service. 34 | 35 | ```bash 36 | SERVICE_80_CHECK_HTTP=/health/endpoint/path 37 | SERVICE_80_CHECK_INTERVAL=15s 38 | SERVICE_80_CHECK_TIMEOUT=1s # optional, Consul default used otherwise 39 | SERVICE_80_CHECK_HTTP_METHOD=HEAD # optional, Consul default used otherwise 40 | ``` 41 | 42 | It works for services on any port, not just 80. If its the only service, 43 | you can also use `SERVICE_CHECK_HTTP`. 44 | 45 | ### Consul HTTPS Check 46 | 47 | This feature is only available when using Consul 0.5 or newer. Containers 48 | specifying these extra metedata in labels or environment will be used to 49 | register an HTTPS health check with the service. 50 | 51 | ```bash 52 | SERVICE_443_CHECK_HTTPS=/health/endpoint/path 53 | SERVICE_443_CHECK_INTERVAL=15s 54 | SERVICE_443_CHECK_TIMEOUT=1s # optional, Consul default used otherwise 55 | SERVICE_443_CHECK_HTTPS_METHOD=HEAD # optional, Consul default used otherwise 56 | ``` 57 | 58 | ### Consul TCP Check 59 | 60 | This feature is only available when using Consul 0.6 or newer. Containers 61 | specifying these extra metadata in labels or environment will be used to 62 | register an TCP health check with the service. 63 | 64 | ```bash 65 | SERVICE_443_CHECK_TCP=true 66 | SERVICE_443_CHECK_INTERVAL=15s 67 | SERVICE_443_CHECK_TIMEOUT=3s # optional, Consul default used otherwise 68 | ``` 69 | 70 | ### Consul Script Check 71 | 72 | This feature is tricky because it lets you specify a script check to run from 73 | Consul. If running Consul in a container, you're limited to what you can run 74 | from that container. For example, curl must be installed for this to work: 75 | 76 | ```bash 77 | SERVICE_CHECK_SCRIPT=curl --silent --fail example.com 78 | ``` 79 | 80 | The default interval for any non-TTL check is 10s, but you can set it with 81 | `_CHECK_INTERVAL`. The check command will be interpolated with the `$SERVICE_IP` 82 | and `$SERVICE_PORT` placeholders: 83 | 84 | ```bash 85 | SERVICE_CHECK_SCRIPT=nc $SERVICE_IP $SERVICE_PORT | grep OK 86 | ``` 87 | 88 | ### Consul TTL Check 89 | 90 | You can also register a TTL check with Consul. Keep in mind, this means Consul 91 | will expect a regular heartbeat ping to its API to keep the service marked 92 | healthy. 93 | 94 | ```bash 95 | SERVICE_CHECK_TTL=30s 96 | ``` 97 | 98 | ### Consul gRPC Check 99 | 100 | This feature is only available when using Consul 1.0.5 or newer. Containers 101 | specifying these extra metadata in labels or environment will be used to 102 | register an gRPC health check with the service. 103 | 104 | ```bash 105 | SERVICE_CHECK_GRPC=true 106 | SERVICE_CHECK_INTERVAL=5s 107 | SERVICE_CHECK_TIMEOUT=3s # optional, Consul default uses 10s 108 | SERVICE_CHECK_GRPC_USE_TLS=true # optional, Consul default uses false 109 | SERVICE_CHECK_TLS_SKIP_VERIFY=true # optional, Consul default uses false 110 | ``` 111 | 112 | ### Consul Initial Health Check Status 113 | 114 | By default when a service is registered against Consul, the state is set to "critical". You can specify the initial health check status. 115 | 116 | ```bash 117 | SERVICE_CHECK_INITIAL_STATUS=passing 118 | ``` 119 | 120 | ### Consul Critical Service Deregistration 121 | 122 | Consul can deregister a service if the check is in the critical state for more than a configurable amount of time. 123 | If enabled this should be much longer than any expected recoverable outage. 124 | 125 | ```bash 126 | SERVICE_CHECK_DEREGISTER_AFTER=10m 127 | ``` 128 | 129 | ## Consul KV 130 | 131 | consulkv://
:/ 132 | consulkv-unix://:/ 133 | 134 | This is a separate backend to use Consul's key-value store instead of its native 135 | service catalog. This behaves more like etcd since it has similar semantics, but 136 | currently doesn't support TTLs. 137 | 138 | If no address and port is specified, it will default to `127.0.0.1:8500`. 139 | 140 | Using the prefix from the Registry URI, service definitions are stored as: 141 | 142 | // = : 143 | 144 | ## Etcd 145 | 146 | etcd://
:/ 147 | 148 | Etcd works similar to Consul KV, except supports service TTLs. It also currently 149 | doesn't support service attributes/tags. 150 | 151 | If no address and port is specified, it will default to `127.0.0.1:2379`. 152 | 153 | Using the prefix from the Registry URI, service definitions are stored as: 154 | 155 | // = : 156 | 157 | ## SkyDNS 2 158 | 159 | skydns2://
:/ 160 | 161 | SkyDNS 2 uses etcd, so this backend writes service definitions in a format compatible with SkyDNS 2. 162 | The path may not be omitted and must be a valid DNS domain for SkyDNS. 163 | 164 | If no address and port is specified, it will default to `127.0.0.1:2379`. 165 | 166 | Using a Registry URI with the domain `cluster.local`, service definitions are stored as: 167 | 168 | /skydns/local/cluster// = {"host":"","port":} 169 | 170 | SkyDNS requires the service ID to be a valid DNS hostname, so this backend requires containers to 171 | override service ID to a valid DNS name. Example: 172 | 173 | $ docker run -d --name redis-1 -e SERVICE_ID=redis-1 -p 6379:6379 redis 174 | 175 | ## Zookeeper Store 176 | 177 | 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: 178 | 179 | $ registrator zookeeper://zookeeper.host/basepath 180 | $ registrator zookeeper://192.168.1.100:9999/basepath 181 | 182 | Within the base path specified in the zookeeper URI, registrator will create the following path tree containing a JSON entry for the service: 183 | 184 | / = 185 | 186 | The JSON will contain all information about the published container service. As an example, the following container start: 187 | 188 | docker run -i -p 80 -e 'SERVICE_80_NAME=www' -t ubuntu:14.04 /bin/bash 189 | 190 | Will result in the zookeeper path and JSON znode body: 191 | 192 | /basepath/www/80 = {"Name":"www","IP":"192.168.1.123","PublicPort":49153,"PrivatePort":80,"ContainerID":"9124853ff0d1","Tags":[],"Attrs":{}} 193 | -------------------------------------------------------------------------------- /docs/user/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ### Why is it registering the wrong service IP? 4 | 5 | If you're getting an odd IP registered for services, such as `127.0.0.1`, then 6 | Registrator was unable to detect the right IP. Since this is hard to do correctly, 7 | it's best to always set the `-ip
` option to the IP you want it to be. 8 | -------------------------------------------------------------------------------- /docs/user/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | This is a short, simple tutorial intended to get you started with Registrator as 4 | quickly as possible. For full reference, see [Run Reference](run.md). 5 | 6 | ## Overview 7 | 8 | Registrator watches for new Docker containers and inspects them to determine 9 | what services they provide. For our purposes, a service is anything listening on 10 | a port. Any services Registrator finds on a container, they will be added to a 11 | service registry, such as Consul or etcd. 12 | 13 | In this tutorial, we're going to use Registrator with Consul, and run a Redis 14 | container that will automatically get added to Consul. 15 | 16 | ## Before Starting 17 | 18 | We're going to need a host running Docker, which could just be a local 19 | [boot2docker](http://boot2docker.io/) VM, and a shell with the `docker` client 20 | pointed to that host. 21 | 22 | We'll also need to have Consul running, which can just be running in a 23 | container. Let's run a single instance of Consul in server bootstrap mode: 24 | ``` 25 | $ docker run -d --name=consul --net=host gliderlabs/consul-server -bootstrap 26 | ``` 27 | Consul is run differently in production, but this will get us through this tutorial. 28 | We can now access Consul's HTTP API via the Docker machine's IP: 29 | ``` 30 | $ curl $(boot2docker ip):8500/v1/catalog/services 31 | {"consul":[]} 32 | ``` 33 | Now we can start Registrator. 34 | 35 | ## Running Registrator 36 | 37 | Registrator is run on every host, but since we only have one host here, we can 38 | just run it once. The primary bit of configuration needed to start Registrator 39 | is how to connect to its registry, or Consul in this case. 40 | 41 | Besides option flags, the only argument Registrator takes is a registry URI, 42 | which encodes what type of registry, how to connect to it, and any options. 43 | ``` 44 | $ docker run -d \ 45 | --name=registrator \ 46 | --net=host \ 47 | --volume=/var/run/docker.sock:/tmp/docker.sock \ 48 | gliderlabs/registrator:latest \ 49 | consul://localhost:8500 50 | ``` 51 | There's a bit going on here in the Docker run arguments. First, we run the 52 | container detached and name it. We also run in host network mode. This makes 53 | sure Registrator has the hostname and IP of the actual host. It also makes it 54 | easier to connect to Consul. We also must mount the Docker socket. 55 | 56 | The last line is the argument to Registrator itself, which is just our 57 | registry URI. We're using `consul` on `localhost:8500`, since this is running on 58 | the same network interface as Consul. 59 | ``` 60 | $ docker logs registrator 61 | ``` 62 | We should see it started up and "Listening for Docker events". That's it, it's 63 | working! 64 | 65 | ## Running Redis 66 | 67 | Now as you start containers, if they provide any services, they'll be added 68 | to Consul. We'll run Redis now from the standard library image: 69 | ``` 70 | $ docker run -d -P --name=redis redis 71 | ``` 72 | Notice we used `-P` to publish all ports. This is not often used except with 73 | Registrator. Not only does it publish all exposed ports the container has, but 74 | it assigns them to a random port on the host. Since the point of Registrator 75 | and Consul is to provide service discovery, the port doesn't matter. Though 76 | there can still be cases where you still want to manually specify the port. 77 | 78 | Let's look at Consul's services endpoint again: 79 | ``` 80 | $ curl $(boot2docker ip):8500/v1/catalog/services 81 | {"consul":[],"redis":[]} 82 | ``` 83 | Consul now has a service called redis. We can see more about the service 84 | including what port was published by looking at the service endpoint for redis: 85 | ``` 86 | $ curl $(boot2docker ip):8500/v1/catalog/service/redis 87 | [{"Node":"boot2docker","Address":"10.0.2.15","ServiceID":"boot2docker:redis:6379","ServiceName":"redis","ServiceTags":null,"ServiceAddress":"","ServicePort":32768}] 88 | ``` 89 | If we remove the redis container, we can see the service is removed from Consul: 90 | ``` 91 | $ docker rm -f redis 92 | redis 93 | $ curl $(boot2docker ip):8500/v1/catalog/service/redis 94 | [] 95 | ``` 96 | That's it! I know this may not be interesting alone, but there's a lot you can 97 | do once services are registered in Consul. However, that's out of the scope of 98 | Registrator. All it does is puts container services into Consul. 99 | 100 | ## Next Steps 101 | 102 | There are more ways to configure Registrator and ways you can run containers to 103 | customize the services that are extracted from them. For this, take a look at 104 | the [Run Reference](run.md) and [Service Model](services.md). 105 | -------------------------------------------------------------------------------- /docs/user/run.md: -------------------------------------------------------------------------------- 1 | # Run Reference 2 | 3 | Registrator is designed to be run once on every host. You *could* run a single 4 | Registrator for your cluster, but you get better scaling properties and easier 5 | configuration by ensuring Registrator runs on every host. Assuming some level of 6 | automation, running everywhere is ironically simpler than running once somewhere. 7 | 8 | ## Running Registrator 9 | 10 | docker run [docker options] gliderlabs/registrator[:tag] [options] 11 | 12 | Registrator requires and recommends some Docker options, has its own set of options 13 | and then requires a Registry URI. Here is a typical way to run Registrator: 14 | 15 | $ docker run -d \ 16 | --name=registrator \ 17 | --net=host \ 18 | --volume=/var/run/docker.sock:/tmp/docker.sock \ 19 | gliderlabs/registrator:latest \ 20 | consul://localhost:8500 21 | 22 | ## Docker Options 23 | 24 | Option | Required | Description 25 | ------ | -------- | ----------- 26 | `--volume=/var/run/docker.sock:/tmp/docker.sock` | yes | Allows Registrator to access Docker API 27 | `--net=host` | recommended | Helps Registrator get host-level IP and hostname 28 | 29 | An alternative to host network mode would be to set the container hostname to the host 30 | hostname (`-h $HOSTNAME`) and using the `-ip` Registrator option below. 31 | 32 | ## Registrator Options 33 | 34 | Option | Since | Description 35 | ------ | ----- | ----------- 36 | `-cleanup` | v7 | Cleanup dangling services 37 | `-deregister ` | v6 | Deregister exited services "always" or "on-success". Default: always 38 | `-internal` | | Use exposed ports instead of published ports 39 | `-ip ` | | Force IP address used for registering services 40 | `-resync ` | v6 | Frequency all services are resynchronized. Default: 0, never 41 | `-retry-attempts ` | v7 | Max retry attempts to establish a connection with the backend 42 | `-retry-interval ` | v7 | Interval (in millisecond) between retry-attempts 43 | `-tags ` | v5 | Force comma-separated tags on all registered services 44 | `-ttl ` | | TTL for services. Default: 0, no expiry (supported backends only) 45 | `-ttl-refresh ` | | Frequency service TTLs are refreshed (supported backends only) 46 | `-useIpFromLabel