dockistrano 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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