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