├── .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 | [](https://circleci.com/gh/gliderlabs/registrator)
6 | [](https://hub.docker.com/r/gliderlabs/registrator/)
7 | [](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 | [](https://circleci.com/gh/gliderlabs/registrator)
6 | [](https://registry.hub.docker.com/u/gliderlabs/registrator/)
7 | [](https://imagelayers.io/?images=gliderlabs%2Fregistrator:latest)
8 | [](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