docker_boss 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +377 -0
- data/Rakefile +1 -0
- data/bin/docker-boss +7 -0
- data/docker_boss.gemspec +32 -0
- data/example.cfg.yml +59 -0
- data/lib/docker_boss.rb +13 -0
- data/lib/docker_boss/cli.rb +85 -0
- data/lib/docker_boss/engine.rb +112 -0
- data/lib/docker_boss/helpers.rb +45 -0
- data/lib/docker_boss/module.rb +33 -0
- data/lib/docker_boss/modules/dns.rb +79 -0
- data/lib/docker_boss/modules/etcd.rb +73 -0
- data/lib/docker_boss/modules/templates.rb +131 -0
- data/lib/docker_boss/version.rb +3 -0
- metadata +174 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b4605ecf2c6938cbd8a53c54afc58505775e5d8a
|
4
|
+
data.tar.gz: 2c16b891bb87d053705ab5665282674c9208d65a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 32b694b16f875586a504b7be2e15cf7fb0a5a3e0ba4f18a153bbd2cd6fd1fe281fa0deeab2317b71d21b68924bfc5abfd15bf85147501416e2a0f5b4341b7918
|
7
|
+
data.tar.gz: 094c3b74f01c15a92f5284faaf3b150fbd62c441ee366b5170f93e7dcba39f689883e93a6ac089ca50ae9b76841b5c544d74d075e85e3bf8d91be9d3f31b59bf
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Alex Hornung
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,377 @@
|
|
1
|
+
# DockerBoss
|
2
|
+
|
3
|
+
DockerBoss monitors Docker containers and keeps track of when a container is started, stopped, changed, etc. On such an event, DockerBoss triggers actions such as updating files, controlling other containers, updating entries in etcd, updating records in a built-in DNS server, etc.
|
4
|
+
|
5
|
+
DockerBoss has been built from the start to be completely pluggable. By default, it ships with 3 different modules:
|
6
|
+
|
7
|
+
- templates: Allows re-rendering configuration files on e.g. a docker volume and then performing an action on either the host or a container, such as restarting, sending a signal, etc.
|
8
|
+
|
9
|
+
- etcd: Allows inserting/removing keys in etcd depending on the currently running containers. This allows, for example, automatically updating etcd entries for a service such as SkyDNS when a container changes IP because it is restarted.
|
10
|
+
|
11
|
+
- dns: The dns module has a very simple built-in DNS server. The DNS server's records get updated based on the container's addresses, names, environment variables, etc. The DNS server will pass through requests for zones that it is not the authoritative server for.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
gem 'docker_boss'
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install docker_boss
|
26
|
+
|
27
|
+
This installs a binary called `docker-boss`.
|
28
|
+
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
DockerBoss can run in a one-off mode, in which it only triggers actions based on the currently running containers and then exits. In addition, it can run in a continuous mode, in which it will trigger actions based on the currently running containers, but then continues to watch Docker for further events, triggering updates on any change.
|
33
|
+
|
34
|
+
To run it in one-off mode, execute:
|
35
|
+
|
36
|
+
$ docker-boss once -c /path/to/config.yml
|
37
|
+
|
38
|
+
To run in watch mode, execute:
|
39
|
+
|
40
|
+
$ docker-boss watch -c /path/to/config.yml
|
41
|
+
|
42
|
+
By default, DockerBoss runs in the foreground. If you want to run DockerBoss as a daemon, execute:
|
43
|
+
|
44
|
+
$ docker-boss watch -c /path/to/config.yml -D
|
45
|
+
|
46
|
+
Both modes support an optional log argument, which allows logging to stdout, syslog or a file:
|
47
|
+
|
48
|
+
$ docker-boss watch -c /path/to/config.yml -l syslog
|
49
|
+
$ docker-boss watch -c /path/to/config.yml -l -
|
50
|
+
$ docker-boss watch -c /path/to/config.yml -l /var/log/docker_boss.log
|
51
|
+
|
52
|
+
|
53
|
+
## Configuration
|
54
|
+
|
55
|
+
An example configuration file with some settings for each of the bundled modules is included in `example.cfg.yml`.
|
56
|
+
|
57
|
+
Each top-level key in the configuration file corresponds to the name of a module. All entries under that key are passed to the module for configuration of that particular module.
|
58
|
+
|
59
|
+
If, for example, a key called `etcd` exists, then the DockerBoss `etcd` module will be instantiated and configured with the settings under the `etcd` key in the configuration.
|
60
|
+
|
61
|
+
For more details about the configuration for each module, have a look at the detailed description of that module.
|
62
|
+
|
63
|
+
### Container description
|
64
|
+
|
65
|
+
Wherever templates are used in configuration settings or external template files, they are generally passed either a single container or an array of containers. Each container is a Ruby Hash, as follows:
|
66
|
+
|
67
|
+
```json
|
68
|
+
{
|
69
|
+
"AppArmorProfile":"",
|
70
|
+
"Args":[
|
71
|
+
"mysqld"
|
72
|
+
],
|
73
|
+
"Config":{
|
74
|
+
"AttachStderr":true,
|
75
|
+
"AttachStdin":false,
|
76
|
+
"AttachStdout":true,
|
77
|
+
"Cmd":[
|
78
|
+
"mysqld"
|
79
|
+
],
|
80
|
+
"CpuShares":0,
|
81
|
+
"Cpuset":"",
|
82
|
+
"Domainname":"",
|
83
|
+
"Entrypoint":[
|
84
|
+
"/docker-entrypoint.sh"
|
85
|
+
],
|
86
|
+
"Env":{
|
87
|
+
"MYSQL_ROOT_PASSWORD":"assbYrwVnWxP",
|
88
|
+
"PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
89
|
+
"MARIADB_MAJOR":"10.0",
|
90
|
+
"MARIADB_VERSION":"10.0.15+maria-1~wheezy"
|
91
|
+
},
|
92
|
+
"ExposedPorts":{
|
93
|
+
"3306/tcp":{
|
94
|
+
|
95
|
+
}
|
96
|
+
},
|
97
|
+
"Hostname":"6b2bbdac4b6e",
|
98
|
+
"Image":"mariadb",
|
99
|
+
"MacAddress":"",
|
100
|
+
"Memory":0,
|
101
|
+
"MemorySwap":0,
|
102
|
+
"NetworkDisabled":false,
|
103
|
+
"OnBuild":null,
|
104
|
+
"OpenStdin":false,
|
105
|
+
"PortSpecs":null,
|
106
|
+
"StdinOnce":false,
|
107
|
+
"Tty":false,
|
108
|
+
"User":"",
|
109
|
+
"Volumes":{
|
110
|
+
"/var/lib/mysql":{
|
111
|
+
|
112
|
+
}
|
113
|
+
},
|
114
|
+
"WorkingDir":""
|
115
|
+
},
|
116
|
+
"Created":"2014-12-24T15:54:44.830878163Z",
|
117
|
+
"Driver":"devicemapper",
|
118
|
+
"ExecDriver":"native-0.2",
|
119
|
+
"HostConfig":{
|
120
|
+
"Binds":null,
|
121
|
+
"CapAdd":null,
|
122
|
+
"CapDrop":null,
|
123
|
+
"ContainerIDFile":"",
|
124
|
+
"Devices":[
|
125
|
+
|
126
|
+
],
|
127
|
+
"Dns":null,
|
128
|
+
"DnsSearch":null,
|
129
|
+
"ExtraHosts":null,
|
130
|
+
"IpcMode":"",
|
131
|
+
"Links":null,
|
132
|
+
"LxcConf":[
|
133
|
+
|
134
|
+
],
|
135
|
+
"NetworkMode":"bridge",
|
136
|
+
"PortBindings":{
|
137
|
+
|
138
|
+
},
|
139
|
+
"Privileged":false,
|
140
|
+
"PublishAllPorts":false,
|
141
|
+
"RestartPolicy":{
|
142
|
+
"MaximumRetryCount":0,
|
143
|
+
"Name":""
|
144
|
+
},
|
145
|
+
"SecurityOpt":null,
|
146
|
+
"VolumesFrom":null
|
147
|
+
},
|
148
|
+
"HostnamePath":"/var/lib/docker/containers/6b2bbdac4b6e01caccf84346aff37f31740760a95d131b519de6e6e0ca6ba2d9/hostname",
|
149
|
+
"HostsPath":"/var/lib/docker/containers/6b2bbdac4b6e01caccf84346aff37f31740760a95d131b519de6e6e0ca6ba2d9/hosts",
|
150
|
+
"Id":"6b2bbdac4b6e01caccf84346aff37f31740760a95d131b519de6e6e0ca6ba2d9",
|
151
|
+
"Image":"dc7e7b74d729c8b7ffab9ac5bc4b9a1463739e085b461b29928bf2fee1ff8303",
|
152
|
+
"MountLabel":"",
|
153
|
+
"Name":"/differentdb",
|
154
|
+
"NetworkSettings":{
|
155
|
+
"Bridge":"docker0",
|
156
|
+
"Gateway":"172.17.42.1",
|
157
|
+
"IPAddress":"172.17.0.19",
|
158
|
+
"IPPrefixLen":16,
|
159
|
+
"MacAddress":"02:42:ac:11:00:13",
|
160
|
+
"PortMapping":null,
|
161
|
+
"Ports":{
|
162
|
+
"3306/tcp":null
|
163
|
+
}
|
164
|
+
},
|
165
|
+
"Path":"/docker-entrypoint.sh",
|
166
|
+
"ProcessLabel":"",
|
167
|
+
"ResolvConfPath":"/var/lib/docker/containers/6b2bbdac4b6e01caccf84346aff37f31740760a95d131b519de6e6e0ca6ba2d9/resolv.conf",
|
168
|
+
"State":{
|
169
|
+
"Error":"",
|
170
|
+
"ExitCode":0,
|
171
|
+
"FinishedAt":"0001-01-01T00:00:00Z",
|
172
|
+
"OOMKilled":false,
|
173
|
+
"Paused":false,
|
174
|
+
"Pid":13435,
|
175
|
+
"Restarting":false,
|
176
|
+
"Running":true,
|
177
|
+
"StartedAt":"2014-12-24T15:54:45.133773245Z"
|
178
|
+
},
|
179
|
+
"Volumes":{
|
180
|
+
"/var/lib/mysql":"/var/lib/docker/vfs/dir/1e3963ffc558c14d4b29bea89d6eafca9945500f5c80ea94b94b6e8664d5a1dc"
|
181
|
+
},
|
182
|
+
"VolumesRW":{
|
183
|
+
"/var/lib/mysql":true
|
184
|
+
}
|
185
|
+
}
|
186
|
+
```
|
187
|
+
|
188
|
+
## Modules
|
189
|
+
|
190
|
+
The core of DockerBoss only keeps track of changes to container state. All actions are part of modules.
|
191
|
+
|
192
|
+
### templates
|
193
|
+
|
194
|
+
The templates module allows re-rendering configuration files on e.g. docker volumes and then running actions such as restarting a container or sending a signal to the root process of the container.
|
195
|
+
|
196
|
+
Each configuration entry can have an optional linked container. The container is specified via its name. If the action(s) performed by a particular configuration entry can themselves trigger further update events, it is important to provide the `linked_container` configuration to avoid an infinite amount of events because each event's actions triggers further events.
|
197
|
+
|
198
|
+
The `linked_container` `action` setting allows performing one of the following actions on the container:
|
199
|
+
|
200
|
+
- `shell:<cmd>` - Execute a command inside the container in a shell
|
201
|
+
- `shell_bg:<cmd>` - Same as `shell`, but does not wait for the result
|
202
|
+
- `exec:<cmd>` - Execute a command inside the container without a shell
|
203
|
+
- `exec_bg:<cmd>` - Same as `exec`, but does not wait for the result
|
204
|
+
- `restart` - Restarts the container
|
205
|
+
- `start` - Starts the container
|
206
|
+
- `stop` - Stops the container
|
207
|
+
- `pause` - Pause the container
|
208
|
+
- `unpause` - Unpause the container
|
209
|
+
- `kill` - Kill the container
|
210
|
+
- `kill:<SIG>` - Send a signal, e.g. `SIGHUP`, to the container's root process
|
211
|
+
|
212
|
+
The `action` setting outside the `linked_container` setting allows running an arbitrary shell command on the host.
|
213
|
+
|
214
|
+
The `files` section allows specifying an array of `file` - `template` pairs. The file and template names themselves can contain ERB templates. These ERB templates can access information about the linked container via the `container` variable.
|
215
|
+
|
216
|
+
The templates themselves should also be ERB templates. They will be rendered with ERB, with a single variable in the namespace called `containers`, which is an array of all currently running containers.
|
217
|
+
|
218
|
+
Example configuration:
|
219
|
+
|
220
|
+
```yaml
|
221
|
+
templates:
|
222
|
+
auto_haproxy:
|
223
|
+
linked_container:
|
224
|
+
name: "front-haproxy"
|
225
|
+
action: "kill:SIGHUP"
|
226
|
+
# Other examples:
|
227
|
+
# action: "shell:cat /proc/cpuinfo > /tmp/cpuinfo"
|
228
|
+
# action: "exec:touch /tmp/foobar"
|
229
|
+
# action: "restart"
|
230
|
+
|
231
|
+
files:
|
232
|
+
- file: "<%= container['Volumes']['/etc/haproxy/proxies'] %>/proxies.cfg"
|
233
|
+
template: "<%= container['Volumes']['/etc/haproxy/proxies'] %>/proxies.cfg.erb"
|
234
|
+
|
235
|
+
action: "echo 'This happens on the host' > /tmp/foo.test"
|
236
|
+
```
|
237
|
+
|
238
|
+
A very simple example template file could look as follows:
|
239
|
+
|
240
|
+
```
|
241
|
+
<% containers.each do |c| %>
|
242
|
+
<%= c['Id'] %> -> <%= c['Name'] %>
|
243
|
+
<% end %>
|
244
|
+
```
|
245
|
+
|
246
|
+
### etcd
|
247
|
+
|
248
|
+
The etcd module adds/updates/removes keys in etcd based on changes to the containers. This can be used to provide dynamic settings based on the containers to other tools interfacing with etcd, such as SkyDNS and confd.
|
249
|
+
|
250
|
+
The `server` setting defines the host and port of the etcd server. SSL and basic HTTP auth are not yet supported.
|
251
|
+
|
252
|
+
The `setup` setting is a template, each line of which can manipulate keys in etcd. These key manipulations are run once when the module/DockerBoss starts, and can be used to ensure a clean slate, free of any old keys from a previous run. Each line must follow one of the following formats:
|
253
|
+
|
254
|
+
- `ensure <key> <value>` - sets a given key in etcd to the given value.
|
255
|
+
- `absent <key>` - removes a given key in etcd.
|
256
|
+
- `absent_recursive <key>` removes a key and all its children.
|
257
|
+
|
258
|
+
The `sets` setting supports any number of children, each of which is an ERB template that will be rendered for each container. The output of the template rendering must be lines of the following format:
|
259
|
+
|
260
|
+
- `ensure <key> <value>` - ensure a key exists in etcd with the given value.
|
261
|
+
|
262
|
+
The etcd will keep track of keys set during previous state updates, and if a key is no longer present, it will be removed from etcd.
|
263
|
+
|
264
|
+
Example configuration:
|
265
|
+
|
266
|
+
```yaml
|
267
|
+
etcd:
|
268
|
+
server:
|
269
|
+
host: '127.0.0.1'
|
270
|
+
port: 4001
|
271
|
+
|
272
|
+
setup: |
|
273
|
+
absent_recursive /skydns/docker
|
274
|
+
absent_recursive /vhosts
|
275
|
+
|
276
|
+
sets:
|
277
|
+
skydns: |
|
278
|
+
<% if container['Config']['Env'].has_key? 'SERVICES' %>
|
279
|
+
<% container['Config']['Env']['SERVICES'].split(',').each do |s| %>
|
280
|
+
ensure <%= "/skydns/#{s.split(':')[0].split('.').reverse.join('/')}" %> <%= as_json(host: container['NetworkSettings']['IPAddress'], port: s.split(':')[1]) %>
|
281
|
+
<% end %>
|
282
|
+
<% elsif container['Config']['Env'].has_key? 'SERVICE_NAME' %>
|
283
|
+
ensure <%= "/skydns/#{container['Config']['Env']['SERVICE_NAME'].split('.').reverse.join('/')}" %> <%= as_json(host: container['NetworkSettings']['IPAddress']) %>
|
284
|
+
<% else %>
|
285
|
+
ensure <%= "/skydns/#{(container['Config']['Hostname'] + ".docker").split('.').reverse.join('/')}" %> <%= as_json(host: container['NetworkSettings']['IPAddress']) %>
|
286
|
+
ensure <%= "/skydns/#{(container['Name'][1..-1] + ".docker").split('.').reverse.join('/')}" %> <%= as_json(host: container['NetworkSettings']['IPAddress']) %>
|
287
|
+
<% end %>
|
288
|
+
|
289
|
+
vhosts: |
|
290
|
+
<% container['Config']['Env'].fetch('VHOSTS', '').split(',').each do |vh| %>
|
291
|
+
ensure <%= "/vhosts/#{vh.split(':')[0]}/#{container['Id']}" %> <%= as_json(host: container['NetworkSettings']['IPAddress'], port: vh.split(':').fetch(1, '80')) %>
|
292
|
+
<% end %>
|
293
|
+
```
|
294
|
+
|
295
|
+
|
296
|
+
### dns
|
297
|
+
|
298
|
+
The DNS module starts a built-in DNS server based on `rubydns`. The DNS server can be configured to support a number of upstream DNS servers, to which queries fall through if no known record is available and it doesn't match any of the internal DNS zones. As Docker can currently only handle IPv4, no `AAAA` records are ever served for containers.
|
299
|
+
|
300
|
+
The `ttl` setting determines the `ttl` for each response, both positive and NXDOMAIN.
|
301
|
+
|
302
|
+
The `listen` setting is an array of addresses/ports on which the DNS server should listen.
|
303
|
+
|
304
|
+
The `upstream` setting is an array of upstream DNS servers to which requests should be forwarded to if no record is available locally and the name is not within one of the local zones.
|
305
|
+
|
306
|
+
The `zones` setting is an array of zones for which the DNS server is authoritative. The DNS server will not forward requests in these zones to upstream DNS servers, not even if no local record is found.
|
307
|
+
|
308
|
+
The `spec` setting is an ERB template which should render out all hostnames for a given container, each on a separate line. A container can have any number of host records, even none at all (by simply not rendering out any hostname).
|
309
|
+
|
310
|
+
Example configuration:
|
311
|
+
|
312
|
+
```yaml
|
313
|
+
dns:
|
314
|
+
ttl: 5
|
315
|
+
listen:
|
316
|
+
- host: 0.0.0.0
|
317
|
+
port: 5300
|
318
|
+
|
319
|
+
upstream:
|
320
|
+
- 8.8.8.8
|
321
|
+
- 8.8.4.4
|
322
|
+
|
323
|
+
zones:
|
324
|
+
- .local
|
325
|
+
- .docker
|
326
|
+
|
327
|
+
spec: |
|
328
|
+
<%= container['Config']['Env'].fetch('SERVICE_NAME', container['Name'][1..-1]) %>.docker
|
329
|
+
<%= container['Config']['Hostname'] %>.docker
|
330
|
+
```
|
331
|
+
|
332
|
+
### Writing your own
|
333
|
+
|
334
|
+
Writing your own module is really quite simple. You only have to provide a `trigger` method that will be called on each state change, and is passed an array of all the currently running containers, as well as the ID of the container that triggered the state change.
|
335
|
+
|
336
|
+
Additionally, you can provide a `run` method which can spawn off a long-running thread. The `run` method must return a `Thread` instance.
|
337
|
+
|
338
|
+
Here's a basic skeleton:
|
339
|
+
```ruby
|
340
|
+
require 'docker_boss'
|
341
|
+
require 'docker_boss/module'
|
342
|
+
|
343
|
+
class DockerBoss::Module::Foo < DockerBoss::Module
|
344
|
+
def initialize(config)
|
345
|
+
@config = config
|
346
|
+
DockerBoss.logger.debug "foo: Set up with config: #{config}"
|
347
|
+
end
|
348
|
+
|
349
|
+
# This method is optional; you should omit it unless you spawn off a
|
350
|
+
# separate, long-running, thread.
|
351
|
+
def run
|
352
|
+
Thread.new do
|
353
|
+
loop do
|
354
|
+
sleep 10
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def trigger(containers, trigger_id)
|
360
|
+
DockerBoss.logger.debug "foo: State change triggered by container_id=#{trigger_id}"
|
361
|
+
containers.each do |c|
|
362
|
+
DockerBoss.logger.debug "foo: container: #{c['Id]}"
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
```
|
367
|
+
|
368
|
+
Any class extending `DockerBoss::Module` is automatically registered as a module. The name of the class defines the name of the configuration key in the config yaml. For the example above, the name of the key would be `foo`. Any key under `foo` in the config yaml would be passed as `config` to the class constructor.
|
369
|
+
|
370
|
+
|
371
|
+
## Contributing
|
372
|
+
|
373
|
+
1. Fork it
|
374
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
375
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
376
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
377
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/docker-boss
ADDED
data/docker_boss.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'docker_boss/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "docker_boss"
|
8
|
+
spec.version = DockerBoss::VERSION
|
9
|
+
spec.authors = ["Alex Hornung"]
|
10
|
+
spec.email = ["alex@alexhornung.com"]
|
11
|
+
spec.description = %q{Templating using docker container information}
|
12
|
+
spec.summary = %q{Templating using docker container information}
|
13
|
+
spec.homepage = "https://github.com/bwalex/docker_boss"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.required_ruby_version = '>= 1.9.3'
|
22
|
+
|
23
|
+
spec.add_dependency "docker-api", "~> 1.17.0"
|
24
|
+
spec.add_dependency "thor", "~> 0.19.1"
|
25
|
+
spec.add_dependency "daemons", "~> 1.1.9"
|
26
|
+
spec.add_dependency "rubydns", "~> 0.9.2"
|
27
|
+
spec.add_dependency "etcd", "~> 0.2.4"
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
30
|
+
spec.add_development_dependency "rake", "~> 10.4.2"
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.1.0"
|
32
|
+
end
|
data/example.cfg.yml
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
dns:
|
2
|
+
ttl: 5
|
3
|
+
listen:
|
4
|
+
- host: 0.0.0.0
|
5
|
+
port: 5300
|
6
|
+
|
7
|
+
upstream:
|
8
|
+
- 8.8.8.8
|
9
|
+
- 8.8.4.4
|
10
|
+
|
11
|
+
zones:
|
12
|
+
- .local
|
13
|
+
- .docker
|
14
|
+
|
15
|
+
spec: |
|
16
|
+
<%= container['Config']['Env'].fetch('SERVICE_NAME', container['Name'][1..-1]) %>.docker
|
17
|
+
<%= container['Config']['Hostname'] %>.docker
|
18
|
+
|
19
|
+
etcd:
|
20
|
+
server:
|
21
|
+
host: '127.0.0.1'
|
22
|
+
port: 4001
|
23
|
+
|
24
|
+
setup: |
|
25
|
+
absent_recursive /skydns/docker
|
26
|
+
|
27
|
+
sets:
|
28
|
+
skydns: |
|
29
|
+
<% if container['Config']['Env'].has_key? 'SERVICES' %>
|
30
|
+
<% container['Config']['Env']['SERVICES'].split(',').each do |s| %>
|
31
|
+
ensure <%= "/skydns/#{s.split(':')[0].split('.').reverse.join('/')}" %> <%= as_json(host: container['NetworkSettings']['IPAddress'], port: s.split(':')[1]) %>
|
32
|
+
<% end %>
|
33
|
+
<% elsif container['Config']['Env'].has_key? 'SERVICE_NAME' %>
|
34
|
+
ensure <%= "/skydns/#{container['Config']['Env']['SERVICE_NAME'].split('.').reverse.join('/')}" %> <%= as_json(host: container['NetworkSettings']['IPAddress']) %>
|
35
|
+
<% else %>
|
36
|
+
ensure <%= "/skydns/#{(container['Config']['Hostname'] + ".docker").split('.').reverse.join('/')}" %> <%= as_json(host: container['NetworkSettings']['IPAddress']) %>
|
37
|
+
ensure <%= "/skydns/#{(container['Name'][1..-1] + ".docker").split('.').reverse.join('/')}" %> <%= as_json(host: container['NetworkSettings']['IPAddress']) %>
|
38
|
+
<% end %>
|
39
|
+
|
40
|
+
vhosts: |
|
41
|
+
<% container['Config']['Env'].fetch('VHOSTS', '').split(',').each do |vh| %>
|
42
|
+
ensure <%= "/vhosts/#{vh.split(':')[0]}/#{container['Id']}" %> <%= as_json(host: container['NetworkSettings']['IPAddress'], port: vh.split(':').fetch(1, '80')) %>
|
43
|
+
<% end %>
|
44
|
+
|
45
|
+
templates:
|
46
|
+
auto_haproxy:
|
47
|
+
linked_container:
|
48
|
+
name: "mydb"
|
49
|
+
action: "restart"
|
50
|
+
# Other examples:
|
51
|
+
# action: "shell:cat /proc/cpuinfo > /tmp/cpuinfo"
|
52
|
+
# action: "exec:touch /tmp/foobar"
|
53
|
+
# action: "kill:SIGHUP"
|
54
|
+
|
55
|
+
files:
|
56
|
+
- file: "<%= container['Volumes']['/var/lib/mysql'] %>/foo.cfg"
|
57
|
+
template: "<%= container['Volumes']['/var/lib/mysql'] %>/foo.cfg.erb"
|
58
|
+
|
59
|
+
action: "echo 'This happens on the host' > /tmp/foo.test"
|
data/lib/docker_boss.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'docker_boss/version'
|
2
|
+
require 'docker_boss/engine'
|
3
|
+
require 'docker_boss'
|
4
|
+
require 'thor'
|
5
|
+
require 'docker'
|
6
|
+
require 'logger'
|
7
|
+
require 'syslog/logger'
|
8
|
+
require 'daemons'
|
9
|
+
require 'yaml'
|
10
|
+
|
11
|
+
class DockerBoss::CLI < Thor
|
12
|
+
desc "once", "Run once and exit"
|
13
|
+
method_option :config, :aliases => "-c", :type => :string, :required => true
|
14
|
+
method_option :log, :aliases => "-l", :type => :string, :default => "-", :desc => "Specify a file to log to, or '-' to log to the standard output, or 'syslog' to log to syslog"
|
15
|
+
method_option :debug, :aliases => "-d", :type => :boolean, :default => false, :desc => "Specify this option to run with debug logging enabled"
|
16
|
+
def once
|
17
|
+
setup_logging
|
18
|
+
read_config
|
19
|
+
begin
|
20
|
+
engine.refresh_and_trigger
|
21
|
+
rescue Docker::Error::DockerError => e
|
22
|
+
DockerBoss.logger.fatal "Error communicating with Docker: #{e.message}"
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "watch", "Run once, then watch for events"
|
28
|
+
method_option :config, :aliases => "-c", :type => :string, :required => true
|
29
|
+
method_option :log, :aliases => "-l", :type => :string, :default => "-", :desc => "Specify a file to log to, or '-' to log to the standard output, or 'syslog' to log to syslog"
|
30
|
+
method_option :debug, :aliases => "-d", :type => :boolean, :default => false, :desc => "Specify this option to run with debug logging enabled"
|
31
|
+
method_option :daemonize, :aliases => "-D", :type => :boolean, :default => false, :desc => "Specify this option to daemonize the process instead of running in the foreground"
|
32
|
+
method_option :incr_refresh, :type => :boolean, :default => false
|
33
|
+
def watch
|
34
|
+
setup_logging
|
35
|
+
read_config
|
36
|
+
|
37
|
+
thw = engine.event_loop
|
38
|
+
|
39
|
+
Daemons.daemonize if options[:daemonize]
|
40
|
+
|
41
|
+
begin
|
42
|
+
engine.refresh_and_trigger
|
43
|
+
thw.next_wait.join
|
44
|
+
rescue Docker::Error::DockerError => e
|
45
|
+
DockerBoss.logger.fatal "Error communicating with Docker: #{e.message}"
|
46
|
+
exit 1
|
47
|
+
rescue Exception => e
|
48
|
+
DockerBoss.logger.fatal "Fatal unhandled exception in event loop: #{e.class.name} -> #{e.message}"
|
49
|
+
e.backtrace.each { |line| DockerBoss.logger.fatal " #{line}" }
|
50
|
+
exit 1
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
no_tasks do
|
55
|
+
def engine
|
56
|
+
@engine ||= begin
|
57
|
+
engine = DockerBoss::Engine.new(options, @config)
|
58
|
+
engine
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def setup_logging
|
63
|
+
case options[:log]
|
64
|
+
when "syslog"
|
65
|
+
@logger = Syslog::Logger.new('docker-boss')
|
66
|
+
when "-"
|
67
|
+
@logger = Logger.new(STDOUT)
|
68
|
+
else
|
69
|
+
@logger = Logger.new(options[:log])
|
70
|
+
end
|
71
|
+
|
72
|
+
@logger.level = options[:debug] ? Logger::DEBUG : Logger::INFO
|
73
|
+
DockerBoss.logger=(@logger)
|
74
|
+
end
|
75
|
+
|
76
|
+
def read_config
|
77
|
+
begin
|
78
|
+
@config = YAML.load_file(options[:config])
|
79
|
+
rescue SyntaxError => e
|
80
|
+
DockerBoss.logger.fatal "Error loading config: #{e.message}"
|
81
|
+
exit 1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'docker_boss'
|
2
|
+
require 'docker_boss/module'
|
3
|
+
require 'docker_boss/modules/templates'
|
4
|
+
require 'docker_boss/modules/dns'
|
5
|
+
require 'docker_boss/modules/etcd'
|
6
|
+
require 'docker'
|
7
|
+
require 'thread'
|
8
|
+
require 'thwait'
|
9
|
+
|
10
|
+
class DockerBoss::Engine
|
11
|
+
def initialize(options, config)
|
12
|
+
@containers = []
|
13
|
+
@options = options
|
14
|
+
@config = config
|
15
|
+
@mutex = Mutex.new
|
16
|
+
@last_etcds
|
17
|
+
@modules = []
|
18
|
+
|
19
|
+
@config.each do |k,v|
|
20
|
+
@modules << DockerBoss::ModuleManager[k].new(v)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def trigger(id = nil)
|
25
|
+
@modules.each do |mod|
|
26
|
+
mod.trigger(@containers, id)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def refresh_all
|
31
|
+
@containers = Docker::Container.all.map { |c| xform_container(c.json) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def refresh_and_trigger
|
35
|
+
@mutex.synchronize {
|
36
|
+
refresh_all
|
37
|
+
trigger
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def xform_container(container)
|
42
|
+
new_env = {}
|
43
|
+
container['Config']['Env'].each do |env|
|
44
|
+
(k,v) = env.split('=', 2)
|
45
|
+
new_env[k] = v || true
|
46
|
+
end
|
47
|
+
container['Config']['Env'] = new_env
|
48
|
+
container
|
49
|
+
end
|
50
|
+
|
51
|
+
def process_event(event)
|
52
|
+
DockerBoss.logger.info "Processing event: #{event}"
|
53
|
+
case event[:status]
|
54
|
+
when 'start' # 'create' also triggers 'start'
|
55
|
+
@mutex.synchronize {
|
56
|
+
if @options[:incr_refresh]
|
57
|
+
new_container = Docker::Container.get(event[:id]).json
|
58
|
+
@containers.delete_if { |c| c['Id'] == event[:id] }
|
59
|
+
@containers << xform_container(new_container)
|
60
|
+
else
|
61
|
+
refresh_all
|
62
|
+
end
|
63
|
+
trigger(event[:id])
|
64
|
+
}
|
65
|
+
when 'die' # 'destroy', 'kill', 'stop' also trigger 'die'
|
66
|
+
@mutex.synchronize {
|
67
|
+
if @options[:incr_refresh]
|
68
|
+
@containers.delete_if { |c| c['Id'] == event[:id] }
|
69
|
+
else
|
70
|
+
refresh_all
|
71
|
+
end
|
72
|
+
trigger(event[:id])
|
73
|
+
}
|
74
|
+
when 'pause'
|
75
|
+
when 'unpause'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def event_loop
|
80
|
+
@events = Queue.new
|
81
|
+
threads = []
|
82
|
+
threads << Thread.new do
|
83
|
+
loop do
|
84
|
+
event = @events.deq
|
85
|
+
process_event(event)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
threads << Thread.new do
|
90
|
+
loop do
|
91
|
+
begin
|
92
|
+
#Docker::Event.stream({}, Docker::Connection.new(Docker.url, {:nonblock => true})) do |event|
|
93
|
+
Docker::Event.stream do |event|
|
94
|
+
DockerBoss.logger.debug "New event on socket: #{event}"
|
95
|
+
@events.enq({:id => event.id, :status => event.status})
|
96
|
+
end
|
97
|
+
rescue Docker::Error::TimeoutError
|
98
|
+
next
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
@modules.each do |mod|
|
104
|
+
begin
|
105
|
+
threads << mod.run
|
106
|
+
rescue NoMethodError
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
ThreadsWait.new(*threads)
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module DockerBoss::Helpers
|
7
|
+
def self.render_erb(template_str, data)
|
8
|
+
tmpl = ERB.new(template_str)
|
9
|
+
ns = OpenStruct.new(data)
|
10
|
+
tmpl.result(ns.instance_eval { binding })
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.render_erb_file(file, data)
|
14
|
+
contents = File.read(file)
|
15
|
+
render_erb(contents, data)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.hash_diff(old, new)
|
19
|
+
changes = {
|
20
|
+
:added => {},
|
21
|
+
:removed => {},
|
22
|
+
:changed => {}
|
23
|
+
}
|
24
|
+
|
25
|
+
new.each do |k,v|
|
26
|
+
if old.has_key? k
|
27
|
+
changes[:changed][k] = v if old[k] != v
|
28
|
+
else
|
29
|
+
changes[:added][k] = v
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
old.each do |k,v|
|
34
|
+
changes[:removed][k] = v unless new.has_key? k
|
35
|
+
end
|
36
|
+
|
37
|
+
changes
|
38
|
+
end
|
39
|
+
|
40
|
+
module TemplateHelpers
|
41
|
+
def as_json(hash)
|
42
|
+
hash.to_json
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'docker_boss'
|
2
|
+
require 'docker_boss/engine'
|
3
|
+
|
4
|
+
module DockerBoss::ModuleManager
|
5
|
+
@modules = {}
|
6
|
+
|
7
|
+
def self.<<(klass)
|
8
|
+
key = klass.name.split('::')[-1].downcase
|
9
|
+
@modules[key] = klass
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.[](key)
|
13
|
+
raise IndexError, "Unnknown module #{key}" unless @modules.has_key? key
|
14
|
+
@modules[key]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class DockerBoss::Module
|
19
|
+
def self.inherited(klass)
|
20
|
+
DockerBoss::ModuleManager << klass
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
end
|
25
|
+
|
26
|
+
def run
|
27
|
+
raise NoMethodError
|
28
|
+
end
|
29
|
+
|
30
|
+
def trigger(containers, trigger_id)
|
31
|
+
raise NoMethodError
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'rubydns'
|
2
|
+
require 'resolv'
|
3
|
+
require 'docker_boss'
|
4
|
+
require 'docker_boss/module'
|
5
|
+
require 'thread'
|
6
|
+
|
7
|
+
class DockerBoss::Module::DNS < DockerBoss::Module
|
8
|
+
attr_reader :records
|
9
|
+
|
10
|
+
def initialize(config)
|
11
|
+
@records = {}
|
12
|
+
@config = config
|
13
|
+
DockerBoss.logger.debug "dns: Set up"
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
listen = []
|
18
|
+
@config['listen'].each do |l|
|
19
|
+
listen << [:udp, l['host'], l['port'].to_i]
|
20
|
+
listen << [:tcp, l['host'], l['port'].to_i]
|
21
|
+
end
|
22
|
+
|
23
|
+
DockerBoss.logger.debug "dns: Starting DNS server"
|
24
|
+
|
25
|
+
Thread.new do
|
26
|
+
RubyDNS::run_server(:listen => listen, :ttl => @config['ttl'], :upstream_dns => @config['upstream'], :zones => @config['zones'], :supervisor_class => Server, :manager => self)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def trigger(containers, trigger_id)
|
31
|
+
records = {}
|
32
|
+
containers.each do |c|
|
33
|
+
names = DockerBoss::Helpers.render_erb(@config['spec'], :container => c)
|
34
|
+
names.lines.each do |n|
|
35
|
+
records[n.lstrip.chomp] = c['NetworkSettings']['IPAddress']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
@records = records
|
40
|
+
end
|
41
|
+
|
42
|
+
class Server < RubyDNS::Server
|
43
|
+
attr_writer :records
|
44
|
+
IN = Resolv::DNS::Resource::IN
|
45
|
+
|
46
|
+
def records
|
47
|
+
@manager.records
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize(options = {})
|
51
|
+
super(options)
|
52
|
+
@manager = options[:manager]
|
53
|
+
|
54
|
+
@ttl = options[:ttl].to_i
|
55
|
+
@zones = options[:zones]
|
56
|
+
servers = options[:upstream_dns].map { |ip| [:udp, ip, 53] }
|
57
|
+
servers.concat(options[:upstream_dns].map { |ip| [:tcp, ip, 53] })
|
58
|
+
@resolver = RubyDNS::Resolver.new(servers)
|
59
|
+
end
|
60
|
+
|
61
|
+
def process(name, resource_class, transaction)
|
62
|
+
zone = @zones.find { |z| name =~ /#{z}$/ }
|
63
|
+
if records.has_key? name
|
64
|
+
# XXX: revisit whenever docker supports IPv6, for AAAA records...
|
65
|
+
if [IN::A].include? resource_class
|
66
|
+
transaction.respond!(records[name], :ttl => @ttl)
|
67
|
+
else
|
68
|
+
transaction.fail!(:NXDomain)
|
69
|
+
end
|
70
|
+
elsif zone
|
71
|
+
soa = Resolv::DNS::Resource::IN::SOA.new(Resolv::DNS::Name.create("#{zone}"), Resolv::DNS::Name.create("dockerboss."), 1, @ttl, @ttl, @ttl, @ttl)
|
72
|
+
transaction.add([soa], :name => "#{zone}.", :ttl => @ttl, :section => :authority)
|
73
|
+
transaction.fail!(:NXDomain)
|
74
|
+
else
|
75
|
+
transaction.passthrough!(@resolver)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'docker_boss'
|
2
|
+
require 'docker_boss/module'
|
3
|
+
require 'docker_boss/helpers'
|
4
|
+
|
5
|
+
require 'erb'
|
6
|
+
require 'ostruct'
|
7
|
+
|
8
|
+
require 'etcd'
|
9
|
+
|
10
|
+
class DockerBoss::Module::Etcd < DockerBoss::Module
|
11
|
+
def initialize(config)
|
12
|
+
@config = config
|
13
|
+
DockerBoss.logger.debug "etcd: Set up to connect to #{@config['server']['host']}, port #{@config['server']['port']}"
|
14
|
+
@client = ::Etcd.client(host: @config['server']['host'], port: @config['server']['port'])
|
15
|
+
@previous_keys = {}
|
16
|
+
setup
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup
|
20
|
+
@config.fetch('setup', '').lines.each do |line|
|
21
|
+
(kw, k, v) = line.lstrip.chomp.split(" ", 3)
|
22
|
+
case kw
|
23
|
+
when 'absent'
|
24
|
+
DockerBoss.logger.debug "etcd: (setup) Remove key `#{k}`"
|
25
|
+
@client.delete(k)
|
26
|
+
when 'absent_recursive'
|
27
|
+
DockerBoss.logger.debug "etcd: (setup) Remove key `#{k}` recursively"
|
28
|
+
@client.delete(k, recursive: true)
|
29
|
+
when 'ensure'
|
30
|
+
DockerBoss.logger.debug "etcd: (setup) Set key `#{k}` => `#{v}`"
|
31
|
+
@client.set(k, value: v)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def trigger(containers, trigger_id)
|
37
|
+
@new_keys = process_specs(containers)
|
38
|
+
changes = DockerBoss::Helpers.hash_diff(@previous_keys, @new_keys)
|
39
|
+
@previous_keys = @new_keys
|
40
|
+
|
41
|
+
changes[:removed].each do |k,v|
|
42
|
+
DockerBoss.logger.debug "etcd: Remove key `#{k}`"
|
43
|
+
@client.delete(k)
|
44
|
+
end
|
45
|
+
|
46
|
+
changes[:added].each do |k,v|
|
47
|
+
DockerBoss.logger.debug "etcd: Add key `#{k}` => `#{v}`"
|
48
|
+
@client.set(k, value: v)
|
49
|
+
end
|
50
|
+
|
51
|
+
changes[:changed].each do |k,v|
|
52
|
+
DockerBoss.logger.debug "etcd: Update key `#{k}` => `#{v}`"
|
53
|
+
@client.set(k, value: v)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def process_specs(containers)
|
58
|
+
values = {}
|
59
|
+
@config['sets'].each do |name,template|
|
60
|
+
tmpl = ERB.new(template)
|
61
|
+
containers.each do |container|
|
62
|
+
ns = OpenStruct.new({ container: container })
|
63
|
+
ns.extend(DockerBoss::Helpers::TemplateHelpers)
|
64
|
+
entries = tmpl.result(ns.instance_eval { binding })
|
65
|
+
entries.lines.each do |line|
|
66
|
+
(keyword, key, value) = line.lstrip.chomp.split(" ", 3)
|
67
|
+
values[key] = value.to_s if keyword == 'ensure'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
values
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'docker_boss'
|
2
|
+
require 'docker_boss/helpers'
|
3
|
+
require 'docker_boss/module'
|
4
|
+
require 'docker'
|
5
|
+
require 'yaml'
|
6
|
+
require 'erb'
|
7
|
+
require 'ostruct'
|
8
|
+
require 'shellwords'
|
9
|
+
|
10
|
+
class DockerBoss::Module::Templates < DockerBoss::Module
|
11
|
+
def initialize(config)
|
12
|
+
@config = config
|
13
|
+
@instances = []
|
14
|
+
|
15
|
+
config.each do |name, inst_cfg|
|
16
|
+
@instances << Instance.new(name, inst_cfg)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def trigger(containers, trigger_id)
|
21
|
+
@instances.each do |instance|
|
22
|
+
begin
|
23
|
+
instance.trigger(containers, trigger_id)
|
24
|
+
rescue ArgumentError => e
|
25
|
+
DockerBoss.logger.error "templates: Error in configuration for instance `#{instance.name}`: #{e.message}"
|
26
|
+
rescue Docker::Error::DockerError => e
|
27
|
+
DockerBoss.logger.error "templates: Error occurred processing instance `#{instance.name}`: #{e.message}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
class Instance
|
34
|
+
attr_reader :name
|
35
|
+
|
36
|
+
def initialize(name, config)
|
37
|
+
@name = name
|
38
|
+
@config = config
|
39
|
+
DockerBoss.logger.debug "templates: Instance `#{@name}`: created"
|
40
|
+
end
|
41
|
+
|
42
|
+
def do_file(f, containers)
|
43
|
+
tmpl_path = DockerBoss::Helpers.render_erb(f['template'], :container => linked_container.json)
|
44
|
+
file_path = DockerBoss::Helpers.render_erb(f['file'], :container => linked_container.json)
|
45
|
+
|
46
|
+
file_contents = DockerBoss::Helpers.render_erb_file(tmpl_path, :containers => containers)
|
47
|
+
|
48
|
+
new_digest = Digest::SHA256.hexdigest file_contents
|
49
|
+
old_digest = (f.has_key? 'checksum') ? f['checksum'] : ""
|
50
|
+
f['checksum'] = new_digest
|
51
|
+
|
52
|
+
File.write(file_path, file_contents) if new_digest != old_digest
|
53
|
+
new_digest != old_digest
|
54
|
+
end
|
55
|
+
|
56
|
+
def do_actions
|
57
|
+
err = false
|
58
|
+
|
59
|
+
if @config.has_key? 'action'
|
60
|
+
err ||= !system(@config['action'])
|
61
|
+
end
|
62
|
+
|
63
|
+
if @config.has_key? 'linked_container' and @config['linked_container'].has_key? 'action'
|
64
|
+
args = @config['linked_container']['action'].split(':', 2)
|
65
|
+
case args.first
|
66
|
+
when 'shell'
|
67
|
+
raise ArgumentError, "action `shell` needs at least one more argument" if args.size < 2
|
68
|
+
command = ["sh", "-c", args[1]]
|
69
|
+
linked_container.exec(command)
|
70
|
+
when 'shell_bg'
|
71
|
+
raise ArgumentError, "action `shell_bg` needs at least one more argument" if args.size < 2
|
72
|
+
command = ["sh", "-c", args[1]]
|
73
|
+
linked_container.exec(command, detach: true)
|
74
|
+
when 'exec'
|
75
|
+
raise ArgumentError, "action `exec` needs at least one more argument" if args.size < 2
|
76
|
+
linked_container.exec(Shellwords.split(args[1]))
|
77
|
+
when 'exec_bg'
|
78
|
+
raise ArgumentError, "action `exec_bg` needs at least one more argument" if args.size < 2
|
79
|
+
linked_container.exec(Shellwords.split(args[1]), detach: true)
|
80
|
+
when 'restart'
|
81
|
+
linked_container.restart
|
82
|
+
when 'start'
|
83
|
+
linked_container.start
|
84
|
+
when 'stop'
|
85
|
+
linked_container.stop
|
86
|
+
when 'pause'
|
87
|
+
linked_container.pause
|
88
|
+
when 'unpause'
|
89
|
+
linked_container.unpause
|
90
|
+
when 'kill'
|
91
|
+
if args.size == 2
|
92
|
+
linked_container.kill(:signal => args[1])
|
93
|
+
else
|
94
|
+
linked_container.kill
|
95
|
+
end
|
96
|
+
else
|
97
|
+
raise ArgumentError, "unknown action `#{args.first}`"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def trigger(containers, trigger_id = nil)
|
103
|
+
if trigger_id.nil? or
|
104
|
+
not has_link? or
|
105
|
+
linked_container.id != trigger_id
|
106
|
+
# Only do something if the linked container is not also the triggering container
|
107
|
+
changed = @config['files'].inject (false) { |changed,f| do_file(f, containers) || changed }
|
108
|
+
DockerBoss.logger.info "templates: Instance `#{@name}`: triggered; changed=#{changed}"
|
109
|
+
do_actions if changed
|
110
|
+
else
|
111
|
+
DockerBoss.logger.info "templates: Instance `#{@name}`: ignored event"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def has_link?
|
116
|
+
@config.has_key? 'linked_container'
|
117
|
+
end
|
118
|
+
|
119
|
+
def linked_container
|
120
|
+
if has_link?
|
121
|
+
(Docker::Container.all(:all => true).find { |c| c.json['Name'] == "/#{@config['linked_container']['name']}" })
|
122
|
+
else
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def linked_container_props
|
128
|
+
data = linked_container.json
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
metadata
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: docker_boss
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Hornung
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-12-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: docker-api
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.17.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.17.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: thor
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.19.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.19.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: daemons
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.1.9
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.1.9
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubydns
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.9.2
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.9.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: etcd
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.2.4
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.2.4
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: bundler
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.3'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.3'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 10.4.2
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ~>
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 10.4.2
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 3.1.0
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ~>
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 3.1.0
|
125
|
+
description: Templating using docker container information
|
126
|
+
email:
|
127
|
+
- alex@alexhornung.com
|
128
|
+
executables:
|
129
|
+
- docker-boss
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- .gitignore
|
134
|
+
- Gemfile
|
135
|
+
- LICENSE.txt
|
136
|
+
- README.md
|
137
|
+
- Rakefile
|
138
|
+
- bin/docker-boss
|
139
|
+
- docker_boss.gemspec
|
140
|
+
- example.cfg.yml
|
141
|
+
- lib/docker_boss.rb
|
142
|
+
- lib/docker_boss/cli.rb
|
143
|
+
- lib/docker_boss/engine.rb
|
144
|
+
- lib/docker_boss/helpers.rb
|
145
|
+
- lib/docker_boss/module.rb
|
146
|
+
- lib/docker_boss/modules/dns.rb
|
147
|
+
- lib/docker_boss/modules/etcd.rb
|
148
|
+
- lib/docker_boss/modules/templates.rb
|
149
|
+
- lib/docker_boss/version.rb
|
150
|
+
homepage: https://github.com/bwalex/docker_boss
|
151
|
+
licenses:
|
152
|
+
- MIT
|
153
|
+
metadata: {}
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - '>='
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: 1.9.3
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - '>='
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0'
|
168
|
+
requirements: []
|
169
|
+
rubyforge_project:
|
170
|
+
rubygems_version: 2.0.14
|
171
|
+
signing_key:
|
172
|
+
specification_version: 4
|
173
|
+
summary: Templating using docker container information
|
174
|
+
test_files: []
|