hr_deploy 0.0.2 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,387 +1,340 @@
1
+ require 'hr_deploy/config_handler'
2
+ require 'hr_deploy/color.rb'
3
+ require 'hr_deploy/tasks/check_network_task'
4
+ require 'hr_deploy/tasks/check_heroku_task'
5
+ require 'hr_deploy/tasks/enable_pinger_task'
6
+ require 'hr_deploy/tasks/disable_pinger_task'
7
+ require 'hr_deploy/tasks/enable_maintenance_task'
8
+ require 'hr_deploy/tasks/disable_maintenance_task'
9
+ require 'hr_deploy/tasks/backup_db_task'
10
+ require 'hr_deploy/tasks/clean_old_assets_task.rb'
11
+ require 'hr_deploy/tasks/clean_new_assets_task.rb'
12
+ require 'hr_deploy/tasks/precompile_assets_task.rb'
13
+ require 'hr_deploy/tasks/push_code_task.rb'
14
+ require 'hr_deploy/tasks/migrate_db_task.rb'
15
+ require 'hr_deploy/tasks/restart_task.rb'
16
+
1
17
  module HR_Deploy
2
18
 
3
19
  class Deployer
4
20
 
5
- DEBUG = false
6
-
7
- attr_reader :target
8
- attr_reader :pinging_interaction
9
- attr_reader :perform_db_backup
10
- attr_reader :confirm
11
-
12
- def initialize(target, confirm)
13
- @target = target
14
- @pinging_interaction = target.requires_pinging_interaction?
15
- @perform_db_backup = target.backup_db?
16
- @confirm = confirm
21
+ def initialize
22
+ @target = nil
23
+ @confirm = nil
24
+ @dry_run = nil
25
+ @start_time = nil
26
+ @tasks_to_run = []
27
+ @finished_tasks = []
17
28
  end
18
29
 
19
- def deploy
20
- confirm_deploy
21
-
22
- start_time = Time.now
23
- puts "#{target.description} deploy started at #{Time.now.strftime('%H:%M:%S')}"
24
- puts
30
+ def deploy(target_name, confirm, dry_run, db_change)
25
31
 
26
- check_connectivity
27
- check_heroku_status
32
+ self.confirm = confirm
33
+ self.dry_run = dry_run
34
+ self.db_change = db_change
28
35
 
29
- if pinging_interaction
30
- disable_pinging
31
- continue?(:disabling_pinger) { enable_maintenance }
32
- else
33
- enable_maintenance
36
+ unless HR_Deploy::ConfigHandler.config_exists?
37
+ puts "No deployment config is present. Run 'hr_deploy generate' to generate a config example"
38
+ exit 1
34
39
  end
35
40
 
36
- if perform_db_backup
37
- continue?(:enabling_maintenance) { backup_db }
38
- continue?(:backing_up_db) { push_code }
39
- else
40
- continue?(:enabling_maintenance) { push_code }
41
- end
41
+ config_handler = HR_Deploy::ConfigHandler.new
42
42
 
43
- continue?(:pushing_code) { migrate_db }
44
- continue?(:migrating_db) { restart }
43
+ # This will set up targets or error in 'config_handler'
44
+ config_handler.attempt_to_load_targets
45
45
 
46
- if pinging_interaction
47
- continue?(:restarting) { enable_pinging }
48
- continue?(:enabling_pinger) { disable_maintenance }
49
- else
50
- continue?(:restarting) { disable_maintenance }
51
- end
46
+ if config_handler.error
47
+ puts config_handler.error
48
+ exit 1
52
49
 
53
- continue?(:disabling_maintenance) do
54
- puts 'Waiting 10 seconds for application to go live...'
55
- sleep 10
56
- puts 'Opening application...'
57
- execute_system_command("heroku open --remote #{target.name}")
58
- puts
50
+ else
51
+ if target_name == nil
59
52
 
