docker_boss 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in docker_boss.gemspec
4
+ gemspec
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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
4
+
5
+ require "docker_boss/cli"
6
+
7
+ DockerBoss::CLI.start
@@ -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"
@@ -0,0 +1,13 @@
1
+ require 'docker_boss/version'
2
+ require 'celluloid'
3
+
4
+ module DockerBoss
5
+ def self.logger
6
+ @@logger ||= Logger.new(STDOUT)
7
+ end
8
+
9
+ def self.logger=(logger)
10
+ @@logger = logger
11
+ Celluloid.logger = logger
12
+ end
13
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module DockerBoss
2
+ VERSION = "0.1.0"
3
+ 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: []