dockistrano 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ require 'redis'
2
+
3
+ module Dockistrano
4
+
5
+ class Hipache
6
+
7
+ def initialize(hipache_ip)
8
+ @hipache_ip = hipache_ip
9
+ end
10
+
11
+ def online?
12
+ redis.ping
13
+ rescue Redis::CannotConnectError
14
+ false
15
+ end
16
+
17
+ def wait_for_online
18
+ tries = 0
19
+ while !online? and tries < 5
20
+ Kernel.sleep 1
21
+ tries += 1
22
+ end
23
+ end
24
+
25
+ def register(container, hostname, ip_address, port)
26
+ wait_for_online
27
+
28
+ raise "Cannot connect to Redis server, registration failed" unless online?
29
+
30
+ unless redis.lrange("frontend:#{hostname}", 0, -1).empty?
31
+ redis.del("frontend:#{hostname}")
32
+ end
33
+
34
+ redis.rpush("frontend:#{hostname}", container)
35
+ redis.rpush("frontend:#{hostname}", "http://#{ip_address}:#{port}")
36
+ end
37
+
38
+ def unregister(container, hostname, ip_address, port)
39
+ if online?
40
+ redis.lrem("frontend:#{hostname}", 0, "http://#{ip_address}:#{port}")
41
+ end
42
+ end
43
+
44
+ def status
45
+ mappings = {}
46
+ if online?
47
+ redis.keys("frontend:*").each do |key|
48
+ host = key.gsub(/^frontend:/, "")
49
+ mappings[host] = redis.lrange(key, 1, -1)
50
+ end
51
+ end
52
+ mappings
53
+ end
54
+
55
+ private
56
+
57
+ def redis
58
+ @redis ||= Redis.new(host: @hipache_ip, port: 16379)
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,48 @@
1
+ module Dockistrano
2
+
3
+ class Registry
4
+
5
+ attr_reader :name
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ end
10
+
11
+ class RepositoryNotFoundInRegistry < StandardError
12
+ end
13
+
14
+ def tags_for_image(image_name)
15
+ result = MultiJson.load(get("repositories", image_name, "tags").body)
16
+ if result["error"]
17
+ if result["error"] == "Repository not found"
18
+ raise RepositoryNotFoundInRegistry.new("Could not find repository #{image_name} in registry #{name}")
19
+ else
20
+ raise result["error"]
21
+ end
22
+ else
23
+ result
24
+ end
25
+ end
26
+
27
+ def latest_id_for_image(image_name, tag)
28
+ response = get("repositories", image_name, "tags", tag)
29
+ if response.kind_of?(Net::HTTPNotFound)
30
+ nil
31
+ else
32
+ MultiJson.load(response.body)
33
+ end
34
+ end
35
+
36
+ def to_s
37
+ name
38
+ end
39
+
40
+ private
41
+
42
+ def get(*url)
43
+ uri = URI.parse("http://#{name}/v1/#{url.join("/")}")
44
+ Net::HTTP.get_response(uri)
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,331 @@
1
+ require 'yaml'
2
+ require 'net/http'
3
+
4
+ module Dockistrano
5
+
6
+ class Service
7
+
8
+ attr_reader :dependencies, :config, :image_name, :registry,
9
+ :tag, :test_command, :provides_env, :backing_service_env,
10
+ :data_directories, :environment, :host, :additional_commands,
11
+ :mount_src
12
+
13
+ attr_writer :tag
14
+
15
+ class ConfigurationFileMissing < StandardError
16
+ end
17
+
18
+ def self.factory(path, environment="default")
19
+ config = if File.exists?(File.join(path, "config", "dockistrano.yml"))
20
+ YAML.load_file(File.join(path, "config", "dockistrano.yml"))
21
+ elsif File.exists?(File.join(path, "dockistrano.yml"))
22
+ YAML.load_file(File.join(path, "dockistrano.yml"))
23
+ else
24
+ raise ConfigurationFileMissing
25
+ end
26
+
27
+ environment ||= "default"
28
+
29
+ Service.new(config, environment)
30
+ end
31
+
32
+ def initialize(config, environment="default")
33
+ @full_config = config
34
+ self.environment = environment
35
+ end
36
+
37
+ def config=(config)
38
+ @config = config
39
+ @dependencies = config["dependencies"] || {}
40
+ @image_name ||= config["image_name"] || Git.repository_name
41
+ @tag ||= config["tag"] || Git.branch
42
+ @registry ||= config["registry"]
43
+ @host ||= config["host"]
44
+ @test_command = config["test_command"]
45
+ @mount_src = config["mount_src"]
46
+ @provides_env = config["provides_env"] || {}
47
+ @additional_commands = config["additional_commands"] || {}
48
+ @data_directories = config["data_directories"] || []
49
+ @backing_service_env ||= {}
50
+ @backing_service_env.merge!(config["backing_service_env"] || {})
51
+
52
+ config["environment"] ||= {}
53
+ end
54
+
55
+ class EnvironmentNotFoundInConfiguration < StandardError
56
+ end
57
+
58
+ def environment=(environment)
59
+ if @full_config[environment]
60
+ self.config = @full_config[environment]
61
+ else
62
+ raise EnvironmentNotFoundInConfiguration.new("Environment '#{environment}' not found in configuration (image: #{@full_config["image_name"]}), available: #{@full_config.keys.join(", ")}")
63
+ end
64
+ end
65
+
66
+ def registry_instance
67
+ @registry_instance ||= Registry.new(registry)
68
+ end
69
+
70
+ def image_id
71
+ Docker.image_id(full_image_name)
72
+ rescue Dockistrano::Docker::ImageNotFound
73
+ nil
74
+ end
75
+
76
+ def full_image_name
77
+ "#{registry}/#{image_name}:#{tag}"
78
+ end
79
+
80
+ # Builds a new image for this service
81
+ def build
82
+ previous_image_id = image_id
83
+ Docker.build(full_image_name)
84
+ if previous_image_id == image_id
85
+ # If the image id hasn't changed the build was not successfull
86
+ false
87
+ else
88
+ true
89
+ end
90
+ end
91
+
92
+ # Tests the image of this services by running a test command
93
+ def test
94
+ environment = "test"
95
+ unless test_command.nil? or test_command.empty?
96
+ ensure_backing_services
97
+ create_data_directories
98
+ Docker.exec(full_image_name, command: test_command, e: checked_environment_variables, v: volumes)
99
+ else
100
+ true
101
+ end
102
+ end
103
+
104
+ # Ensures that the right backing services are running to execute this services
105
+ # When a backing services is not running it is started
106
+ def ensure_backing_services
107
+ backing_services.each do |name, service|
108
+ service.start unless service.running?
109
+ end
110
+ end
111
+
112
+ # Stops the container of the current service
113
+ def stop
114
+ if !host.nil?
115
+ hipache = Hipache.new(ENV['DOCKER_HOST_IP'])
116
+ if host.kind_of?(String)
117
+ hipache.unregister(image_name, host, ip_address, port)
118
+ else
119
+ host.each do |hostname, port|
120
+ hipache.unregister(image_name, hostname, ip_address, port)
121
+ end
122
+ end
123
+ end
124
+
125
+ Docker.stop_all_containers_from_image(full_image_name)
126
+ end
127
+
128
+ # Returns if this service is running
129
+ def running?
130
+ Docker.running_container_id(full_image_name)
131
+ end
132
+
133
+ # Pulls backing services for this service
134
+ def pull_backing_services
135
+ backing_services.each do |name, service|
136
+ service.pull
137
+ end
138
+ end
139
+
140
+ # Pulls the service's container
141
+ def pull
142
+ Dockistrano::Docker.pull("#{registry}/#{image_name}", tag_with_fallback)
143
+ end
144
+
145
+ # Pushes the local image for this service to the registry
146
+ def push
147
+ Dockistrano::Docker.push("#{registry}/#{image_name}", tag)
148
+ end
149
+
150
+ # Starts this service
151
+ def start(options={})
152
+ ensure_backing_services
153
+ create_data_directories
154
+ environment = checked_environment_variables
155
+
156
+ if additional_commands.any?
157
+ additional_commands.each do |name, command|
158
+ Docker.run(full_image_name, e: environment, v: volumes, p: ports, d: true, command: command)
159
+ end
160
+ end
161
+
162
+ Docker.run(full_image_name, e: environment, v: volumes, p: ports, d: true)
163
+
164
+ if !host.nil?
165
+ hipache = Hipache.new(ENV['DOCKER_HOST_IP'])
166
+ if host.kind_of?(String)
167
+ hipache.register(image_name, host, ip_address, port)
168
+ else
169
+ host.each do |hostname, port|
170
+ hipache.register(image_name, hostname, ip_address, port)
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ # Runs a command in this container
177
+ def run(command, options={})
178
+ Docker.run(full_image_name, command: command, e: environment_variables, v: volumes, p: ports)
179
+ end
180
+
181
+ # Executes a command in this container
182
+ def exec(command, options={})
183
+ create_data_directories
184
+ Docker.exec(full_image_name, command: command, e: environment_variables, v: volumes, p: ports)
185
+ end
186
+
187
+ # Starts a console in the docker container
188
+ def console(command, options={})
189
+ create_data_directories
190
+ Docker.console(full_image_name, command: command, e: environment_variables, v: volumes, p: ports)
191
+ end
192
+
193
+ # Lists all backing services for this service
194
+ def backing_services
195
+ @backing_services ||= {}.tap do |hash|
196
+ dependencies.collect do |name, config|
197
+ hash[name] = ServiceDependency.factory(self, name, config)
198
+ end
199
+ end
200
+ end
201
+
202
+ # Returns an array of environment variables
203
+ def environment_variables
204
+ vars = {}
205
+
206
+ config["environment"].each do |name, value|
207
+ vars[name.upcase] = value
208
+ end
209
+
210
+ backing_services.each do |name, backing_service|
211
+ vars["#{name.upcase}_IP"] = backing_service.ip_address
212
+ vars["#{name.upcase}_PORT"] = backing_service.port
213
+
214
+ backing_service.backing_service_env.each do |k,v|
215
+ vars["#{name.upcase}_#{k.upcase}"] = v
216
+ end
217
+
218
+ vars.merge!(backing_service.environment_variables)
219
+ end
220
+
221
+ vars.merge!(provides_env)
222
+ vars.each do |key, value|
223
+ vars.each do |replacement_key, replacement_value|
224
+ unless vars[key].nil? or replacement_value.nil?
225
+ vars[key] = vars[key].gsub('$'+replacement_key, replacement_value)
226
+ end
227
+ end
228
+ end
229
+
230
+ vars
231
+ end
232
+
233
+ # Returns the mounted volumes for this service
234
+ def volumes
235
+ [].tap do |volumes|
236
+ volumes << "/dockistrano/#{image_name.gsub("-", "_")}/data:/dockistrano/data"
237
+ if mount_src and !mount_src.empty?
238
+ volumes << "/dockistrano/#{image_name.gsub("-", "_")}/src:#{mount_src}"
239
+ end
240
+ end
241
+ end
242
+
243
+ def directories_required_on_host
244
+ (volumes.collect { |v| v.split(":").first } + backing_services.values.map(&:directories_required_on_host)).flatten
245
+ end
246
+
247
+ # Returns a list of available tags in the registry for the image
248
+ def available_tags
249
+ @available_tags ||= begin
250
+ registry_instance.tags_for_image(full_image_name)
251
+ rescue Dockistrano::Registry::RepositoryNotFoundInRegistry
252
+ []
253
+ end
254
+ end
255
+
256
+ class NoTagFoundForImage < StandardError
257
+ end
258
+
259
+ # Returns the tag that is available with fallback.
260
+ def tag_with_fallback
261
+ fallback_tags = [tag, "develop", "master", "latest"]
262
+
263
+ begin
264
+ tag_suggestion = fallback_tags.shift
265
+ final_tag = tag_suggestion if available_tags.include?(tag_suggestion)
266
+ end while !final_tag and fallback_tags.any?
267
+
268
+ if final_tag
269
+ final_tag
270
+ else
271
+ raise NoTagFoundForImage.new("No tag found for image #{image_name}, available tags: #{available_tags}")
272
+ end
273
+ end
274
+
275
+ def ip_address
276
+ container_settings["NetworkSettings"]["IPAddress"] if running?
277
+ end
278
+
279
+ def port
280
+ container_settings["NetworkSettings"]["PortMapping"]["Tcp"].keys.first if running?
281
+ end
282
+
283
+ def ports
284
+ (config["ports"] || {}).collect { |k,v| "#{k}:#{v}" }
285
+ end
286
+
287
+ def attach
288
+ Docker.attach(Docker.running_container_id(full_image_name))
289
+ end
290
+
291
+ def logs
292
+ Docker.logs(Docker.last_run_container_id(full_image_name))
293
+ end
294
+
295
+ def create_data_directories
296
+ if data_directories.any?
297
+ image_config = Docker.inspect_image(full_image_name)
298
+ image_user = image_config["container_config"]["User"]
299
+
300
+ command = "mkdir -p #{data_directories.collect { |dir| "/dockistrano/data/#{dir}"}.join(" ") }; "
301
+ command += "chown #{image_user}:#{image_user} #{data_directories.collect { |dir| "/dockistrano/data/#{dir}"}.join(" ") }"
302
+ bash_command = "/bin/bash -c '#{command}'"
303
+ Docker.run(full_image_name, command: bash_command, v: volumes, e: environment_variables, u: "root")
304
+ end
305
+ end
306
+
307
+ def newer_version_available?
308
+ registry_image_id = registry_instance.latest_id_for_image(image_name, tag)
309
+ registry_image_id and image_id != registry_image_id
310
+ end
311
+
312
+ private
313
+
314
+ def container_settings
315
+ @container_settings ||= Docker.inspect_container(Docker.running_container_id(full_image_name))
316
+ end
317
+
318
+ class EnvironmentVariablesMissing < StandardError
319
+ end
320
+
321
+ def checked_environment_variables
322
+ vars = environment_variables
323
+ if (empty_vars = vars.select { |k,v| v.nil? }).any?
324
+ raise EnvironmentVariablesMissing.new("Unable to execute container because of missing environment variables: #{empty_vars.keys.join(", ")}")
325
+ else
326
+ vars
327
+ end
328
+ end
329
+
330
+ end
331
+ end
@@ -0,0 +1,114 @@
1
+ module Dockistrano
2
+
3
+ class ServiceDependency
4
+
5
+ # Creates a new service instance based on the name and configuration. When
6
+ # configuration is not local, the configuration is fetched from Github and
7
+ # processed.
8
+ def self.factory(service, name, config)
9
+ ServiceDependency.new(service, name, config).backing_service
10
+ end
11
+
12
+ class DefaultEnvironmentMissingInConfiguration < StandardError
13
+ end
14
+
15
+ attr_reader :service, :name, :config
16
+
17
+ def initialize(service, name, config)
18
+ @service = service
19
+ @name = name
20
+ @config = config
21
+ end
22
+
23
+ def backing_service
24
+ @backing_service ||= begin
25
+ backing_service = Service.new("default" => {
26
+ "registry" => service.registry,
27
+ "image_name" => name,
28
+ "tag" => service.tag,
29
+ "backing_service_env" => config
30
+ })
31
+
32
+ backing_service.tag = tag_with_fallback(service.tag)
33
+
34
+ begin
35
+ loaded_config = load_config
36
+ if loaded_config and loaded_config["default"]
37
+ backing_service.config = loaded_config["default"]
38
+ else
39
+ raise DefaultEnvironmentMissingInConfiguration.new("No 'default' configuration found in /dockistrano.yml file in #{name} container.")
40
+ end
41
+ rescue ContainerConfigurationMissing
42
+ puts "Warning: no configuration file found for service #{name}."
43
+ rescue HostDirectoriesMissing
44
+ puts "Error: missing host directory configuration for #{name}. Please execute `doc setup`"
45
+ exit 1
46
+ end
47
+
48
+ backing_service
49
+ end
50
+ end
51
+
52
+ def load_config
53
+ load_from_cache || load_from_image
54
+ end
55
+
56
+ def load_from_cache
57
+ image_id = backing_service.image_id
58
+ if image_id and File.exists?("tmp/configuration_cache/#{image_id}")
59
+ YAML.load_file("tmp/configuration_cache/#{image_id}")
60
+ else
61
+ nil
62
+ end
63
+ end
64
+
65
+ class ContainerConfigurationMissing < StandardError
66
+ end
67
+
68
+ class HostDirectoriesMissing < StandardError
69
+ end
70
+
71
+ def load_from_image
72
+ raw_config = Docker.run(backing_service.full_image_name, command: "cat /dockistrano.yml")
73
+ if raw_config.empty? or raw_config.include?("No such file or directory")
74
+ if raw_config.include?("failed to mount")
75
+ raise HostDirectoriesMissing
76
+ else
77
+ raise ContainerConfigurationMissing
78
+ end
79
+ else
80
+ FileUtils.mkdir_p("tmp/configuration_cache")
81
+ file = File.open("tmp/configuration_cache/#{backing_service.image_id}", "w+")
82
+ file.write(raw_config)
83
+ file.close
84
+
85
+ config = YAML.load(raw_config)
86
+ end
87
+ end
88
+
89
+ class NoTagFoundForImage < StandardError
90
+ end
91
+
92
+ def tag_with_fallback(tag)
93
+ fallback_tags = [tag, "develop", "master", "latest"]
94
+
95
+ available_tags = Docker.tags_for_image("#{backing_service.registry}/#{backing_service.image_name}")
96
+
97
+ begin
98
+ tag_suggestion = fallback_tags.shift
99
+ final_tag = tag_suggestion if available_tags.include?(tag_suggestion)
100
+ end while !final_tag and fallback_tags.any?
101
+
102
+ if final_tag
103
+ final_tag
104
+ else
105
+ raise NoTagFoundForImage.new("No tag found for image #{backing_service.image_name}, locally available tags: #{available_tags} `doc pull` for more tags from repository.")
106
+ end
107
+ end
108
+
109
+ def self.clear_cache
110
+ `rm -rf tmp/configuration_cache/`
111
+ end
112
+
113
+ end
114
+ end
@@ -0,0 +1,3 @@
1
+ module Dockistrano
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,14 @@
1
+ require "dockistrano/cli"
2
+ require "dockistrano/docker"
3
+ require "dockistrano/command_line"
4
+ require "dockistrano/service"
5
+ require "dockistrano/service_dependency"
6
+ require "dockistrano/version"
7
+ require "dockistrano/git"
8
+ require "dockistrano/registry"
9
+ require "dockistrano/hipache"
10
+
11
+ module Dockistrano
12
+ # Your code goes here...
13
+
14
+ end