60
- puts "Deploy completed in #{(Time.now - start_time).round} seconds"
61
- end
62
- end
53
+ # Deploy to default target
54
+ default_targets = config_handler.targets.find_all { |target| target.default? }
55
+ if default_targets.count == 0
63
56
 
64
- private
57
+ puts 'No default target specified. Edit config to specify one'
58
+ exit 1
59
+ elsif default_targets.count > 1
65
60
 
66
- def confirm_deploy
67
- answered = false
68
-
69
- until answered
70
- print "Deploy to #{target.description}? (Yes / No) > "
71
- answer = $stdin.gets.chomp
61
+ puts 'Multiple default targets specified. Edit config to specify only one'
62
+ exit 1
63
+ else
72
64
 
73
- if answer == 'Yes' || answer == 'yes'
74
- answered = true
75
- puts
76
- elsif answer == 'No' || answer == 'no'
77
- abort_deploy(nil)
65
+ self.target = default_targets.first
66
+ deploy_to_target
67
+ end
78
68
  else
79
- puts 'Unknown answer'
80
- puts
69
+
70
+ target = config_handler.targets.find { |target| target.name == target_name }
71
+ if target
72
+ self.target = target
73
+ deploy_to_target
74
+ else
75
+ puts "No such target: #{target_name}"
76
+ exit 1
77
+ end
81
78
  end
82
79
  end
83
80
  end
84
81
 
85
- def abort_deploy(message, stage = nil)
82
+ private
86
83
 
87
- if stage
88
- puts 'Notes:'
89
- puts
84
+ attr_accessor :target
85
+ attr_accessor :confirm
86
+ attr_accessor :dry_run
87
+ attr_accessor :db_change
88
+ attr_accessor :start_time
89
+ attr_reader :tasks_to_run
90
+ attr_reader :finished_tasks
90
91
 
91
- case stage
92
- when :disabling_pinger
93
- puts '1) Application state did not change'
94
- puts '2) Availability pinging could not be disabled'
92
+ def deploy_to_target
93
+ tools = ['git', 'heroku']
94
+ tools << 'curl' if target.requires_curl?
95
+ tools << 'rake' if target.requires_rake?
95
96
 
96
- when :enabling_maintenance
97
- puts '1) Application state did not change'
98
- puts
99
- puts '2) Maintenance mode could not be enabled'
100
- if pinging_interaction
101
- puts
102
- puts '3) Availability pinging is disabled'
103
- puts "* To enable: curl #{target.enable_pinger_url}"
104
- end
97
+ # Will print out an error and halt app if a required tool is unavailable
98
+ assert_tools_available(tools)
105
99
 
106
- when :backing_up_db
107
- puts '1) Application state did not change'
108
- puts
109
- puts '2) Maintenance mode is enabled'
110
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
111
- puts
112
- puts '3) Database backup could not be created'
113
- puts
114
- if pinging_interaction
115
- puts '4) Availability pinging is disabled'
116
- puts "* To enable: curl #{target.enable_pinger_url}"
117
- end
100
+ if dry_run
101
+ puts 'Dry run started'
118
102
 
119
- when :pushing_code
120
- puts '1) Application state is unknown'
121
- puts
122
- puts '2) Maintenance mode is enabled'
123
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
124
- puts
125
- puts '3) Application code was not pushed properly'
126
- puts "* Check releases with 'heroku releases --remote #{target.name}' and rollback with " +
127
- "'heroku rollback (version) --remote #{target.name}' if needed"
128
- if pinging_interaction
129
- puts
130
- puts '4) Availability pinging is disabled'
131
- puts "* To enable: curl #{target.enable_pinger_url}"
132
- end
103
+ elsif confirm_deploy
104
+ puts
105
+ self.start_time = Time.now
106
+ puts "Deploy started at #{start_time.strftime('%H:%M:%S')}"
133
107
 
