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,92 @@
|
|
1
|
+
require_relative 'logging'
|
2
|
+
require_relative 'shell'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Centurion; end
|
6
|
+
|
7
|
+
class Centurion::Dogestry
|
8
|
+
include Centurion::Logging
|
9
|
+
attr_accessor :options
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
# Cross-platform way of finding an executable in the $PATH.
|
16
|
+
# which('ruby') #=> /usr/bin/ruby
|
17
|
+
def which(cmd)
|
18
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
19
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
20
|
+
exts.each do |ext|
|
21
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
22
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
return nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_before_exec
|
29
|
+
unless which('dogestry')
|
30
|
+
message = 'Unable to find "dogestry" executable'
|
31
|
+
error message
|
32
|
+
raise message
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def aws_access_key_id
|
37
|
+
@options[:aws_access_key_id]
|
38
|
+
end
|
39
|
+
|
40
|
+
def aws_secret_key
|
41
|
+
@options[:aws_secret_key]
|
42
|
+
end
|
43
|
+
|
44
|
+
def s3_bucket
|
45
|
+
@options[:s3_bucket]
|
46
|
+
end
|
47
|
+
|
48
|
+
def s3_region
|
49
|
+
@options[:s3_region] || 'us-east-1'
|
50
|
+
end
|
51
|
+
|
52
|
+
def s3_url
|
53
|
+
"s3://#{s3_bucket}/?region=#{s3_region}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def set_envs()
|
57
|
+
ENV['AWS_ACCESS_KEY'] = aws_access_key_id
|
58
|
+
ENV['AWS_SECRET_KEY'] = aws_secret_key
|
59
|
+
|
60
|
+
# If we want TLS, then try to pass a sane directory to Dogestry, which doesn't
|
61
|
+
# speak individual filenames.
|
62
|
+
if @options[:tlsverify]
|
63
|
+
ENV['DOCKER_CERT_PATH'] =
|
64
|
+
if @options[:tlscacert] || @options[:tlscert]
|
65
|
+
File.dirname(
|
66
|
+
@options[:tlscacert] ||
|
67
|
+
@options[:tlscert]
|
68
|
+
)
|
69
|
+
else
|
70
|
+
@options[:original_docker_cert_path]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
info "Dogestry ENV: #{ENV.inspect}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def exec_command(command, repo, flags="")
|
78
|
+
command = "dogestry #{flags} #{command} #{s3_url} #{repo}"
|
79
|
+
info "Executing: #{command}"
|
80
|
+
command
|
81
|
+
end
|
82
|
+
|
83
|
+
def pull(repo, pull_hosts)
|
84
|
+
validate_before_exec
|
85
|
+
set_envs()
|
86
|
+
|
87
|
+
hosts = pull_hosts.join(",")
|
88
|
+
flags = "-pullhosts #{hosts}"
|
89
|
+
|
90
|
+
Centurion::Shell.echo(exec_command('pull', repo, flags))
|
91
|
+
end
|
92
|
+
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,218 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'capistrano_dsl'
|
3
|
+
|
4
|
+
module Centurion
|
5
|
+
class Service
|
6
|
+
extend ::Capistrano::DSL
|
7
|
+
|
8
|
+
attr_accessor :command, :dns, :extra_hosts, :image, :name, :volumes, :port_bindings, :network_mode, :cap_adds, :cap_drops
|
9
|
+
attr_reader :memory, :cpu_shares, :env_vars
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
@env_vars = {}
|
14
|
+
@volumes = []
|
15
|
+
@port_bindings = []
|
16
|
+
@cap_adds = []
|
17
|
+
@cap_drops = []
|
18
|
+
@network_mode = 'bridge'
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.from_env
|
22
|
+
Service.new(fetch(:name)).tap do |s|
|
23
|
+
s.image = if fetch(:tag, nil)
|
24
|
+
"#{fetch(:image, nil)}:#{fetch(:tag)}"
|
25
|
+
else
|
26
|
+
fetch(:image, nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
s.cap_adds = fetch(:cap_adds, [])
|
30
|
+
s.cap_drops = fetch(:cap_drops, [])
|
31
|
+
s.dns = fetch(:dns, nil)
|
32
|
+
s.extra_hosts = fetch(:extra_hosts, nil)
|
33
|
+
s.volumes = fetch(:binds, [])
|
34
|
+
s.port_bindings = fetch(:port_bindings, [])
|
35
|
+
s.network_mode = fetch(:network_mode, 'bridge')
|
36
|
+
s.command = fetch(:command, nil)
|
37
|
+
|
38
|
+
s.add_env_vars(fetch(:env_vars, {}))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_env_vars(new_vars)
|
43
|
+
@env_vars.merge!(new_vars)
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_port_bindings(host_port, container_port, type = 'tcp', host_ip = nil)
|
47
|
+
@port_bindings << PortBinding.new(host_port, container_port, type, host_ip)
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_volume(host_volume, container_volume)
|
51
|
+
@volumes << Volume.new(host_volume, container_volume)
|
52
|
+
end
|
53
|
+
|
54
|
+
def cap_adds=(capabilites)
|
55
|
+
unless capabilites.is_a? Array
|
56
|
+
raise ArgumentError, "invalid value for capability additions: #{capabilites}, value must be an array"
|
57
|
+
end
|
58
|
+
@cap_adds = capabilites
|
59
|
+
end
|
60
|
+
|
61
|
+
def cap_drops=(capabilites)
|
62
|
+
unless capabilites.is_a? Array
|
63
|
+
raise ArgumentError, "invalid value for capability drops: #{capabilites}, value must be an array"
|
64
|
+
end
|
65
|
+
@cap_drops = capabilites
|
66
|
+
end
|
67
|
+
|
68
|
+
def network_mode=(mode)
|
69
|
+
@network_mode = mode
|
70
|
+
end
|
71
|
+
|
72
|
+
def memory=(bytes)
|
73
|
+
if !bytes || !is_a_uint64?(bytes)
|
74
|
+
raise ArgumentError, "invalid value for cgroup memory constraint: #{bytes}, value must be a between 0 and 18446744073709551615"
|
75
|
+
end
|
76
|
+
@memory = bytes
|
77
|
+
end
|
78
|
+
|
79
|
+
def cpu_shares=(shares)
|
80
|
+
if !shares || !is_a_uint64?(shares)
|
81
|
+
raise ArgumentError, "invalid value for cgroup CPU constraint: #{shares}, value must be a between 0 and 18446744073709551615"
|
82
|
+
end
|
83
|
+
@cpu_shares = shares
|
84
|
+
end
|
85
|
+
|
86
|
+
def image=(image)
|
87
|
+
@image = image
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_config(server_hostname, &block)
|
91
|
+
container_config = {}.tap do |c|
|
92
|
+
c['Image'] = image
|
93
|
+
c['Hostname'] = block.call(server_hostname) if block_given?
|
94
|
+
c['Cmd'] = command if command
|
95
|
+
c['Memory'] = memory if memory
|
96
|
+
c['CpuShares'] = cpu_shares if cpu_shares
|
97
|
+
end
|
98
|
+
|
99
|
+
unless port_bindings.empty?
|
100
|
+
container_config['ExposedPorts'] = port_bindings.reduce({}) do |config, binding|
|
101
|
+
config["#{binding.container_port}/#{binding.type}"] = {}
|
102
|
+
config
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
unless env_vars.empty?
|
107
|
+
container_config['Env'] = env_vars.map do |k,v|
|
108
|
+
"#{k}=#{interpolate_var(v, server_hostname)}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
unless volumes.empty?
|
113
|
+
container_config['Volumes'] = volumes.inject({}) do |memo, v|
|
114
|
+
memo[v.container_volume] = {}
|
115
|
+
memo
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
container_config
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_host_config(restart_policy = nil)
|
123
|
+
host_config = {}
|
124
|
+
|
125
|
+
# Set capability additions and drops
|
126
|
+
host_config['CapAdd'] = cap_adds if cap_adds
|
127
|
+
host_config['CapDrop'] = cap_drops if cap_drops
|
128
|
+
|
129
|
+
# Map some host volumes if needed
|
130
|
+
host_config['Binds'] = volume_binds_config if volume_binds_config
|
131
|
+
|
132
|
+
# Bind the ports
|
133
|
+
host_config['PortBindings'] = port_bindings_config
|
134
|
+
|
135
|
+
# Set the network mode
|
136
|
+
host_config['NetworkMode'] = network_mode
|
137
|
+
|
138
|
+
# DNS if specified
|
139
|
+
host_config['Dns'] = dns if dns
|
140
|
+
|
141
|
+
# Add ExtraHosts if needed
|
142
|
+
host_config['ExtraHosts'] = extra_hosts if extra_hosts
|
143
|
+
|
144
|
+
# Restart Policy
|
145
|
+
if restart_policy
|
146
|
+
host_config['RestartPolicy'] = {}
|
147
|
+
|
148
|
+
restart_policy_name = restart_policy.name
|
149
|
+
restart_policy_name = 'on-failure' unless ["always", "on-failure", "no"].include?(restart_policy_name)
|
150
|
+
|
151
|
+
host_config['RestartPolicy']['Name'] = restart_policy_name
|
152
|
+
host_config['RestartPolicy']['MaximumRetryCount'] = restart_policy.max_retry_count || 10 if restart_policy_name == 'on-failure'
|
153
|
+
end
|
154
|
+
|
155
|
+
host_config
|
156
|
+
end
|
157
|
+
|
158
|
+
def build_console_config(server_name, &block)
|
159
|
+
build_config(server_name, &block).merge({
|
160
|
+
'Cmd' => ['/bin/bash'],
|
161
|
+
'AttachStdin' => true,
|
162
|
+
'Tty' => true,
|
163
|
+
'OpenStdin' => true,
|
164
|
+
})
|
165
|
+
end
|
166
|
+
|
167
|
+
def volume_binds_config
|
168
|
+
@volumes.map { |volume| "#{volume.host_volume}:#{volume.container_volume}" }
|
169
|
+
end
|
170
|
+
|
171
|
+
def port_bindings_config
|
172
|
+
@port_bindings.inject({}) do |memo, binding|
|
173
|
+
config = {}
|
174
|
+
config['HostPort'] = binding.host_port.to_s
|
175
|
+
config['HostIp'] = binding.host_ip if binding.host_ip
|
176
|
+
memo["#{binding.container_port}/#{binding.type}"] = [config]
|
177
|
+
memo
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def public_ports
|
182
|
+
@port_bindings.map(&:host_port)
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def is_a_uint64?(value)
|
188
|
+
result = false
|
189
|
+
if !value.is_a? Integer
|
190
|
+
return result
|
191
|
+
end
|
192
|
+
if value < 0 || value > 0xFFFFFFFFFFFFFFFF
|
193
|
+
return result
|
194
|
+
end
|
195
|
+
return true
|
196
|
+
end
|
197
|
+
|
198
|
+
def interpolate_var(val, hostname)
|
199
|
+
val.to_s.gsub('%DOCKER_HOSTNAME%', hostname)
|
200
|
+
.gsub('%DOCKER_HOST_IP%', host_ip(hostname))
|
201
|
+
end
|
202
|
+
|
203
|
+
def host_ip(hostname)
|
204
|
+
@host_ip ||= {}
|
205
|
+
return @host_ip[hostname] if @host_ip.has_key?(hostname)
|
206
|
+
@host_ip[hostname] = Socket.getaddrinfo(hostname, nil).first[2]
|
207
|
+
end
|
208
|
+
|
209
|
+
class RestartPolicy < Struct.new(:name, :max_retry_count)
|
210
|
+
end
|
211
|
+
|
212
|
+
class Volume < Struct.new(:host_volume, :container_volume)
|
213
|
+
end
|
214
|
+
|
215
|
+
class PortBinding < Struct.new(:host_port, :container_port, :type, :host_ip)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Centurion; end
|
2
|
+
|
3
|
+
module Centurion::Shell
|
4
|
+
def self.echo(command)
|
5
|
+
if Thread.list.find_all { |t| t.status == 'run' }.count > 1
|
6
|
+
run_without_echo(command)
|
7
|
+
else
|
8
|
+
run_with_echo(command)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.run_without_echo(command)
|
13
|
+
output = Queue.new
|
14
|
+
output_thread = Thread.new do
|
15
|
+
while true do
|
16
|
+
begin
|
17
|
+
puts output.pop
|
18
|
+
rescue => e
|
19
|
+
info "Rescuing... #{e.message}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
IO.popen(command) do |io|
|
25
|
+
io.each_line { |line| output << line }
|
26
|
+
end
|
27
|
+
|
28
|
+
output_thread.kill
|
29
|
+
validate_status(command)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.run_with_echo(command)
|
33
|
+
$stdout.sync = true
|
34
|
+
$stderr.sync = true
|
35
|
+
IO.popen(command) do |io|
|
36
|
+
io.each_char { |char| print char }
|
37
|
+
end
|
38
|
+
validate_status(command)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.validate_status(command)
|
42
|
+
unless $?.success?
|
43
|
+
raise "The command failed with a non-zero exit status: #{$?.exitstatus}. Command: '#{command}'"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# Modified from ActiveSupport 4.2.1 lib/active_support/core_ext/numeric/bytes.rb
|
2
|
+
#
|
3
|
+
# NOTE that THIS LICENSE ONLY APPLIES TO THIS FILE itself, not
|
4
|
+
# to the rest of the project.
|
5
|
+
#
|
6
|
+
# ORIGINAL ACTIVE SUPPORT LICENSE FOLLOWS:
|
7
|
+
#
|
8
|
+
# Copyright (c) 2005-2015 David Heinemeier Hansson
|
9
|
+
#
|
10
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
11
|
+
# a copy of this software and associated documentation files (the
|
12
|
+
# "Software"), to deal in the Software without restriction, including
|
13
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
14
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
15
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
16
|
+
# the following conditions:
|
17
|
+
#
|
18
|
+
# The above copyright notice and this permission notice shall be
|
19
|
+
# included in all copies or substantial portions of the Software.
|
20
|
+
#
|
21
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
22
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
23
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
24
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
25
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
26
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
27
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
28
|
+
unless Numeric.constants.include?(:KILOBYTE)
|
29
|
+
class Numeric
|
30
|
+
KILOBYTE = 1024 unless defined? KILOBYTE
|
31
|
+
MEGABYTE = KILOBYTE * 1024 unless defined? MEGABYTE
|
32
|
+
GIGABYTE = MEGABYTE * 1024 unless defined? GIGABYTE
|
33
|
+
TERABYTE = GIGABYTE * 1024 unless defined? TERABYTE
|
34
|
+
PETABYTE = TERABYTE * 1024 unless defined? PETABYTE
|
35
|
+
EXABYTE = PETABYTE * 1024 unless defined? EXABYTE
|
36
|
+
|
37
|
+
# Enables the use of byte calculations and declarations, like 45.bytes + 2.6.megabytes
|
38
|
+
#
|
39
|
+
# 2.bytes # => 2
|
40
|
+
def bytes
|
41
|
+
self
|
42
|
+
end unless method_defined? :bytes
|
43
|
+
alias :byte :bytes unless method_defined? :byte
|
44
|
+
|
45
|
+
# Returns the number of bytes equivalent to the kilobytes provided.
|
46
|
+
#
|
47
|
+
# 2.kilobytes # => 2048
|
48
|
+
def kilobytes
|
49
|
+
self * KILOBYTE
|
50
|
+
end unless method_defined? :kilobytes
|
51
|
+
alias :kilobyte :kilobytes unless method_defined? :kilobyte
|
52
|
+
|
53
|
+
# Returns the number of bytes equivalent to the megabytes provided.
|
54
|
+
#
|
55
|
+
# 2.megabytes # => 2_097_152
|
56
|
+
def megabytes
|
57
|
+
self * MEGABYTE
|
58
|
+
end unless method_defined? :megabytes?
|
59
|
+
alias :megabyte :megabytes unless method_defined? :megabyte
|
60
|
+
|
61
|
+
# Returns the number of bytes equivalent to the gigabytes provided.
|
62
|
+
#
|
63
|
+
# 2.gigabytes # => 2_147_483_648
|
64
|
+
def gigabytes
|
65
|
+
self * GIGABYTE
|
66
|
+
end unless method_defined? :gigabytes
|
67
|
+
alias :gigabyte :gigabytes unless method_defined? :gigabyte
|
68
|
+
|
69
|
+
# Returns the number of bytes equivalent to the terabytes provided.
|
70
|
+
#
|
71
|
+
# 2.terabytes # => 2_199_023_255_552
|
72
|
+
def terabytes
|
73
|
+
self * TERABYTE
|
74
|
+
end unless method_defined? :terabytes
|
75
|
+
alias :terabyte :terabytes unless method_defined? :terabyte
|
76
|
+
|
77
|
+
# Returns the number of bytes equivalent to the petabytes provided.
|
78
|
+
#
|
79
|
+
# 2.petabytes # => 2_251_799_813_685_248
|
80
|
+
def petabytes
|
81
|
+
self * PETABYTE
|
82
|
+
end unless method_defined? :petabytes
|
83
|
+
alias :petabyte :petabytes unless method_defined? :petabyte
|
84
|
+
|
85
|
+
# Returns the number of bytes equivalent to the exabytes provided.
|
86
|
+
#
|
87
|
+
# 2.exabytes # => 2_305_843_009_213_693_952
|
88
|
+
def exabytes
|
89
|
+
self * EXABYTE
|
90
|
+
end unless method_defined? :exabytes
|
91
|
+
alias :exabyte :exabytes unless method_defined? :exabyte
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|