centurion 1.0.6

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,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