134
- when :migrating_db
135
- puts '1) Application state is unknown'
136
- puts
137
- puts '2) Maintenance mode is enabled'
138
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
139
- puts
140
- puts '3) Code was pushed but database was not migrated properly'
141
- puts "* Check releases with 'heroku releases --remote #{target.name}' and rollback " +
142
- "with 'heroku rollback (version) --remote #{target.name}' if needed"
143
- if perform_db_backup
144
- puts "* List database backups with 'heroku pgbackups --remote #{target.name}' and restore " +
145
- "with 'heroku pgbackups:restore (database name) (version) --remote #{target.name}' if needed"
146
- end
147
- if pinging_interaction
148
- puts
149
- puts '4) Availability pinging is disabled'
150
- puts "* To enable: curl #{target.enable_pinger_url}"
151
- end
108
+ else
109
+ print_fail 'Deploy aborted'
110
+ exit 1
111
+ end
152
112
 
153
- when :restarting
154
- puts '1) Application state is unknown'
155
- puts
156
- puts '2) Maintenance mode is enabled'
157
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
158
- puts
159
- puts '3) Database was migrated but could not restart'
160
- puts "* Try to restart manually with 'heroku restart --remote #{target.name}'"
161
- puts "* Check releases with 'heroku releases --remote #{target.name}' and rollback " +
162
- "with 'heroku rollback (version) --remote #{target.name}' if needed"
163
- if perform_db_backup
164
- puts "* List database backups with 'heroku pgbackups --remote #{target.name}' and restore " +
165
- "with 'heroku pgbackups:restore (database name) (version) --remote #{target.name}' if needed"
166
- end
167
- if pinging_interaction
168
- puts
169
- puts '4) Availability pinging is disabled'
170
- puts "* To enable: curl #{target.enable_pinger_url}"
171
- end
113
+ puts
172
114
 
173
- when :enabling_pinger
174
- puts '1) Application has been deployed'
175
- puts
176
- puts '2) Maintenance mode is enabled'
177
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
178
- if pinging_interaction
179
- puts
180
- puts '3) Availability pinging could not be enabled'
181
- puts "* To enable: curl #{target.enable_pinger_url}"
182
- end
115
+ load_tasks
116
+ run_tasks
117
+ end
183
118
 
184
- when :disabling_maintenance
185
- puts '1) Application has been deployed'
186
- puts
187
- puts '2) Maintenance mode could not be disabled'
188
- puts "* To disable: heroku maintenance:off --remote #{target.name}"
119
+ def assert_tools_available(tools)
189
120
 
190
- else
191
- raise 'Unknown stage'
121
+ def command_available?(cmd)
122
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
123
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
124
+ exts.each do |ext|
125
+ exe = File.join(path, "#{cmd}#{ext}")
126
+ return true if File.executable? exe
127
+ end
192
128
  end
193
-
194
- puts
129
+ false
195
130
  end
196
131
 
197
- if message
198
- puts "Deploy aborted with message: #{message}"
199
- else
200
- puts 'Deploy aborted'
132
+ all_tools_available = true
133
+
134
+ tools.each do |tool|
135
+ unless command_available?(tool)
136
+ puts "You don't have access to '#{tool}' command"
137
+ all_tools_available = false
138
+ end
201
139
  end
202
140
 
203
- exit
141
+ exit 1 unless all_tools_available
204
142
  end
205
143
 
206
- def continue?(stage)
207
-
208
- if confirm
209
- # User wants confirmations
210
-
211
- answered = false
212
- until answered
213
-
214
- case stage
215
- when :disabling_pinger
216
- print 'Pinger disabled? (Yes / No) > '
217
- when :enabling_maintenance
218
- print 'Maintenance mode enabled? (Yes / No) > '
219
- when :backing_up_db
220
- print 'Database backed up? (Yes / No) > '
221
- when :pushing_code
222
- print 'Code pushed successfully? (Yes / No) > '
223
- when :migrating_db
224
- print 'Database migrated? (Yes / No) > '
225
- when :restarting
226
- print 'Restarted? (Yes / No) > '
227
- when :enabling_pinger
228
- print 'Pinger enabled? (Yes / No) > '
229
- when :disabling_maintenance
230
- print 'Maintenance mode disabled? (Yes / No) > '
231
- else
232
- raise 'Unknown stage'
233
- end
234
-
235
- answer = $stdin.gets.chomp
144
+ def confirm_deploy
236
145
 
