rory-deploy 1.8.4.1
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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/CONTRIBUTORS.md +77 -0
- data/Gemfile +5 -0
- data/LICENSE +19 -0
- data/README.md +574 -0
- data/Rakefile +15 -0
- data/bin/rory +80 -0
- data/bin/rory-gen-config +79 -0
- data/lib/capistrano_dsl.rb +91 -0
- data/lib/centurion.rb +9 -0
- data/lib/centurion/deploy.rb +139 -0
- data/lib/centurion/deploy_dsl.rb +180 -0
- data/lib/centurion/docker_registry.rb +89 -0
- data/lib/centurion/docker_server.rb +79 -0
- data/lib/centurion/docker_server_group.rb +33 -0
- data/lib/centurion/docker_via_api.rb +166 -0
- data/lib/centurion/docker_via_cli.rb +81 -0
- data/lib/centurion/dogestry.rb +92 -0
- data/lib/centurion/logging.rb +28 -0
- data/lib/centurion/service.rb +218 -0
- data/lib/centurion/shell.rb +46 -0
- data/lib/centurion/version.rb +3 -0
- data/lib/core_ext/numeric_bytes.rb +94 -0
- data/lib/tasks/centurion.rake +15 -0
- data/lib/tasks/deploy.rake +250 -0
- data/lib/tasks/info.rake +24 -0
- data/lib/tasks/list.rake +56 -0
- data/rory-deploy.gemspec +33 -0
- data/spec/capistrano_dsl_spec.rb +67 -0
- data/spec/deploy_dsl_spec.rb +184 -0
- data/spec/deploy_spec.rb +212 -0
- data/spec/docker_registry_spec.rb +105 -0
- data/spec/docker_server_group_spec.rb +31 -0
- data/spec/docker_server_spec.rb +92 -0
- data/spec/docker_via_api_spec.rb +246 -0
- data/spec/docker_via_cli_spec.rb +91 -0
- data/spec/dogestry_spec.rb +73 -0
- data/spec/logging_spec.rb +41 -0
- data/spec/service_spec.rb +288 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/matchers/capistrano_dsl_matchers.rb +13 -0
- data/spec/support/matchers/exit_code_matches.rb +38 -0
- metadata +214 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# The centurion:setup task is always run first to do any prework needed
|
2
|
+
# to ensure a consistent and functioning environment
|
3
|
+
|
4
|
+
namespace :centurion do
|
5
|
+
task :setup => [:clean_environment]
|
6
|
+
|
7
|
+
task :clean_environment do
|
8
|
+
ENV.delete('DOCKER_HOST')
|
9
|
+
ENV.delete('DOCKER_TLS_VERIFY')
|
10
|
+
# Preserve original DOCKER_CERT_PATH for use by dogestry.
|
11
|
+
# See also Centurion::Dogestry#set_envs
|
12
|
+
set(:original_docker_cert_path, ENV['DOCKER_CERT_PATH'])
|
13
|
+
ENV.delete('DOCKER_CERT_PATH')
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,250 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'excon'
|
3
|
+
require 'centurion/deploy'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
task :deploy do
|
7
|
+
invoke 'deploy:get_image'
|
8
|
+
invoke 'deploy:stop'
|
9
|
+
invoke 'deploy:start_new'
|
10
|
+
invoke 'deploy:cleanup'
|
11
|
+
end
|
12
|
+
|
13
|
+
task :deploy_console do
|
14
|
+
invoke 'deploy:get_image'
|
15
|
+
#invoke 'deploy:stop'
|
16
|
+
invoke 'deploy:launch_console'
|
17
|
+
invoke 'deploy:cleanup'
|
18
|
+
end
|
19
|
+
|
20
|
+
task :rolling_deploy do
|
21
|
+
invoke 'deploy:get_image'
|
22
|
+
invoke 'deploy:rolling_deploy'
|
23
|
+
invoke 'deploy:cleanup'
|
24
|
+
end
|
25
|
+
|
26
|
+
task :stop => ['deploy:stop']
|
27
|
+
task :enter_container => ['deploy:enter_container']
|
28
|
+
|
29
|
+
namespace :dev do
|
30
|
+
task :export_only do
|
31
|
+
# This removes the known-to-be-problematic bundler env
|
32
|
+
# vars but doesn't try to sanitize the whole ENV. Doing
|
33
|
+
# so breaks things like rbenv which we need to use when
|
34
|
+
# testing. A /bin/bash -l will help further clean it up.
|
35
|
+
ENV.reject! { |(var, val)| var =~ /^BUNDLE_/ }
|
36
|
+
fetch(:env_vars).each { |(var, value)| ENV[var] = value }
|
37
|
+
exec fetch(:development_shell, '/bin/bash -l')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
namespace :deploy do
|
42
|
+
include Centurion::Deploy
|
43
|
+
|
44
|
+
namespace :dogestry do
|
45
|
+
task :validate_pull_image do
|
46
|
+
['aws_access_key_id', 'aws_secret_key', 's3_bucket'].each do |env_var|
|
47
|
+
unless fetch(env_var.to_sym)
|
48
|
+
error "\n\n#{env_var} is not defined."
|
49
|
+
exit(1)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
task :pull_image do
|
55
|
+
invoke 'deploy:dogestry:validate_pull_image'
|
56
|
+
|
57
|
+
# Create Centurion::Dogestry instance
|
58
|
+
registry = Centurion::Dogestry.new(
|
59
|
+
aws_access_key_id: fetch(:aws_access_key_id),
|
60
|
+
aws_secret_key: fetch(:aws_secret_key),
|
61
|
+
s3_bucket: fetch(:s3_bucket),
|
62
|
+
s3_region: fetch(:s3_region) || 'us-east-1',
|
63
|
+
tlsverify: fetch(:tlsverify),
|
64
|
+
tlscacert: fetch(:tlscacert),
|
65
|
+
tlscert: fetch(:tlscert),
|
66
|
+
original_docker_cert_path: fetch(:original_docker_cert_path)
|
67
|
+
)
|
68
|
+
|
69
|
+
target_servers = Centurion::DockerServerGroup.new(fetch(:hosts), fetch(:docker_path))
|
70
|
+
pull_hosts = []
|
71
|
+
|
72
|
+
target_servers.each do |target_server|
|
73
|
+
docker_host = "tcp://#{target_server.hostname}:#{target_server.port}"
|
74
|
+
pull_hosts.push(docker_host)
|
75
|
+
end
|
76
|
+
|
77
|
+
image_and_tag = "#{fetch(:image)}:#{fetch(:tag)}"
|
78
|
+
info "** Pulling image(#{image_and_tag}) from S3 to Docker Hosts: #{pull_hosts}"
|
79
|
+
|
80
|
+
registry.pull(image_and_tag, pull_hosts)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
task :get_image do
|
85
|
+
invoke 'deploy:pull_image'
|
86
|
+
invoke 'deploy:determine_image_id_from_first_server'
|
87
|
+
invoke 'deploy:verify_image'
|
88
|
+
end
|
89
|
+
|
90
|
+
# stop
|
91
|
+
# - remote: list
|
92
|
+
# - remote: stop
|
93
|
+
task :stop do
|
94
|
+
on_each_docker_host do |server|
|
95
|
+
stop_containers(server, defined_service, fetch(:stop_timeout, 30))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# start
|
100
|
+
# - remote: create
|
101
|
+
# - remote: start
|
102
|
+
# - remote: inspect container
|
103
|
+
task :start_new do
|
104
|
+
on_each_docker_host do |server|
|
105
|
+
start_new_container(server, defined_service, defined_restart_policy)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
task :launch_console do
|
110
|
+
on_first_docker_host do |server|
|
111
|
+
defined_service.port_bindings.clear
|
112
|
+
launch_console(server, defined_service)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
task :enter_container do
|
117
|
+
on_first_docker_host do |server|
|
118
|
+
enter_container(server, defined_service)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
task :rolling_deploy do
|
123
|
+
on_each_docker_host do |server|
|
124
|
+
service = defined_service
|
125
|
+
|
126
|
+
stop_containers(server, service, fetch(:stop_timeout, 30))
|
127
|
+
|
128
|
+
container = start_new_container(server, service, defined_restart_policy)
|
129
|
+
|
130
|
+
public_ports = service.public_ports - fetch(:rolling_deploy_skip_ports, [])
|
131
|
+
public_ports.each do |port|
|
132
|
+
wait_for_health_check_ok(
|
133
|
+
fetch(:health_check, method(:http_status_ok?)),
|
134
|
+
server,
|
135
|
+
container['Id'],
|
136
|
+
port,
|
137
|
+
fetch(:status_endpoint, '/'),
|
138
|
+
fetch(:image),
|
139
|
+
fetch(:tag),
|
140
|
+
fetch(:rolling_deploy_wait_time, 5),
|
141
|
+
fetch(:rolling_deploy_retries, 24)
|
142
|
+
)
|
143
|
+
end
|
144
|
+
|
145
|
+
wait_for_load_balancer_check_interval
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
task :cleanup do
|
150
|
+
on_each_docker_host do |server|
|
151
|
+
cleanup_containers(server, defined_service)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
task :determine_image_id do
|
156
|
+
registry = Centurion::DockerRegistry.new(
|
157
|
+
fetch(:docker_registry),
|
158
|
+
fetch(:registry_user),
|
159
|
+
fetch(:registry_password)
|
160
|
+
)
|
161
|
+
exact_image = registry.digest_for_tag(fetch(:image), fetch(:tag))
|
162
|
+
set :image_id, exact_image
|
163
|
+
info "RESOLVED #{fetch(:image)}:#{fetch(:tag)} => #{exact_image[0..11]}"
|
164
|
+
end
|
165
|
+
|
166
|
+
task :determine_image_id_from_first_server do
|
167
|
+
on_each_docker_host do |target_server|
|
168
|
+
image_detail = target_server.inspect_image(fetch(:image), fetch(:tag))
|
169
|
+
|
170
|
+
# Handle CamelCase in response from Docker API
|
171
|
+
# See https://github.com/newrelic/centurion/issues/85
|
172
|
+
exact_image = image_detail["id"] || image_detail["Id"]
|
173
|
+
|
174
|
+
set :image_id, exact_image
|
175
|
+
info "RESOLVED #{fetch(:image)}:#{fetch(:tag)} => #{exact_image[0..11]}"
|
176
|
+
break
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
task :pull_image do
|
181
|
+
if fetch(:no_pull)
|
182
|
+
info "--no-pull option specified: skipping pull"
|
183
|
+
next
|
184
|
+
end
|
185
|
+
|
186
|
+
info "Fetching image #{fetch(:image)}:#{fetch(:tag)} IN PARALLEL\n"
|
187
|
+
|
188
|
+
if fetch(:registry) == 'dogestry'
|
189
|
+
invoke 'deploy:dogestry:pull_image'
|
190
|
+
else
|
191
|
+
build_server_group.each_in_parallel do |target_server|
|
192
|
+
target_server.pull(fetch(:image), fetch(:tag))
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
task :verify_image do
|
198
|
+
on_each_docker_host do |target_server|
|
199
|
+
image_detail = target_server.inspect_image(fetch(:image), fetch(:tag))
|
200
|
+
|
201
|
+
# Handle CamelCase in response from Docker API
|
202
|
+
# See https://github.com/newrelic/centurion/issues/85
|
203
|
+
found_image_id = image_detail["id"] || image_detail["Id"]
|
204
|
+
|
205
|
+
if found_image_id == fetch(:image_id)
|
206
|
+
info "Image #{found_image_id[0..7]} found on #{target_server.hostname}"
|
207
|
+
else
|
208
|
+
raise "Did not find image #{fetch(:image_id)} on host #{target_server.hostname}!"
|
209
|
+
end
|
210
|
+
|
211
|
+
# Again, handle CamelCase in response from Docker API
|
212
|
+
container_config = image_detail["container_config"] || image_detail["ContainerConfig"]
|
213
|
+
|
214
|
+
# Print the container config
|
215
|
+
container_config.each_pair do |key,value|
|
216
|
+
info "\t#{key} => #{value.inspect}"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
task :promote_from_staging do
|
222
|
+
if fetch(:environment) == 'staging'
|
223
|
+
error "\n\nYour target environment needs to not be 'staging' to promote from staging."
|
224
|
+
exit(1)
|
225
|
+
end
|
226
|
+
|
227
|
+
starting_environment = current_environment
|
228
|
+
|
229
|
+
# Set our env to staging so we can grab the current tag.
|
230
|
+
invoke 'environment:staging'
|
231
|
+
|
232
|
+
staging_tags = get_current_tags_for(fetch(:image)).map { |t| t[:tags] }.flatten.uniq
|
233
|
+
|
234
|
+
if staging_tags.size != 1
|
235
|
+
error "\n\nUh, oh: Not sure which staging tag to deploy! Found:(#{staging_tags.join(', ')})"
|
236
|
+
exit(1)
|
237
|
+
end
|
238
|
+
|
239
|
+
info "Staging environment has #{staging_tags.first} deployed."
|
240
|
+
|
241
|
+
# Make sure that we set our env back to production, then update the tag.
|
242
|
+
set_current_environment(starting_environment)
|
243
|
+
set :tag, staging_tags.first
|
244
|
+
|
245
|
+
info "Deploying #{fetch(:tag)} to the #{starting_environment} environment"
|
246
|
+
|
247
|
+
invoke 'deploy'
|
248
|
+
end
|
249
|
+
|
250
|
+
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
|
data/lib/tasks/list.rake
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'centurion/docker_registry'
|
2
|
+
|
3
|
+
task :list do
|
4
|
+
invoke 'list:tags'
|
5
|
+
invoke 'list:running_containers'
|
6
|
+
end
|
7
|
+
|
8
|
+
namespace :list do
|
9
|
+
task :running_container_tags do
|
10
|
+
|
11
|
+
tags = get_current_tags_for(fetch(:image))
|
12
|
+
|
13
|
+
$stderr.puts "\n\nCurrent #{current_environment} tags for #{fetch(:image)}:\n\n"
|
14
|
+
tags.each do |info|
|
15
|
+
if info && !info[:tags].empty?
|
16
|
+
$stderr.puts "#{'%-20s' % info[:server]}: #{info[:tags].join(', ')}"
|
17
|
+
else
|
18
|
+
$stderr.puts "#{'%-20s' % info[:server]}: NO TAGS!"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
$stderr.puts "\nAll tags for this image: #{tags.map { |t| t[:tags] }.flatten.uniq.join(', ')}"
|
23
|
+
end
|
24
|
+
|
25
|
+
task :tags do
|
26
|
+
begin
|
27
|
+
registry = Centurion::DockerRegistry.new(
|
28
|
+
fetch(:docker_registry),
|
29
|
+
fetch(:registry_user),
|
30
|
+
fetch(:registry_password)
|
31
|
+
)
|
32
|
+
tags = registry.repository_tags(fetch(:image))
|
33
|
+
tags.each do |tag|
|
34
|
+
puts "\t#{tag[0]}\t-> #{tag[1][0..11]}"
|
35
|
+
end
|
36
|
+
rescue StandardError => e
|
37
|
+
error "Couldn't communicate with Registry: #{e.message}"
|
38
|
+
end
|
39
|
+
puts
|
40
|
+
end
|
41
|
+
|
42
|
+
task :running_containers do
|
43
|
+
on_each_docker_host do |target_server|
|
44
|
+
begin
|
45
|
+
running_containers = target_server.ps
|
46
|
+
running_containers.each do |container|
|
47
|
+
puts container.inspect
|
48
|
+
end
|
49
|
+
rescue StandardError => e
|
50
|
+
error "Couldn't communicate with Docker on #{target_server.hostname}: #{e.message}"
|
51
|
+
raise
|
52
|
+
end
|
53
|
+
puts
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/rory-deploy.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'centurion/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'rory-deploy'
|
8
|
+
spec.version = Centurion::VERSION
|
9
|
+
spec.authors = ['Hugo Chinchilla']
|
10
|
+
spec.email = ['hchinchilla@habitissimo.com']
|
11
|
+
spec.summary = <<-EOS.gsub(/^\s+/, '')
|
12
|
+
Fork from newrelic/centurion used at Habitissimo to handle deploys
|
13
|
+
EOS
|
14
|
+
spec.homepage = 'https://github.com/habitissimo/rory'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_dependency 'trollop', '~> 2.1'
|
23
|
+
spec.add_dependency 'excon', '~> 0.33'
|
24
|
+
spec.add_dependency 'logger-colors', '~> 1.0'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler', '~> 0'
|
27
|
+
spec.add_development_dependency 'rake', '~> 0'
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3.1'
|
29
|
+
spec.add_development_dependency 'pry', '~> 0'
|
30
|
+
spec.add_development_dependency 'simplecov', '~> 0'
|
31
|
+
|
32
|
+
spec.required_ruby_version = '>= 1.9.3'
|
33
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'capistrano_dsl'
|
3
|
+
|
4
|
+
class DSLTest
|
5
|
+
extend Capistrano::DSL
|
6
|
+
end
|
7
|
+
|
8
|
+
describe Capistrano::DSL do
|
9
|
+
before do
|
10
|
+
DSLTest.clear_env
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'handling multiple environments' do
|
14
|
+
it 'sets the environment' do
|
15
|
+
expect { DSLTest.set_current_environment(:test) }.not_to raise_error
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'fetchs the current environment' do
|
19
|
+
DSLTest.set_current_environment(:test)
|
20
|
+
expect(DSLTest.current_environment).to eq(:test)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'without a current environment set' do
|
25
|
+
it 'dies if the current_environment is not set' do
|
26
|
+
expect { DSLTest.set(:foo, 'asdf') }.to raise_error(Capistrano::DSL::CurrentEnvironmentNotSetError)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'with a current environment set' do
|
31
|
+
before do
|
32
|
+
DSLTest.set_current_environment(:test)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'stores variables in the environment' do
|
36
|
+
expect { DSLTest.set(:foo, 'bar') }.not_to raise_error
|
37
|
+
expect(DSLTest).to have_key_and_value(:foo, 'bar')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'deletes keys from the environment' do
|
41
|
+
DSLTest.set(:foo, 'bar')
|
42
|
+
expect(DSLTest).to have_key_and_value(:foo, 'bar')
|
43
|
+
DSLTest.delete(:foo)
|
44
|
+
expect(DSLTest.fetch(:foo)).to be_nil
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'returns true for any? when the value exists' do
|
48
|
+
DSLTest.set(:foo, 'bar')
|
49
|
+
expect(DSLTest.any?(:foo)).to be_truthy
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'returns false for any? when the value does not exist' do
|
53
|
+
expect(DSLTest.any?(:foo)).to be_falsey
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'passes through the any? method to values that support it' do
|
57
|
+
class NoAny
|
58
|
+
def any?
|
59
|
+
'oh no'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
DSLTest.set(:foo, NoAny.new)
|
64
|
+
expect(DSLTest.any?(:foo)).to eq('oh no')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|