centurion 1.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/CONTRIBUTORS.md +42 -0
- data/Gemfile +6 -0
- data/LICENSE +19 -0
- data/README.md +239 -0
- data/Rakefile +15 -0
- data/bin/centurion +70 -0
- data/bin/centurionize +60 -0
- data/centurion.gemspec +40 -0
- data/lib/capistrano_dsl.rb +91 -0
- data/lib/centurion.rb +5 -0
- data/lib/centurion/deploy.rb +145 -0
- data/lib/centurion/deploy_dsl.rb +94 -0
- data/lib/centurion/docker_registry.rb +35 -0
- data/lib/centurion/docker_server.rb +58 -0
- data/lib/centurion/docker_server_group.rb +31 -0
- data/lib/centurion/docker_via_api.rb +121 -0
- data/lib/centurion/docker_via_cli.rb +71 -0
- data/lib/centurion/logging.rb +28 -0
- data/lib/centurion/version.rb +3 -0
- data/lib/tasks/deploy.rake +177 -0
- data/lib/tasks/info.rake +24 -0
- data/lib/tasks/list.rake +52 -0
- data/spec/capistrano_dsl_spec.rb +67 -0
- data/spec/deploy_dsl_spec.rb +104 -0
- data/spec/deploy_spec.rb +220 -0
- data/spec/docker_server_group_spec.rb +31 -0
- data/spec/docker_server_spec.rb +43 -0
- data/spec/docker_via_api_spec.rb +111 -0
- data/spec/docker_via_cli_spec.rb +42 -0
- data/spec/logging_spec.rb +41 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/matchers/capistrano_dsl_matchers.rb +13 -0
- metadata +243 -0
@@ -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,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
|
data/lib/tasks/info.rake
ADDED
@@ -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
|