237
- if answer == 'Yes' || answer == 'yes'
238
- answered = true
239
- puts
240
- yield
241
- elsif answer == 'No' || answer == 'no'
242
- puts
243
- abort_deploy(nil, stage)
244
- else
245
- puts 'Unknown answer'
246
- puts
247
- end
146
+ loop do
147
+ if db_change
148
+ print "Deploy to #{target.description}? (changing DB structure) [Yes / No] > "
149
+ else
150
+ print "Deploy to #{target.description}? (not changing DB structure) [Yes / No] > "
248
151
  end
152
+ answer = $stdin.gets.chomp.downcase
249
153
 
250
- else
251
- # User does not want confirmations, continue
252
- yield
253
- end
254
- end
154
+ if answer == 'yes'
155
+ return true
255
156
 
256
- def execute_system_command(cmd)
257
- if DEBUG
258
- puts "Executing command: #{cmd}"
259
- else
260
- system(cmd)
157
+ elsif answer == 'no'
158
+ return false
159
+
160
+ else
161
+ puts 'Unknown answer'
162
+ puts
163
+ end
261
164
  end
262
165
  end
263
166
 
264
- def check_connectivity
265
- require 'open-uri'
167
+ def load_tasks
168
+ tasks_to_run << HR_Deploy::CheckNetworkTask.new(dry_run: dry_run)
169
+ tasks_to_run << HR_Deploy::CheckHerokuTask.new(dry_run: dry_run)
266
170
 
267
- puts 'Checking Internet connection...'
171
+ if target.pinging_interaction?
172
+ tasks_to_run << HR_Deploy::DisablePingerTask.new(target: target, confirm: confirm, dry_run: dry_run)
173
+ end
268
174
 
269
- begin
270
- open('http://www.google.com')
271
- rescue
272
- puts 'Not connected to the Internet'
273
- puts
274
- abort_deploy('Not connected to the Internet')
275
- else
276
- puts 'Internet connection is fine'
277
- puts
175
+ if target.maintenance_interaction?
176
+ tasks_to_run << HR_Deploy::EnableMaintenanceTask.new(target: target, confirm: confirm, dry_run: dry_run)
278
177
  end
279
- end
280
178
 
281
- def check_heroku_status
179
+ if target.backup_db? && db_change
180
+ tasks_to_run << HR_Deploy::BackupDBTask.new(target: target, confirm: confirm, dry_run: dry_run)
181
+ end
282
182
 
283
- require 'uri'
284
- require 'net/http'
285
- require 'net/https'
286
- require 'json'
183
+ if target.s3_asset_sync?
184
+ tasks_to_run << HR_Deploy::CleanOldAssetsTask.new(confirm: confirm, dry_run: dry_run)
185
+ tasks_to_run << HR_Deploy::PrecompileAssetsTask.new(confirm: confirm, dry_run: dry_run)
186
+ end
287
187
 
288
- puts 'Checking Heroku status...'
188
+ tasks_to_run << HR_Deploy::PushCodeTask.new(target: target, confirm: confirm, dry_run: dry_run)
289
189
 
290
- uri = URI.parse('https://status.heroku.com/api/v3/current-status')
190
+ if db_change
191
+ tasks_to_run << HR_Deploy::MigrateDBTask.new(target: target, confirm: confirm, dry_run: dry_run)
192
+ tasks_to_run << HR_Deploy::RestartTask.new(target: target, confirm: confirm, dry_run: dry_run)
193
+ end
291
194
 
292
- http = Net::HTTP.new(uri.host, uri.port)
293
- http.use_ssl = true
195
+ if target.maintenance_interaction?
196
+ tasks_to_run << HR_Deploy::DisableMaintenanceTask.new(target: target, confirm: confirm, dry_run: dry_run)
197
+ end
294
198
 
