foreplay 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|