foreplay 0.0.1 → 0.0.2
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 +2 -2
- data/.rvmrc +5 -0
- data/README.md +3 -3
- data/features/check.feature +74 -0
- data/features/deploy.feature +96 -0
- data/features/setup.feature +140 -0
- data/features/support/env.rb +5 -0
- data/foreplay.gemspec +2 -2
- data/lib/foreplay.rb +4 -6
- data/lib/foreplay/cli.rb +32 -8
- data/lib/foreplay/deploy.rb +305 -0
- data/lib/foreplay/setup.rb +32 -0
- data/lib/foreplay/{generators → setup}/foreplay.yml +17 -15
- data/lib/foreplay/utility.rb +29 -0
- data/lib/foreplay/version.rb +1 -1
- data/spec/lib/foreplay/deploy_spec.rb +153 -0
- data/spec/lib/foreplay/setup_spec.rb +13 -0
- data/spec/lib/foreplay/utility_spec.rb +28 -0
- data/spec/spec_helper.rb +16 -0
- metadata +41 -31
- data/features/config.feature +0 -66
- data/features/generator.feature +0 -97
- data/features/support/setup.rb +0 -1
- data/lib/foreplay/config.rb +0 -25
- data/lib/foreplay/generators/setup.rb +0 -20
- data/spec/foreplay_spec.rb +0 -7
@@ -0,0 +1,305 @@
|
|
1
|
+
require 'thor/group'
|
2
|
+
require 'yaml'
|
3
|
+
require 'net/ssh'
|
4
|
+
require 'active_support/inflector'
|
5
|
+
require 'active_support/core_ext/object'
|
6
|
+
require 'active_support/core_ext/hash'
|
7
|
+
require 'colorize'
|
8
|
+
require 'foreplay/utility'
|
9
|
+
|
10
|
+
module Foreplay
|
11
|
+
class Deploy < Thor::Group
|
12
|
+
include Thor::Actions
|
13
|
+
|
14
|
+
argument :mode, :type => :string, :required => true
|
15
|
+
argument :environment, :type => :string, :required => true
|
16
|
+
argument :filters, :type => :hash, :required => false
|
17
|
+
|
18
|
+
DEFAULTS_KEY = 'defaults'
|
19
|
+
|
20
|
+
def parse
|
21
|
+
# Explain what we're going to do
|
22
|
+
puts '%sing %s environment, %s, %s' % [
|
23
|
+
mode.capitalize,
|
24
|
+
environment.dup.yellow,
|
25
|
+
explanatory_text(filters, 'role'),
|
26
|
+
explanatory_text(filters, 'server')
|
27
|
+
]
|
28
|
+
|
29
|
+
config_file = "#{Dir.getwd}/config/foreplay.yml"
|
30
|
+
|
31
|
+
begin
|
32
|
+
config_yml = File.read config_file
|
33
|
+
rescue Errno::ENOENT
|
34
|
+
terminate "Can't find configuration file #{config_file}.\nPlease run foreplay setup or create the file manually."
|
35
|
+
end
|
36
|
+
|
37
|
+
config_all = YAML.load(config_yml)
|
38
|
+
config_env = config_all[environment] || {}
|
39
|
+
|
40
|
+
# This environment
|
41
|
+
terminate("No deployment configuration defined for #{environment} environment.\nCheck #{config_file}") unless config_all.has_key? environment
|
42
|
+
|
43
|
+
# Establish defaults
|
44
|
+
# First the default defaults
|
45
|
+
defaults = {
|
46
|
+
:name => File.basename(Dir.getwd),
|
47
|
+
:environment => environment,
|
48
|
+
:env => { 'RAILS_ENV' => environment },
|
49
|
+
:port => 50000
|
50
|
+
}
|
51
|
+
|
52
|
+
defaults = Foreplay::Utility::supermerge(defaults, config_all[DEFAULTS_KEY]) if config_all.has_key? DEFAULTS_KEY # Then the global defaults
|
53
|
+
defaults = Foreplay::Utility::supermerge(defaults, config_env[DEFAULTS_KEY]) if config_env.has_key? DEFAULTS_KEY # Then the defaults for this environment
|
54
|
+
|
55
|
+
config_env.each do |role, additional_instructions|
|
56
|
+
next if role == DEFAULTS_KEY # 'defaults' is not a role
|
57
|
+
next if filters.has_key?('role') && filters['role'] != role # Only deploy to the role we've specified (or all roles if none is specified)
|
58
|
+
|
59
|
+
instructions = Foreplay::Utility::supermerge(defaults, additional_instructions).symbolize_keys
|
60
|
+
instructions[:role] = role
|
61
|
+
required_keys = [:name, :environment, :role, :servers, :path, :repository]
|
62
|
+
required_keys.each { |key| terminate("Required key #{key} not found in instructions for #{environment} environment.\nCheck #{config_file}") unless instructions.has_key? key }
|
63
|
+
|
64
|
+
deploy_role instructions
|
65
|
+
end
|
66
|
+
|
67
|
+
puts mode == :deploy ? 'Finished deployment' : 'Deployment configuration check was successful'
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def deploy_role instructions
|
73
|
+
servers = instructions[:servers]
|
74
|
+
preposition = mode == :deploy ? 'to' : 'for'
|
75
|
+
|
76
|
+
puts "#{mode.capitalize}ing #{instructions[:name].yellow} #{preposition} #{servers.join(', ').yellow} in the #{instructions[:role].dup.yellow} role on the #{environment.dup.yellow} environment..." if servers.length > 1
|
77
|
+
servers.each { |server| deploy_to_server server, instructions }
|
78
|
+
end
|
79
|
+
|
80
|
+
def deploy_to_server server, instructions
|
81
|
+
name = instructions[:name]
|
82
|
+
role = instructions[:role]
|
83
|
+
path = instructions[:path]
|
84
|
+
repository = instructions[:repository]
|
85
|
+
user = instructions[:user]
|
86
|
+
port = instructions[:port]
|
87
|
+
preposition = mode == :deploy ? 'to' : 'for'
|
88
|
+
|
89
|
+
instructions[:server] = server
|
90
|
+
|
91
|
+
puts "#{mode.capitalize}ing #{name.yellow} #{preposition} #{server.yellow} in the #{role.dup.yellow} role on the #{environment.dup.yellow} environment"
|
92
|
+
|
93
|
+
# Substitute variables in the path
|
94
|
+
path.gsub! '%u', user
|
95
|
+
path.gsub! '%a', name
|
96
|
+
|
97
|
+
# Find out which port we're currently running on
|
98
|
+
steps = [ { :command => 'mkdir -p .foreplay && touch .foreplay/current_port && cat .foreplay/current_port', :silent => true } ]
|
99
|
+
|
100
|
+
current_port_string = execute_on_server(steps, instructions).strip!
|
101
|
+
puts current_port_string.blank? ? ' No instance is currently deployed' : " Current instance is using port #{current_port_string}"
|
102
|
+
|
103
|
+
current_port = current_port_string.to_i
|
104
|
+
|
105
|
+
# Switch ports
|
106
|
+
if current_port == port
|
107
|
+
current_port = port + 1000
|
108
|
+
former_port = port
|
109
|
+
else
|
110
|
+
current_port = port
|
111
|
+
former_port = port + 1000
|
112
|
+
end
|
113
|
+
|
114
|
+
# Contents of .foreman file
|
115
|
+
current_service = '%s-%s' % [name, current_port]
|
116
|
+
former_service = '%s-%s' % [name, former_port]
|
117
|
+
|
118
|
+
instructions[:foreman]['app'] = current_service
|
119
|
+
instructions[:foreman]['port'] = current_port
|
120
|
+
instructions[:foreman]['user'] = user
|
121
|
+
|
122
|
+
# Commands to execute on remote server
|
123
|
+
steps = [
|
124
|
+
{ :command => "echo #{current_port} > .foreplay/current_port",
|
125
|
+
:commentary => "Setting the port for the new instance to #{current_port}" },
|
126
|
+
{ :command => "mkdir -p #{path} && cd #{path} && rm -rf #{current_port} && git clone #{repository} #{current_port}",
|
127
|
+
:commentary => "Cloning repository #{repository}" },
|
128
|
+
{ :command => "rvm rvmrc trust #{current_port}",
|
129
|
+
:commentary => 'Trusting the .rvmrc file for the new instance' },
|
130
|
+
{ :command => "cd #{current_port}",
|
131
|
+
:commentary => 'Configuring the new instance' },
|
132
|
+
{ :key => :env,
|
133
|
+
:delimiter => '=',
|
134
|
+
:prefix => '.',
|
135
|
+
:commentary => 'Building .env' },
|
136
|
+
{ :key => :foreman,
|
137
|
+
:delimiter => ': ',
|
138
|
+
:prefix => '.',
|
139
|
+
:commentary => 'Building .foreman' },
|
140
|
+
{ :key => :database,
|
141
|
+
:delimiter => ': ',
|
142
|
+
:suffix => '.yml',
|
143
|
+
:commentary => 'Building config/database.yml',
|
144
|
+
:before => ' ',
|
145
|
+
:header => "#{environment}:",
|
146
|
+
:path => 'config/' },
|
147
|
+
{ :command => "bundle install",
|
148
|
+
:commentary => 'Using bundler to install the required gems' },
|
149
|
+
{ :command => "sudo ln -f `which foreman` /usr/bin/foreman",
|
150
|
+
:commentary => 'Setting the current version of foreman to be the default' },
|
151
|
+
{ :command => "sudo foreman export upstart /etc/init",
|
152
|
+
:commentary => "Converting #{current_service} to an upstart service" },
|
153
|
+
{ :command => "sudo start #{current_service} || sudo restart #{current_service}",
|
154
|
+
:commentary => 'Starting the service',
|
155
|
+
:ignore_error => true },
|
156
|
+
{ :command => 'sleep 60',
|
157
|
+
:commentary => 'Waiting 60s to give service time to start' },
|
158
|
+
{ :command => "sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{current_port.to_i + 100}",
|
159
|
+
:commentary => "Adding firewall rule to direct incoming traffic on port 80 to port #{current_port}" },
|
160
|
+
{ :command => "sudo iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{former_port.to_i + 100}",
|
161
|
+
:commentary => "Removing previous firewall directing traffic to port #{former_port}",
|
162
|
+
:ignore_error => true },
|
163
|
+
{ :command => "sudo iptables-save > /etc/iptables/rules.v4",
|
164
|
+
:commentary => "Saving iptables rules to /etc/iptables/rules.v4" },
|
165
|
+
{ :command => "sudo iptables-save > /etc/iptables.up.rules",
|
166
|
+
:commentary => "Saving iptables rules to /etc/iptables.up.rules" },
|
167
|
+
{ :command => "sudo iptables-save -c | egrep REDIRECT --color=never",
|
168
|
+
:ignore_error => true,
|
169
|
+
:commentary => "Current firewall NAT configuration:" },
|
170
|
+
{ :command => "sudo stop #{former_service} || echo 'No previous instance running'",
|
171
|
+
:commentary => 'Stopping the previous instance',
|
172
|
+
:ignore_error => true },
|
173
|
+
]
|
174
|
+
|
175
|
+
execute_on_server steps, instructions
|
176
|
+
end
|
177
|
+
|
178
|
+
def execute_on_server steps, instructions
|
179
|
+
server = instructions[:server]
|
180
|
+
user = instructions[:user]
|
181
|
+
password = instructions[:password]
|
182
|
+
keyfile = instructions[:keyfile]
|
183
|
+
private_key = instructions[:private_key]
|
184
|
+
|
185
|
+
keyfile.sub! '~', ENV['HOME'] || '/' unless keyfile.blank? # Remote shell won't expand this for us
|
186
|
+
|
187
|
+
# SSH authentication methods
|
188
|
+
options = { :verbose => :warn }
|
189
|
+
|
190
|
+
if password.blank?
|
191
|
+
# If there's no password we must supply a private key
|
192
|
+
if private_key.blank?
|
193
|
+
terminate('No authentication methods supplied. You must supply a private key, key file or password in the configuration file') if keyfile.blank?
|
194
|
+
# Get the key from the key file
|
195
|
+
puts " Using private key from #{keyfile}"
|
196
|
+
private_key = File.read keyfile
|
197
|
+
else
|
198
|
+
puts " Using private key from the configuration file"
|
199
|
+
end
|
200
|
+
|
201
|
+
options[:key_data] = [private_key]
|
202
|
+
else
|
203
|
+
# Use the password supplied
|
204
|
+
options[:password] = password
|
205
|
+
end
|
206
|
+
|
207
|
+
# Capture output of last command to return to the calling routine
|
208
|
+
output = ''
|
209
|
+
|
210
|
+
if mode == :deploy
|
211
|
+
puts " Connecting to #{server}"
|
212
|
+
|
213
|
+
# SSH connection
|
214
|
+
begin
|
215
|
+
Net::SSH.start(server, user, options) do |session|
|
216
|
+
puts " Successfully connected to #{server}"
|
217
|
+
|
218
|
+
session.shell do |sh|
|
219
|
+
steps.each do |step|
|
220
|
+
# Output from this step
|
221
|
+
output = ''
|
222
|
+
previous = '' # We don't need or want the final CRLF
|
223
|
+
|
224
|
+
commands = build_commands step, instructions
|
225
|
+
|
226
|
+
commands.each do |command|
|
227
|
+
process = sh.execute command
|
228
|
+
|
229
|
+
process.on_output do |p, o|
|
230
|
+
previous = o
|
231
|
+
output += previous
|
232
|
+
end
|
233
|
+
|
234
|
+
sh.wait!
|
235
|
+
|
236
|
+
if step[:ignore_error] == true || process.exit_status == 0
|
237
|
+
print output.gsub!(/^/, " ") unless step[:silent] == true
|
238
|
+
else
|
239
|
+
terminate(output)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
rescue SocketError => e
|
246
|
+
terminate "There was a problem starting an ssh session on #{server}:\n#{e.message}"
|
247
|
+
end
|
248
|
+
else
|
249
|
+
# Deployment check: just say what we would have done
|
250
|
+
steps.each do |step|
|
251
|
+
commands = build_commands step, instructions
|
252
|
+
|
253
|
+
commands.each { |command| puts " #{command}" unless step[:silent] }
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
output
|
258
|
+
end
|
259
|
+
|
260
|
+
def build_commands step, instructions
|
261
|
+
puts " #{(step[:commentary] || step[:command]).yellow}" unless step[:silent] == true
|
262
|
+
|
263
|
+
# Each step can be (1) a command or (2) a series of values to add to a file
|
264
|
+
if step.has_key? :key
|
265
|
+
# Add values from the config file to a file on the remote machine
|
266
|
+
key = step[:key]
|
267
|
+
prefix = step[:prefix] || ''
|
268
|
+
suffix = step[:suffix] || ''
|
269
|
+
path = step[:path] || ''
|
270
|
+
before = step[:before] || ''
|
271
|
+
delimiter = step[:delimiter] || ''
|
272
|
+
after = step[:after] || ''
|
273
|
+
|
274
|
+
step[:silent] = true
|
275
|
+
filename = '%s%s%s%s' % [path, prefix, key, suffix]
|
276
|
+
|
277
|
+
if step.has_key?(:header)
|
278
|
+
commands = ['echo "%s" > %s' % [step[:header], filename]]
|
279
|
+
redirect = '>>'
|
280
|
+
else
|
281
|
+
commands = []
|
282
|
+
redirect = '>'
|
283
|
+
end
|
284
|
+
|
285
|
+
instructions[key].each do |k, v|
|
286
|
+
commands << 'echo "%s%s%s%s%s" %s %s' % [before, k, delimiter, v, after, redirect, filename]
|
287
|
+
redirect = '>>'
|
288
|
+
end
|
289
|
+
|
290
|
+
commands
|
291
|
+
else
|
292
|
+
# ...or just execute the command specified
|
293
|
+
[step[:command]]
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def explanatory_text(hsh, key)
|
298
|
+
hsh.has_key?(key) ? "#{hsh[key].dup.yellow} #{key}" : "all #{key.pluralize}"
|
299
|
+
end
|
300
|
+
|
301
|
+
def terminate(message)
|
302
|
+
raise message
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'thor/group'
|
2
|
+
|
3
|
+
module Foreplay
|
4
|
+
class Setup < Thor::Group
|
5
|
+
include Thor::Actions
|
6
|
+
|
7
|
+
class_option :name, :aliases => '-n', :default => File.basename(Dir.getwd)
|
8
|
+
class_option :repository, :aliases => '-r'
|
9
|
+
class_option :user, :aliases => '-u'
|
10
|
+
class_option :password
|
11
|
+
class_option :keyfile
|
12
|
+
class_option :private_key, :aliases => '-k'
|
13
|
+
class_option :port, :aliases => '-p', :default => 50000
|
14
|
+
class_option :path, :aliases => '-f'
|
15
|
+
class_option :servers, :aliases => '-s', :type => :array
|
16
|
+
class_option :db_adapter, :aliases => '-a', :default => 'postgresql'
|
17
|
+
class_option :db_encoding, :aliases => '-e', :default => 'utf8'
|
18
|
+
class_option :db_pool, :default => 5
|
19
|
+
class_option :db_name, :aliases => '-d'
|
20
|
+
class_option :db_host, :aliases => '-h'
|
21
|
+
class_option :db_user
|
22
|
+
class_option :db_password
|
23
|
+
|
24
|
+
def self.source_root
|
25
|
+
File.dirname(__FILE__)
|
26
|
+
end
|
27
|
+
|
28
|
+
def config
|
29
|
+
template('setup/foreplay.yml', 'config/foreplay.yml')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -43,8 +43,7 @@
|
|
43
43
|
#
|
44
44
|
# value Normally defined as Notes
|
45
45
|
# ------------- -------------------- -------------------------------------------------------------------
|
46
|
-
# name: Global default App name (if omitted then
|
47
|
-
# Rails.application.class.parent_name.underscore is used)
|
46
|
+
# name: Global default App name (if omitted then current folder name is used)
|
48
47
|
# user: Global default The username to connect with (must have SSH permissions)
|
49
48
|
# password: Global default The password to use to connect (not necessary if you've set up SSH
|
50
49
|
# keys - see below)
|
@@ -54,6 +53,9 @@
|
|
54
53
|
# path: Global default An absolute path to deploy the app on each server. %a will be
|
55
54
|
# translated to the application name. %u will be translated to the
|
56
55
|
# login user name
|
56
|
+
# port: Global default The base port for the web app to listen on. Port 80 will be
|
57
|
+
# mapped to this port so it's usually OK to leave this at the
|
58
|
+
# default value of 50000.
|
57
59
|
# database: Environment default The database.yml elements to write to the config folder
|
58
60
|
# key: value
|
59
61
|
# servers: [server1, server2, server3]
|
@@ -64,22 +66,22 @@
|
|
64
66
|
# key: value
|
65
67
|
#
|
66
68
|
defaults:
|
67
|
-
name: <%= @name %>
|
68
|
-
repository:
|
69
|
-
user:
|
70
|
-
|
71
|
-
|
69
|
+
name: <%= @options.name? ? @options.name : 'TODO Put the app name here' %>
|
70
|
+
repository: <%= @options.repository? ? @options.repository : 'TODO Put the git repository path here' %>
|
71
|
+
user: <%= @options.user? ? @options.user : 'TODO Put here the user to logon to the deployment server' %><%= @options.password? ? "\n password: #{@options.password}" : '' %><%= @options.keyfile? ? "\n keyfile: #{@options.keyfile}" : '' %><%= @options.private_key? ? "\n private_key: #{@options.private_key}" : '' %>
|
72
|
+
path: <%= @options.path? ? @options.path : 'TODO Put here the path to deploy to on the deployment server' %>
|
73
|
+
port: <%= @options.port %>
|
72
74
|
production:
|
73
75
|
defaults:
|
74
76
|
database:
|
75
|
-
adapter:
|
76
|
-
encoding:
|
77
|
-
database:
|
78
|
-
pool:
|
79
|
-
host:
|
80
|
-
username:
|
81
|
-
password:
|
77
|
+
adapter: <%= @options.db_adapter %>
|
78
|
+
encoding: <%= @options.db_encoding %>
|
79
|
+
database: <%= @options.db_name? ? @options.db_name : 'TODO Put the database name here' %>
|
80
|
+
pool: <%= @options.db_pool %>
|
81
|
+
host: <%= @options.db_host? ? @options.db_host : 'TODO Put here the database host name' %>
|
82
|
+
username: <%= @options.db_user? ? @options.db_user : 'TODO Put here the database user' %>
|
83
|
+
password: <%= @options.db_password? ? @options.db_password : 'TODO Put here the database user\'s password' %>
|
82
84
|
web:
|
83
|
-
servers: [
|
85
|
+
servers: <%= @options.servers? ? @options.servers : '[TODO Put here the name or names of the production web servers]' %>
|
84
86
|
foreman:
|
85
87
|
concurrency: 'web=1,worker=0,scheduler=0'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Foreplay
|
2
|
+
class Utility
|
3
|
+
# Returns a new hash with +hash+ and +other_hash+ merged recursively, including arrays.
|
4
|
+
#
|
5
|
+
# h1 = { x: { y: [4,5,6] }, z: [7,8,9] }
|
6
|
+
# h2 = { x: { y: [7,8,9] }, z: 'xyz' }
|
7
|
+
# h1.supermerge(h2)
|
8
|
+
# #=> {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]}
|
9
|
+
def self.supermerge(hash, other_hash)
|
10
|
+
raise 'supermerge only works if you pass two hashes' unless hash.is_a?(Hash) && other_hash.is_a?(Hash)
|
11
|
+
|
12
|
+
new_hash = hash.deep_dup.with_indifferent_access
|
13
|
+
|
14
|
+
other_hash.each_pair do |k,v|
|
15
|
+
tv = new_hash[k]
|
16
|
+
|
17
|
+
if tv.is_a?(Hash) && v.is_a?(Hash)
|
18
|
+
new_hash[k] = Foreplay::Utility::supermerge(tv, v)
|
19
|
+
elsif tv.is_a?(Array) || v.is_a?(Array)
|
20
|
+
new_hash[k] = Array.wrap(tv) + Array.wrap(v)
|
21
|
+
else
|
22
|
+
new_hash[k] = v
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
new_hash
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/foreplay/version.rb
CHANGED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'foreplay'
|
3
|
+
require 'net/ssh/shell'
|
4
|
+
|
5
|
+
describe Foreplay::Deploy do
|
6
|
+
let (:ssh) { double(Net::SSH) }
|
7
|
+
let (:session) { double(Net::SSH::Connection::Session) }
|
8
|
+
let (:shell) { double(Net::SSH::Shell) }
|
9
|
+
let (:process) { double(Net::SSH::Shell::Process) }
|
10
|
+
|
11
|
+
before :each do
|
12
|
+
# Setup foreplay
|
13
|
+
`rm -f config/foreplay.yml`
|
14
|
+
`foreplay setup -r git@github.com:Xenapto/foreplay.git -s web.example.com -f apps/%a -u fred --password trollope`
|
15
|
+
|
16
|
+
# Stub all the things
|
17
|
+
Net::SSH.stub(:start).and_yield(session)
|
18
|
+
session.stub(:shell).and_yield(shell)
|
19
|
+
shell.stub(:execute).and_return(process)
|
20
|
+
shell.stub(:wait!).and_return(false)
|
21
|
+
process.stub(:on_output).and_yield(process, "output message\n")
|
22
|
+
process.stub(:exit_status).and_return(0)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should complain on check if there's no config file" do
|
26
|
+
`rm -f config/foreplay.yml`
|
27
|
+
expect { Foreplay::Deploy.start([:check, 'production', '']) }.to raise_error(RuntimeError, /.*Please run foreplay setup or create the file manually.*/)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should complain on deploy if there's no config file" do
|
31
|
+
`rm -f config/foreplay.yml`
|
32
|
+
expect { Foreplay::Deploy.start([:deploy, 'production', '']) }.to raise_error(RuntimeError, /.*Please run foreplay setup or create the file manually.*/)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should complain if there are no authentication methods defined in the config file" do
|
36
|
+
`rm -f config/foreplay.yml`
|
37
|
+
`foreplay setup -r git@github.com:Xenapto/foreplay.git -s web.example.com -f apps/%a -u fred --password ""`
|
38
|
+
expect { Foreplay::Deploy.start([:deploy, 'production', '']) }.to raise_error(RuntimeError, /.*No authentication methods supplied. You must supply a private key, key file or password in the configuration file*/)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should complain if the private keyfile defined in the config file doesn't exist" do
|
42
|
+
`rm -f config/foreplay.yml`
|
43
|
+
`foreplay setup -r git@github.com:Xenapto/foreplay.git -s web.example.com -f apps/%a -u fred --keyfile "/home/fred/no-such-file"`
|
44
|
+
expect { Foreplay::Deploy.start([:deploy, 'production', '']) }.to raise_error(Errno::ENOENT, /.*No such file or directory - \/home\/fred\/no-such-file*/)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should terminate if a remote process exits with a non-zero status" do
|
48
|
+
process.stub(:exit_status).and_return(1)
|
49
|
+
expect { Foreplay::Deploy.start([:deploy, 'production', '']) }.to raise_error(RuntimeError, /.*output message*/)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should terminate if a connection can't be established with the remote server" do
|
53
|
+
Net::SSH.unstub(:start)
|
54
|
+
expect { Foreplay::Deploy.start([:deploy, 'production', '']) }.to raise_error(RuntimeError, /.*There was a problem starting an ssh session on web.example.com*/)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should check the config" do
|
58
|
+
output = <<OUTPUT
|
59
|
+
Checking \e[0;33;49mproduction\e[0m environment, all roles, all servers
|
60
|
+
Checking \e[0;33;49mforeplay\e[0m for \e[0;33;49mweb.example.com\e[0m in the \e[0;33;49mweb\e[0m role on the \e[0;33;49mproduction\e[0m environment
|
61
|
+
No instance is currently deployed
|
62
|
+
\e[0;33;49mSetting the port for the new instance to 50000\e[0m
|
63
|
+
echo 50000 > .foreplay/current_port
|
64
|
+
\e[0;33;49mCloning repository git@github.com:Xenapto/foreplay.git\e[0m
|
65
|
+
mkdir -p apps/foreplay && cd apps/foreplay && rm -rf 50000 && git clone git@github.com:Xenapto/foreplay.git 50000
|
66
|
+
\e[0;33;49mTrusting the .rvmrc file for the new instance\e[0m
|
67
|
+
rvm rvmrc trust 50000
|
68
|
+
\e[0;33;49mConfiguring the new instance\e[0m
|
69
|
+
cd 50000
|
70
|
+
\e[0;33;49mBuilding .env\e[0m
|
71
|
+
\e[0;33;49mBuilding .foreman\e[0m
|
72
|
+
\e[0;33;49mBuilding config/database.yml\e[0m
|
73
|
+
\e[0;33;49mUsing bundler to install the required gems\e[0m
|
74
|
+
bundle install
|
75
|
+
\e[0;33;49mSetting the current version of foreman to be the default\e[0m
|
76
|
+
sudo ln -f `which foreman` /usr/bin/foreman
|
77
|
+
\e[0;33;49mConverting foreplay-50000 to an upstart service\e[0m
|
78
|
+
sudo foreman export upstart /etc/init
|
79
|
+
\e[0;33;49mStarting the service\e[0m
|
80
|
+
sudo start foreplay-50000 || sudo restart foreplay-50000
|
81
|
+
\e[0;33;49mWaiting 60s to give service time to start\e[0m
|
82
|
+
sleep 60
|
83
|
+
\e[0;33;49mAdding firewall rule to direct incoming traffic on port 80 to port 50000\e[0m
|
84
|
+
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 50100
|
85
|
+
\e[0;33;49mRemoving previous firewall directing traffic to port 51000\e[0m
|
86
|
+
sudo iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 51100
|
87
|
+
\e[0;33;49mSaving iptables rules to /etc/iptables/rules.v4\e[0m
|
88
|
+
sudo iptables-save > /etc/iptables/rules.v4
|
89
|
+
\e[0;33;49mSaving iptables rules to /etc/iptables.up.rules\e[0m
|
90
|
+
sudo iptables-save > /etc/iptables.up.rules
|
91
|
+
\e[0;33;49mCurrent firewall NAT configuration:\e[0m
|
92
|
+
sudo iptables-save -c | egrep REDIRECT --color=never
|
93
|
+
\e[0;33;49mStopping the previous instance\e[0m
|
94
|
+
sudo stop foreplay-51000 || echo 'No previous instance running'
|
95
|
+
Deployment configuration check was successful
|
96
|
+
OUTPUT
|
97
|
+
|
98
|
+
output.split("\n").reverse.each { |line| $stdout.should_receive(:puts).with(line) }
|
99
|
+
Foreplay::Deploy.start [:check, 'production', '']
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should deploy to the environment" do
|
103
|
+
Net::SSH.should_receive(:start).with('web.example.com', 'fred', { :verbose => :warn, :password => 'trollope' }).and_yield(session)
|
104
|
+
|
105
|
+
[
|
106
|
+
'mkdir -p .foreplay && touch .foreplay/current_port && cat .foreplay/current_port',
|
107
|
+
'echo 50000 > .foreplay/current_port',
|
108
|
+
'mkdir -p apps/foreplay && cd apps/foreplay && rm -rf 50000 && git clone git@github.com:Xenapto/foreplay.git 50000',
|
109
|
+
'rvm rvmrc trust 50000',
|
110
|
+
'cd 50000',
|
111
|
+
'echo "RAILS_ENV=production" > .env',
|
112
|
+
'echo "concurrency: web=1,worker=0,scheduler=0" > .foreman',
|
113
|
+
'echo "app: foreplay-50000" >> .foreman',
|
114
|
+
'echo "port: 50000" >> .foreman',
|
115
|
+
'echo "user: fred" >> .foreman',
|
116
|
+
'echo "production:" > config/database.yml',
|
117
|
+
'echo " adapter: postgresql" >> config/database.yml',
|
118
|
+
'echo " encoding: utf8" >> config/database.yml',
|
119
|
+
'echo " database: TODO Put the database name here" >> config/database.yml',
|
120
|
+
'echo " pool: 5" >> config/database.yml',
|
121
|
+
'echo " host: TODO Put here the database host name" >> config/database.yml',
|
122
|
+
'echo " username: TODO Put here the database user" >> config/database.yml',
|
123
|
+
'echo " password: TODO Put here the database user\'s password" >> config/database.yml',
|
124
|
+
'bundle install',
|
125
|
+
'sudo ln -f `which foreman` /usr/bin/foreman',
|
126
|
+
'sudo foreman export upstart /etc/init',
|
127
|
+
'sudo start foreplay-50000 || sudo restart foreplay-50000',
|
128
|
+
'sleep 60',
|
129
|
+
'sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 50100',
|
130
|
+
'sudo iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 51100',
|
131
|
+
'sudo iptables-save > /etc/iptables/rules.v4',
|
132
|
+
'sudo iptables-save > /etc/iptables.up.rules',
|
133
|
+
'sudo iptables-save -c | egrep REDIRECT --color=never',
|
134
|
+
'sudo stop foreplay-51000 || echo \'No previous instance running\''
|
135
|
+
].each do |command|
|
136
|
+
shell.should_receive(:execute).with(command).and_return(process)
|
137
|
+
end
|
138
|
+
|
139
|
+
Foreplay::Deploy.start [:deploy, 'production', '']
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should use another port if there's already an installed instance" do
|
143
|
+
process.stub(:on_output).and_yield(process, "50000\n")
|
144
|
+
shell.should_receive(:execute).with('echo 51000 > .foreplay/current_port').and_return(process)
|
145
|
+
Foreplay::Deploy.start [:deploy, 'production', '']
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should use the private key provided in the config file" do
|
149
|
+
`rm -f config/foreplay.yml`
|
150
|
+
`foreplay setup -r git@github.com:Xenapto/foreplay.git -s web.example.com -f apps/%a -u fred -k "top secret private key"`
|
151
|
+
Foreplay::Deploy.start([:deploy, 'production', ''])
|
152
|
+
end
|
153
|
+
end
|