295
- request = Net::HTTP::Get.new(uri.request_uri)
199
+ if target.pinging_interaction?
200
+ tasks_to_run << HR_Deploy::EnablePingerTask.new(target: target, confirm: confirm, dry_run: dry_run)
201
+ end
296
202
 
297
- begin
298
- response = http.request(request)
203
+ if target.s3_asset_sync?
204
+ tasks_to_run << HR_Deploy::CleanNewAssetsTask.new(confirm: confirm, dry_run: dry_run)
205
+ end
206
+ end
299
207
 
300
- rescue
301
- puts 'Could not fetch Heroku status'
302
- puts
303
- abort_deploy('Could not fetch Heroku status')
208
+ def run_tasks
209
+ tasks_to_run.each do |task|
304
210
 
305
- else
306
- if response.code == '200'
211
+ task.run
307
212
 
308
- status = JSON.parse(response.body)
309
- status = status['status'] if status
213
+ if task.successful?
214
+ finished_tasks << task
215
+ puts
216
+ else
217
+ puts
218
+ abort_deploy(task)
219
+ end
220
+ end
310
221
 
311
- if status && status['Production'] && status['Development']
222
+ # Successfully finished running all tasks
223
+ deploy_succeeded
224
+ end
312
225
 
313
- production_ok = status['Production'] == 'green'
314
- development_ok = status['Development'] == 'green'
226
+ def abort_deploy(failed_task)
315
227
 
316
- if production_ok && development_ok
317
- puts 'Heroku is operating correctly'
318
- puts
319
- else
320
- puts 'Heroku is having problems'
321
- puts
322
- abort_deploy('Heroku is having problems')
323
- end
324
- else
228
+ puts '-' * 50
229
+ puts
230
+ print_app_status "* #{application_state(failed_task)}"
231
+ puts
232
+ print_fail "* #{failed_task.error}"
233
+ puts
325
234
 
326
- puts 'Could not parse Heroku status'
327
- puts
328
- abort_deploy('Could not parse Heroku status')
329
- end
235
+ # Logic to remove tasks for which it doesn't make sense to print reversal instruction
236
+ filter_finished_tasks
330
237
 
331
- else
332
- puts 'Could not fetch Heroku status'
238
+ finished_tasks.each do |task|
239
+ if task.reversal_instruction
240
+ puts "* #{task.reversal_instruction}"
333
241
  puts
334
- abort_deploy('Could not fetch Heroku status')
335
242
  end
336
243
  end
337
- end
338
244
 
339
- def disable_pinging
340
- puts 'Disabling availability pinger...'
341
- execute_system_command("curl #{target.disable_pinger_url}")
342
- puts
245
+ print_fail 'Deploy aborted'
246
+ exit 1
343
247
  end
344
248
 
345
- def enable_maintenance
346
- puts 'Enabling maintenance mode...'
347
- execute_system_command("heroku maintenance:on --remote #{target.name}")
348
- puts
349
- end
249
+ def deploy_succeeded
250
+ if dry_run
251
+ print_deploy_successful 'Dry run complete'
252
+ else
253
+ print_deploy_successful "Deploy complete in #{(Time.now - start_time).round} seconds"
254
+ end
350
255
 
351
- def backup_db
352
- puts 'Backing up database...'
353
- execute_system_command("heroku pgbackups:capture --expire --remote #{target.name}")
354
- puts
256
+ # Execution is finished
257
+ exit 0
355
258
  end
356
259
 
