centurion 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ require_relative 'docker_server'
2
+ require_relative 'logging'
3
+
4
+ module Centurion; end
5
+
6
+ class Centurion::DockerServerGroup
7
+ include Enumerable
8
+ include Centurion::Logging
9
+
10
+ attr_reader :hosts
11
+
12
+ def initialize(hosts, docker_path)
13
+ raise ArgumentError.new('Bad Host list!') if hosts.nil? || hosts.empty?
14
+ @hosts = hosts.map { |hostname| Centurion::DockerServer.new(hostname, docker_path) }
15
+ end
16
+
17
+ def each(&block)
18
+ @hosts.each do |host|
19
+ info "----- Connecting to Docker on #{host.hostname} -----"
20
+ block.call(host)
21
+ end
22
+ end
23
+
24
+ def each_in_parallel(&block)
25
+ threads = @hosts.map do |host|
26
+ Thread.new { block.call(host) }
27
+ end
28
+
29
+ threads.each { |t| t.join }
30
+ end
31
+ end
@@ -0,0 +1,121 @@
1
+ require 'excon'
2
+ require 'json'
3
+ require 'uri'
4
+
5
+ module Centurion; end
6
+
7
+ class Centurion::DockerViaApi
8
+ def initialize(hostname, port)
9
+ @base_uri = "http://#{hostname}:#{port}"
10
+
11
+ configure_excon_globally
12
+ end
13
+
14
+ def ps(options={})
15
+ path = "/v1.7/containers/json"
16
+ path += "?all=1" if options[:all]
17
+ response = Excon.get(@base_uri + path)
18
+
19
+ raise unless response.status == 200
20
+ JSON.load(response.body)
21
+ end
22
+
23
+ def inspect_image(image, tag = "latest")
24
+ repository = "#{image}:#{tag}"
25
+ path = "/v1.7/images/#{repository}/json"
26
+
27
+ response = Excon.get(
28
+ @base_uri + path,
29
+ :headers => {'Accept' => 'application/json'}
30
+ )
31
+ raise response.inspect unless response.status == 200
32
+ JSON.load(response.body)
33
+ end
34
+
35
+ def old_containers_for_port(host_port)
36
+ old_containers = ps(all: true).select do |container|
37
+ container["Status"] =~ /^Exit /
38
+ end.select do |container|
39
+ inspected = inspect_container container["Id"]
40
+ container_listening_on_port?(inspected, host_port)
41
+ end
42
+ old_containers
43
+ end
44
+
45
+ def remove_container(container_id)
46
+ path = "/v1.7/containers/#{container_id}"
47
+ response = Excon.delete(
48
+ @base_uri + path,
49
+ )
50
+ raise response.inspect unless response.status == 204
51
+ true
52
+ end
53
+
54
+ def stop_container(container_id)
55
+ path = "/v1.7/containers/#{container_id}/stop?t=30"
56
+ response = Excon.post(
57
+ @base_uri + path,
58
+ )
59
+ raise response.inspect unless response.status == 204
60
+ true
61
+ end
62
+
63
+ def create_container(configuration)
64
+ path = "/v1.7/containers/create"
65
+ response = Excon.post(
66
+ @base_uri + path,
67
+ :body => configuration.to_json,
68
+ :headers => { "Content-Type" => "application/json" }
69
+ )
70
+ raise response.inspect unless response.status == 201
71
+ JSON.load(response.body)
72
+ end
73
+
74
+ def start_container(container_id, configuration)
75
+ path = "/v1.7/containers/#{container_id}/start"
76
+ response = Excon.post(
77
+ @base_uri + path,
78
+ :body => configuration.to_json,
79
+ :headers => { "Content-Type" => "application/json" }
80
+ )
81
+ case response.status
82
+ when 204
83
+ true
84
+ when 500
85
+ fail "Failed to start container! \"#{response.body}\""
86
+ else
87
+ raise response.inspect
88
+ end
89
+ end
90
+
91
+ def inspect_container(container_id)
92
+ path = "/v1.7/containers/#{container_id}/json"
93
+ response = Excon.get(
94
+ @base_uri + path,
95
+ )
96
+ raise response.inspect unless response.status == 200
97
+ JSON.load(response.body)
98
+ end
99
+
100
+ private
101
+
102
+ # use on result of inspect container, not on an item in a list
103
+ def container_listening_on_port?(container, port)
104
+ port_bindings = container['HostConfig']['PortBindings']
105
+ return false unless port_bindings
106
+
107
+ port_bindings.values.flatten.any? do |port_binding|
108
+ port_binding['HostPort'].to_i == port.to_i
109
+ end
110
+ end
111
+
112
+ def configure_excon_globally
113
+ Excon.defaults[:connect_timeout] = 120
114
+ Excon.defaults[:read_timeout] = 120
115
+ Excon.defaults[:write_timeout] = 120
116
+ Excon.defaults[:debug_request] = true
117
+ Excon.defaults[:debug_response] = true
118
+ Excon.defaults[:nonblock] = false
119
+ Excon.defaults[:tcp_nodelay] = true
120
+ end
121
+ end
@@ -0,0 +1,71 @@
1
+ require 'pty'
2
+ require_relative 'logging'
3
+
4
+ module Centurion; end
5
+
6
+ class Centurion::DockerViaCli
7
+ include Centurion::Logging
8
+
9
+ def initialize(hostname, port, docker_path)
10
+ @docker_host = "tcp://#{hostname}:#{port}"
11
+ @docker_path = docker_path
12
+ end
13
+
14
+ def pull(image, tag='latest')
15
+ info "Using CLI to pull"
16
+ echo("#{@docker_path} -H=#{@docker_host} pull #{image}:#{tag}")
17
+ end
18
+
19
+ def tail(container_id)
20
+ info "Tailing the logs on #{container_id}"
21
+ echo("#{@docker_path} -H=#{@docker_host} logs -f #{container_id}")
22
+ end
23
+
24
+ def attach(container_id)
25
+ Process.exec("#{@docker_path} -H=#{@docker_host} attach #{container_id}")
26
+ end
27
+
28
+ private
29
+
30
+ def echo(command)
31
+ if Thread.list.find_all { |t| t.status == 'run' }.count > 1
32
+ run_without_echo(command)
33
+ else
34
+ run_with_echo(command)
35
+ end
36
+ end
37
+
38
+ def run_without_echo(command)
39
+ output = Queue.new
40
+ output_thread = Thread.new do
41
+ while true do
42
+ begin
43
+ puts output.pop
44
+ rescue => e
45
+ info "Rescuing... #{e.message}"
46
+ end
47
+ end
48
+ end
49
+
50
+ IO.popen(command) do |io|
51
+ io.each_line { |line| output << line }
52
+ end
53
+
54
+ output_thread.kill
55
+
56
+ unless $?.success?
57
+ raise "The command failed with a non-zero exit status: #{$?.exitstatus}"
58
+ end
59
+ end
60
+
61
+ def run_with_echo( command )
62
+ $stdout.sync = true
63
+ $stderr.sync = true
64
+ IO.popen(command) do |io|
65
+ io.each_char { |char| print char }
66
+ end
67
+ unless $?.success?
68
+ raise "The command failed with a non-zero exit status: #{$?.exitstatus}"
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ require 'logger/colors'
2
+ require 'logger'
3
+
4
+ module Centurion; end
5
+
6
+ module Centurion::Logging
7
+ def info(*args)
8
+ log.info args.join(' ')
9
+ end
10
+
11
+ def warn(*args)
12
+ log.warn args.join(' ')
13
+ end
14
+
15
+ def error(*args)
16
+ log.error args.join(' ')
17
+ end
18
+
19
+ def debug(*args)
20
+ log.debug args.join(' ')
21
+ end
22
+
23
+ private
24
+
25
+ def log(*args)
26
+ @@logger ||= Logger.new(STDOUT)
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Centurion
2
+ VERSION = '1.0.6'
3
+ end
@@ -0,0 +1,177 @@
1
+ require 'thread'
2
+ require 'excon'
3
+ require 'centurion/deploy'
4
+
5
+ task :deploy do
6
+ invoke 'deploy:get_image'
7
+ invoke 'deploy:stop'
8
+ invoke 'deploy:start_new'
9
+ invoke 'deploy:cleanup'
10
+ end
11
+
12
+ task :deploy_console do
13
+ invoke 'deploy:get_image'
14
+ invoke 'deploy:stop'
15
+ invoke 'deploy:launch_console'
16
+ invoke 'deploy:cleanup'
17
+ end
18
+
19
+ task :rolling_deploy do
20
+ invoke 'deploy:get_image'
21
+ invoke 'deploy:rolling_deploy'
22
+ invoke 'deploy:cleanup'
23
+ end
24
+
25
+ task :stop => ['deploy:stop']
26
+
27
+ namespace :deploy do
28
+ include Centurion::Deploy
29
+
30
+ task :get_image do
31
+ invoke 'deploy:pull_image'
32
+ invoke 'deploy:determine_image_id_from_first_server'
33
+ invoke 'deploy:verify_image'
34
+ end
35
+
36
+ # stop
37
+ # - remote: list
38
+ # - remote: stop
39
+ task :stop do
40
+ on_each_docker_host { |server| stop_containers(server, fetch(:port_bindings)) }
41
+ end
42
+
43
+ # start
44
+ # - remote: create
45
+ # - remote: start
46
+ # - remote: inspect container
47
+ task :start_new do
48
+ on_each_docker_host do |server|
49
+ start_new_container(
50
+ server,
51
+ fetch(:image_id),
52
+ fetch(:port_bindings),
53
+ fetch(:binds),
54
+ fetch(:env_vars)
55
+ )
56
+ end
57
+ end
58
+
59
+ task :launch_console do
60
+ on_each_docker_host do |server|
61
+ launch_console(
62
+ server,
63
+ fetch(:image_id),
64
+ fetch(:port_bindings),
65
+ fetch(:binds),
66
+ fetch(:env_vars)
67
+ )
68
+ end
69
+ end
70
+
71
+ task :rolling_deploy do
72
+ on_each_docker_host do |server|
73
+ stop_containers(server, fetch(:port_bindings))
74
+
75
+ start_new_container(
76
+ server,
77
+ fetch(:image_id),
78
+ fetch(:port_bindings),
79
+ fetch(:binds),
80
+ fetch(:env_vars)
81
+ )
82
+
83
+ fetch(:port_bindings).each_pair do |container_port, host_ports|
84
+ wait_for_http_status_ok(
85
+ server,
86
+ host_ports.first['HostPort'],
87
+ fetch(:status_endpoint, '/status/check'),
88
+ fetch(:image),
89
+ fetch(:tag),
90
+ fetch(:rolling_deploy_wait_time, 5),
91
+ fetch(:rolling_deploy_retries, 24)
92
+ )
93
+ end
94
+
95
+ wait_for_load_balancer_check_interval
96
+ end
97
+ end
98
+
99
+ task :cleanup do
100
+ on_each_docker_host do |target_server|
101
+ cleanup_containers(target_server, fetch(:port_bindings))
102
+ end
103
+ end
104
+
105
+ task :determine_image_id do
106
+ registry = Centurion::DockerRegistry.new()
107
+ exact_image = registry.digest_for_tag(fetch(:image), fetch(:tag))
108
+ set :image_id, exact_image
109
+ $stderr.puts "RESOLVED #{fetch(:image)}:#{fetch(:tag)} => #{exact_image[0..11]}"
110
+ end
111
+
112
+ task :determine_image_id_from_first_server do
113
+ on_each_docker_host do |target_server|
114
+ image_detail = target_server.inspect_image(fetch(:image), fetch(:tag))
115
+ exact_image = image_detail["id"]
116
+ set :image_id, exact_image
117
+ $stderr.puts "RESOLVED #{fetch(:image)}:#{fetch(:tag)} => #{exact_image[0..11]}"
118
+ break
119
+ end
120
+ end
121
+
122
+ task :pull_image do
123
+ $stderr.puts "Fetching image #{fetch(:image)}:#{fetch(:tag)} IN PARALLEL\n"
124
+
125
+ target_servers = Centurion::DockerServerGroup.new(fetch(:hosts), fetch(:docker_path))
126
+ target_servers.each_in_parallel do |target_server|
127
+ target_server.pull(fetch(:image), fetch(:tag))
128
+ end
129
+ end
130
+
131
+ task :verify_image do
132
+ on_each_docker_host do |target_server|
133
+ image_detail = target_server.inspect_image(fetch(:image), fetch(:tag))
134
+ found_image_id = image_detail["id"]
135
+
136
+ if found_image_id == fetch(:image_id)
137
+ $stderr.puts "Image #{found_image_id[0..7]} found on #{target_server.hostname}"
138
+ else
139
+ raise "Did not find image #{fetch(:image_id)} on host #{target_server.hostname}!"
140
+ end
141
+
142
+ # Print the container config
143
+ image_detail["container_config"].each_pair do |key,value|
144
+ $stderr.puts "\t#{key} => #{value.inspect}"
145
+ end
146
+ end
147
+ end
148
+
149
+ task :promote_from_staging do
150
+ if fetch(:environment) == 'staging'
151
+ $stderr.puts "\n\nYour target environment needs to not be 'staging' to promote from staging."
152
+ exit(1)
153
+ end
154
+
155
+ starting_environment = current_environment
156
+
157
+ # Set our env to staging so we can grab the current tag.
158
+ invoke 'environment:staging'
159
+
160
+ staging_tags = get_current_tags_for(fetch(:image)).map { |t| t[:tags] }.flatten.uniq
161
+
162
+ if staging_tags.size != 1
163
+ $stderr.puts "\n\nUh, oh: Not sure which staging tag to deploy! Found:(#{staging_tags.join(', ')})"
164
+ exit(1)
165
+ end
166
+
167
+ $stderr.puts "Staging environment has #{staging_tags.first} deployed."
168
+
169
+ # Make sure that we set our env back to production, then update the tag.
170
+ set_current_environment(starting_environment)
171
+ set :tag, staging_tags.first
172
+
173
+ $stderr.puts "Deploying #{fetch(:tag)} to the #{starting_environment} environment"
174
+
175
+ invoke 'deploy'
176
+ end
177
+ end
@@ -0,0 +1,24 @@
1
+ task :info => 'info:default'
2
+
3
+ namespace :info do
4
+ task :default do
5
+ puts "Environment: #{fetch(:environment)}"
6
+ puts "Project: #{fetch(:project)}"
7
+ puts "Image: #{fetch(:image)}"
8
+ puts "Tag: #{fetch(:tag)}"
9
+ puts "Port Bindings: #{fetch(:port_bindings).inspect}"
10
+ puts "Mount Point: #{fetch(:binds).inspect}"
11
+ puts "ENV: #{fetch(:env_vars).inspect}"
12
+ puts "Hosts: #{fetch(:hosts).inspect}"
13
+ end
14
+
15
+ task :run_command do
16
+ example_host = fetch(:hosts).first
17
+ env_args = ""
18
+ fetch(:env_vars, {}).each_pair do |name,value|
19
+ env_args << "-e #{name}='#{value}' "
20
+ end
21
+ volume_args = fetch(:binds, []).map {|bind| "-v #{bind}"}.join(" ")
22
+ puts "docker -H=tcp://#{example_host} run #{env_args} #{volume_args} #{fetch(:image)}:#{fetch(:tag)}"
23
+ end
24
+ end