357
- def push_code
358
- puts 'Pushing code...'
359
- execute_system_command("git push #{target.name} master")
360
- puts
260
+ def application_state(failed_task)
261
+
262
+ did_not_change = 'Application state did not change'
263
+ unknown = 'Application state is unknown'
264
+ deployed = 'Application has been deployed'
265
+
266
+ case failed_task
267
+ when HR_Deploy::CheckNetworkTask
268
+ did_not_change
269
+ when HR_Deploy::CheckHerokuTask
270
+ did_not_change
271
+ when HR_Deploy::DisablePingerTask
272
+ did_not_change
273
+ when HR_Deploy::EnableMaintenanceTask
274
+ did_not_change
275
+ when HR_Deploy::BackupDBTask
276
+ did_not_change
277
+ when HR_Deploy::CleanOldAssetsTask
278
+ did_not_change
279
+ when HR_Deploy::PrecompileAssetsTask
280
+ did_not_change
281
+ when HR_Deploy::PushCodeTask
282
+ did_not_change
283
+ when HR_Deploy::MigrateDBTask
284
+ unknown
285
+ when HR_Deploy::RestartTask
286
+ unknown
287
+ when HR_Deploy::DisableMaintenanceTask
288
+ deployed
289
+ when HR_Deploy::EnablePingerTask
290
+ deployed
291
+ when HR_Deploy::CleanNewAssetsTask
292
+ deployed
293
+ else
294
+ raise "Can't determine application state: failed task type is unknown"
295
+ end
361
296
  end
362
297
 
363
- def migrate_db
364
- puts 'Migrating database...'
365
- execute_system_command("heroku run rake db:migrate --remote #{target.name}")
366
- puts
298
+ def filter_finished_tasks
299
+
300
+ # Doesn't make sense to show instruction to enable pinger if it has been enabled
301
+ enable_pinger = finished_tasks.find { |task| task.class == HR_Deploy::EnablePingerTask }
302
+ disable_pinger = finished_tasks.find { |task| task.class == HR_Deploy::DisablePingerTask }
303
+ if enable_pinger && disable_pinger
304
+ finished_tasks.delete(enable_pinger)
305
+ finished_tasks.delete(disable_pinger)
306
+ end
307
+
308
+ # Doesn't make sense to show instruction to disable maintenance if it has already been disabled
309
+ enable_maintenance = finished_tasks.find { |task| task.class == HR_Deploy::EnableMaintenanceTask }
310
+ disable_maintenance = finished_tasks.find { |task| task.class == HR_Deploy::DisableMaintenanceTask }
311
+ if enable_maintenance && disable_maintenance
312
+ finished_tasks.delete(enable_maintenance)
313
+ finished_tasks.delete(disable_maintenance)
314
+ end
315
+
316
+ # Doesn't make sense to show instruction to reverse pushing code and db migration
317
+ # if has already pushed, migrated and restarted (deploy itself is complete)
318
+ if finished_tasks.find { |task| task.class == HR_Deploy::RestartTask }
319
+ push_code = finished_tasks.find { |task| task.class == HR_Deploy::PushCodeTask }
320
+ migrate_db = finished_tasks.find { |task| task.class == HR_Deploy::MigrateDBTask }
321
+ restart = finished_tasks.find { |task| task.class == HR_Deploy::RestartTask }
322
+ finished_tasks.delete(push_code)
323
+ finished_tasks.delete(migrate_db)
324
+ finished_tasks.delete(restart)
325
+ end
367
326
  end
368
327
 
369
- def restart
370
- puts 'Restarting...'
371
- execute_system_command("heroku restart --remote #{target.name}")
372
- puts
328
+ def print_fail(msg)
329
+ puts msg.red
373
330
  end
374
331
 
375
- def enable_pinging
376
- puts 'Enabling availability pinger...'
377
- execute_system_command("curl #{target.enable_pinger_url}")
378
- puts
332
+ def print_deploy_successful(msg)
333
+ puts msg.green
379
334
  end
380
335
 
381
- def disable_maintenance
382
- puts 'Disabling maintenance mode...'
383
- execute_system_command("heroku maintenance:off --remote #{target.name}")
384
- puts
336
+ def print_app_status(msg)
337
+ puts msg.yellow
385
338
  end
386
339
  end
387
340
  end