etsy-deployinator 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ZWRmY2ZlZDEzOTQ5YjJiY2YwOGNiN2RmZjFlNzY1ZDkyOWFhYTNiZA==
5
- data.tar.gz: !binary |-
6
- YjE2MjYwOGUyOTRmYzI1MmI0Y2RkMjQ3ZGQzOTYyZjZkYzYyOGY3Yg==
2
+ SHA1:
3
+ metadata.gz: f825a038dbf37c774aada076bb6d5c16185ed23e
4
+ data.tar.gz: df3bcfdc241bb68177693fa2a3603510844dd989
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- MzExZmU1Y2JlNjY5ZTg2ZWM2ZWVmZDU1NjNmMjM0NmEyNTZkZjg1OTUwZTAy
10
- NDBiYWM5OTkwYjYyZGQ4MjhiYTliOWJmNDU2YTEzNjFmMmRlOWRiYzMyNzdj
11
- Y2E5MGU5Zjc2M2ExMmU2NjU0ZjRmZDVhNDZjZDM0NWY2ZGZmM2I=
12
- data.tar.gz: !binary |-
13
- ZTQ5OWJiNzNkZmQzNzJmOTI1ZTkzMjFmNzNjMzBkZDIxNjhmN2Y0YWVmYjkw
14
- NjlhODNjZjc4YzQ4MDgxOWQzYmY1NjdmNzA5NGU2NDEwNzg5NTY5MTFmNzYy
15
- YjZmODRmMTE5ZTYwMjA4ZmY4MjY2OTI1NzQ3ZDk0YzczZmVhYWY=
6
+ metadata.gz: 8a63d5192abcbc7d262f61ef706713988e74c1080111329c0c6fe28781214ec82b441e7e06f6a7fc79e31b371384597f263a3a814f40296c3c846e600b28b2d9
7
+ data.tar.gz: 21122e91e6b8a87f49c69bb567c4c8a710359f467d98498e65de600e43937c3905cef52c5e13cbd388a2c5514f0d4c06e58cae9bc688492bb0cbce121ab2682b
@@ -0,0 +1,3 @@
1
+ .bundle/
2
+ vendor/
3
+ Gemfile.lock
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - 2.2.0
7
+ before_install:
8
+ - gem update bundler
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in deployinator.gemspec
4
3
  gemspec
data/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
  _________ ______ _____ _____
3
3
  ______ /_____ ________ ___ /______ _____ _____(_)_______ ______ ___ /_______ ________
4
4
  _ __ / _ _ \___ __ \__ / _ __ \__ / / /__ / __ __ \_ __ `/_ __/_ __ \__ ___/
5
- / /_/ / / __/__ /_/ /_ / / /_/ /_ /_/ / _ / _ / / // /_/ / / /_ / /_/ /_ /
6
- \__,_/ \___/ _ .___/ /_/ \____/ _\__, / /_/ /_/ /_/ \__,_/ \__/ \____/ /_/
5
+ / /_/ / / __/__ /_/ /_ / / /_/ /_ /_/ / _ / _ / / // /_/ / / /_ / /_/ /_ /
6
+ \__,_/ \___/ _ .___/ /_/ \____/ _\__, / /_/ /_/ /_/ \__,_/ \__/ \____/ /_/
7
7
  /_/ /____/ Deploy with style!
8
8
  </pre>
9
9
 
@@ -28,9 +28,9 @@ Deployinator is a deployment framework extracted from Etsy. We've been using it
28
28
  ## Stacks
29
29
  Deployments are grouped by "stacks". You might have a "web" and "search" stack.
30
30
 
31
- Each of those stacks might have different deployment environments, such as "staging" or "production".
31
+ Each of those stacks might have different deployment environments, such as "staging" or "production".
32
32
 
33
- You can map a button to each of these environments, to create multi-stage pushes within each stack.
33
+ You can map a button to each of these environments, to create multi-stage pushes within each stack.
34
34
 
35
35
  ## Installation
36
36
 
@@ -55,13 +55,14 @@ you can skip the bundler steps.
55
55
  - Run the following command to make deployinator gem's rake tasks available to you:
56
56
 
57
57
  ```sh
58
+ $ shopt -s xpg_echo
58
59
  $ echo "require 'deployinator'\nload 'deployinator/tasks/initialize.rake' " > Rakefile
59
60
  ```
60
61
 
61
62
  - Create a binstub for the deploy log tailing backend:
62
63
 
63
64
  ```sh
64
- bundle install --binstubs deployinator
65
+ bundle binstub etsy-deployinator
65
66
  ```
66
67
 
67
68
  - Initialize the project directory by running the following command replacing ___Company___ with the name of your company/organization. This must start with a capital letter.
@@ -116,14 +117,14 @@ underscores but if you forget the rake task will convert from camelcase for you.
116
117
  $ bundle exec shotgun --host localhost -p 7777 config.ru
117
118
  ```
118
119
 
119
- - You will probably want a robust server like apache to handle production traffic.
120
+ - You will probably want a robust server like apache to handle production traffic.
120
121
 
121
122
  - The `config/base.rb` file is the base config for the application. Replace all
122
123
  occurences of ___test_stack___ with the name you chose above. Also the example
123
124
  below uses a git repository of http://github.com/etsy/deployinator, feel free to
124
125
  replace this with your specific repository
125
126
 
126
- ```ruby
127
+ ```ruby
127
128
  Deployinator.app_context['test_stack_config'] = {
128
129
  :prod_host => "localhost",
129
130
  :checkout_path => "/tmp/deployinator_dev/"
@@ -232,7 +233,7 @@ For example you could wrap your capistrano deploy:
232
233
  run_cmd %Q{cap deploy}
233
234
 
234
235
 
235
- #### log_and_stream
236
+ #### log_and_stream
236
237
 
237
238
  Output information to the log file, and the streaming output handler.
238
239
  The real time output console renders HTML so you should use markup here.
@@ -241,7 +242,7 @@ The real time output console renders HTML so you should use markup here.
241
242
 
242
243
  #### log_and_shout
243
244
 
244
- Output an announcement message with build related information.
245
+ Output an announcement message with build related information.
245
246
  Also includes hooks for Email.
246
247
 
247
248
  log_and_shout({
@@ -348,8 +349,20 @@ To set these simple override the methods in your view class. For example:
348
349
  This can be done on a global layout that extends the gem's default layout or on
349
350
  a stack by stack basis in their own view.
350
351
 
352
+ ### Maintenance mode
353
+ Deployinator has a setting for maintenance mode, which is mostly useful if you
354
+ have major changes that affect all stacks and you want to make sure no deploys
355
+ are going on while you make the change. In order to enable it, you have to set
356
+ `Deployinator.maintenance_mode = true`. This will make all pages for anyone
357
+ not in `Deployinator.admin_groups` go to `/maintenance`. As a Deployinator
358
+ admin you can then still deploy stacks and use the app.
359
+
360
+ On the maintenance page Deployinator will show the value of
361
+ `Deployinator.maintenance_contact` as the place to get help in case you need
362
+ any or are confused about maintenance mode.
363
+
351
364
  ## Hacking on the gem
352
- If you find issues with the gem, or would like to play around with it, you can check it out from git and start hacking on it.
365
+ If you find issues with the gem, or would like to play around with it, you can check it out from git and start hacking on it.
353
366
  First tell bundler to use your local copy instead by running:
354
367
  ```sh
355
368
  $ bundle config local.deployinator /path/to/DeployinatorGem
@@ -359,6 +372,30 @@ Next, on every code change, you can install from the checked out gem by running
359
372
  $ bundle install --no-deployment && bundle install --deployment
360
373
  ```
361
374
 
375
+ ### Stats dashboard
376
+ The `/stats` page pulls from `log/deployinator.log` to show a graph of deployments per day for each stack over time. By default, it shows all stacks. To blacklist or whitelist certain stacks, update `config/base.rb` with:
377
+ ```rb
378
+ Deployinator.stats_included_stacks = ['my_whitelisted_stack', 'another_whitelisted_stack']
379
+ Deployinator.stats_ignored_stacks = ['my_stack_to_ignore', 'another_stack_to_ignore']
380
+ Deployinator.stats_extra_grep = 'Production deploy' # filter out log entries matching this string
381
+ ```
382
+
383
+ Whitelisting stacks or applying a custom extra grep can help speed up graph rendering when you have a large log file.
384
+
385
+ If at some point you change the name of a stack, you can group the old log entries with the new by adding the following to `config/base.rb`:
386
+
387
+ ```rb
388
+ Deployinator.stats_renamed_stacks = [
389
+ {
390
+ :previous_stack => {
391
+ :stack => [ "old_stack_name" ]
392
+ },
393
+ :new_name => "new_stack_name"
394
+ }
395
+ ]
396
+ ```
397
+
398
+
362
399
  ### Contributing
363
400
 
364
401
  1. Fork it
data/Rakefile CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  require 'rake/testtask'
3
2
  require 'rdoc/task'
4
3
  require 'bundler/gem_tasks'
@@ -54,8 +54,8 @@ EM.run do
54
54
  if stack && @@channels.key?(stack)
55
55
  @@channels[stack].unsubscribe(session_id)
56
56
  @@channels_count[stack] -= 1
57
- if @@channels_count[stack] == 0
58
- @@tailers[stack].close
57
+ if @@channels_count[stack] == 0 && @@tailers.has_key?(stack)
58
+ @@tailers[stack].close
59
59
  @@tailers.delete(stack)
60
60
  end
61
61
  end
@@ -20,14 +20,18 @@ Gem::Specification.new do |gem|
20
20
  gem.required_ruby_version = '>= 1.9.3'
21
21
 
22
22
  gem.add_development_dependency "mocha", "~> 0.14"
23
+ gem.add_development_dependency "test-unit", ">= 3"
23
24
 
24
25
  gem.add_runtime_dependency "rake", "~> 10", ">= 10.3.2"
25
26
  gem.add_runtime_dependency "json", "~> 1.8"
26
27
  gem.add_runtime_dependency "mustache", "~> 0.99"
28
+ # pony pulls in mail >2.0. mail 3.0 needs ruby 2 or greater. to maintain ruby 1.9 compat we're pinning mail here
29
+ gem.add_runtime_dependency "mail", "2.6.3"
27
30
  gem.add_runtime_dependency "pony", "~> 1.5"
28
31
  gem.add_runtime_dependency "tlsmail", "~> 0.0"
29
32
  gem.add_runtime_dependency "eventmachine", "~> 1.0", ">= 1.0.4"
30
- gem.add_runtime_dependency "eventmachine-tail", "~> 0.6", ">= 0.6.4"
33
+ gem.add_runtime_dependency "eventmachine-tail", "~> 0.6", ">= 0.6.5"
31
34
  gem.add_runtime_dependency "em-websocket", "~> 0.5", ">= 0.5.1"
32
35
  gem.add_runtime_dependency "sinatra", "~> 1.4", ">=1.4.3"
36
+ gem.add_runtime_dependency "celluloid", "~> 0.16"
33
37
  end
@@ -54,10 +54,27 @@ module Deployinator
54
54
  attr_accessor :admin_groups
55
55
  @admin_groups = []
56
56
 
57
+ attr_accessor :maintenance_mode
58
+
59
+ attr_accessor :maintenance_contact
60
+
57
61
  # the controller class. defaults to Deployinator::Controller
58
62
  # if you override this it should be a subclass of Deployinator::Controller
59
63
  attr_accessor :deploy_controller
60
64
 
65
+ # include stacks in /stats
66
+ attr_accessor :stats_included_stacks
67
+
68
+ # exclude stacks in /stats, even if present in stats_included_stacks
69
+ attr_accessor :stats_ignored_stacks
70
+
71
+ # filter log entries in /stats
72
+ attr_accessor :stats_extra_grep
73
+
74
+ # list of configurations for grouping historical / renamed stacks in /stats
75
+ attr_accessor :stats_renamed_stacks
76
+ @@stats_renamed_stacks = []
77
+
61
78
  def initialize
62
79
  @stack_plugins = {}
63
80
  @global_plugins = []
@@ -101,12 +118,14 @@ end
101
118
  Deployinator.root_dir = Dir.pwd
102
119
  Deployinator.app_context = {}
103
120
  Deployinator.admin_groups = []
121
+ Deployinator.maintenance_mode = false
122
+ Deployinator.maintenance_contact = "admin@deployinator.example.com"
104
123
  Deployinator.global_plugins = []
105
124
  Deployinator.log_file = Deployinator.root(["log", "development.log"])
106
125
  Deployinator.log_path = Deployinator.root(["log", "deployinator.log"])
107
126
  Deployinator.timing_log_path = Deployinator.root(["log", "deployinator-timing.log"])
108
127
  Deployinator.git_sha_length = "10"
109
- Deployinator.default_user = `whoami`
128
+ Deployinator.default_user = `/usr/bin/whoami`
110
129
  Deployinator.stack_tailer_port = 7778
111
130
  Deployinator.github_host = 'github.com'
112
131
  Deployinator.app_context['context'] = 'dev'
@@ -12,6 +12,8 @@ require 'deployinator/views/log'
12
12
  require 'deployinator/views/run_logs'
13
13
  require 'deployinator/views/log_table'
14
14
  require 'deployinator/views/deploys_status'
15
+ require 'deployinator/views/stats'
16
+ require 'deployinator/views/maintenance'
15
17
 
16
18
  module Deployinator
17
19
  class DeployinatorApp < Sinatra::Base
@@ -41,12 +43,32 @@ module Deployinator
41
43
  register_plugins(nil)
42
44
  init(env)
43
45
  @disabled_override = params[:override].nil? ? false : true
46
+ pass if [
47
+ "/maintenance",
48
+ "/css/maintenance.css",
49
+ "/images/maintenance.gif",
50
+ "/static/css/style.css"
51
+ ].include? request.path_info
52
+ if Deployinator.maintenance_mode == true && !is_admin?
53
+ redirect "/maintenance"
54
+ end
55
+ end
56
+
57
+ before do
44
58
  end
45
59
 
46
60
  get '/' do
47
61
  mustache Deployinator::Views::Index
48
62
  end
49
63
 
64
+ get '/stats' do
65
+ mustache Deployinator::Views::Stats
66
+ end
67
+
68
+ get '/maintenance' do
69
+ mustache Deployinator::Views::Maintenance
70
+ end
71
+
50
72
  get '/run_logs/view/:log' do
51
73
  template = open("#{File.dirname(__FILE__)}/templates/stream.mustache").read
52
74
  template.gsub!("{{ yield }}", "{{{yield}}}")
@@ -95,17 +117,6 @@ module Deployinator
95
117
  git_head_rev(params[:stack]).chomp
96
118
  end
97
119
 
98
- get '/:thing' do
99
- @stack = params[:thing]
100
- @params = params
101
- register_plugins(@stack)
102
- begin
103
- mustache @stack
104
- rescue Errno::ENOENT
105
- pass
106
- end
107
- end
108
-
109
120
  get '/:stack/remove-lock' do
110
121
  stack = params[:stack]
111
122
  unlock_pushes(stack) if can_remove_stack_lock?
@@ -129,6 +140,10 @@ module Deployinator
129
140
  send_file "#{File.dirname(__FILE__)}/static/css/highlight.css"
130
141
  end
131
142
 
143
+ get '/css/maintenance.css' do
144
+ send_file "#{File.dirname(__FILE__)}/static/css/maintenance.css"
145
+ end
146
+
132
147
  get '/js/flot/jquery.flot.min.js' do
133
148
  send_file "#{File.dirname(__FILE__)}/static/js/flot/jquery.flot.min.js"
134
149
  end
@@ -149,6 +164,14 @@ module Deployinator
149
164
  send_file "#{File.dirname(__FILE__)}/static/js/jquery.timed_bar.js"
150
165
  end
151
166
 
167
+ get '/js/stats_load.js' do
168
+ send_file "#{File.dirname(__FILE__)}/static/js/stats_load.js"
169
+ end
170
+
171
+ get '/images/maintenance.gif' do
172
+ send_file "#{File.dirname(__FILE__)}/static/images/maintenance.gif"
173
+ end
174
+
152
175
  get '/deploys_status' do
153
176
  mustache Deployinator::Views::DeploysStatus
154
177
  end
@@ -172,6 +195,7 @@ module Deployinator
172
195
  # the background.
173
196
  post '/deploys/?' do
174
197
  params[:username] = @username
198
+ params[:groups] = @groups
175
199
  params[:block] = Proc.new { |line| foo = line }
176
200
  deploy_running = is_deploy_active?(params[:stack], params[:stage])
177
201
 
@@ -180,7 +204,7 @@ module Deployinator
180
204
  return 403
181
205
  end
182
206
 
183
- fork {
207
+ pid = fork {
184
208
  Signal.trap("HUP") { exit }
185
209
  Deployinator.setup_logging
186
210
  $0 = get_deploy_process_title(params[:stack], params[:stage])
@@ -188,6 +212,7 @@ module Deployinator
188
212
  d = controller.new
189
213
  d.run(params)
190
214
  }
215
+ Process.detach(pid)
191
216
  200
192
217
  end
193
218
 
@@ -200,5 +225,21 @@ module Deployinator
200
225
 
201
226
  200
202
227
  end
228
+
229
+ get '/:thing' do
230
+ @stack = params[:thing]
231
+
232
+ unless Deployinator.get_stacks.include?(@stack)
233
+ raise "No such stack #{@stack}"
234
+ end
235
+
236
+ @params = params
237
+ register_plugins(@stack)
238
+ begin
239
+ mustache @stack
240
+ rescue Errno::ENOENT
241
+ pass
242
+ end
243
+ end
203
244
  end
204
245
  end
@@ -29,6 +29,7 @@ module Deployinator
29
29
  @deploy_start_time = Time.now.to_i
30
30
  @start_time = Time.now.to_i
31
31
  @username = args[:username]
32
+ @groups = args[:groups]
32
33
  @host = `hostname -s`
33
34
  @stack = args[:stack]
34
35
  @method = args[:method]
@@ -37,7 +38,7 @@ module Deployinator
37
38
 
38
39
  # This gets the runlog output on the console; is used by log_and_stream
39
40
  @block = args[:block] || Proc.new do |output|
40
- $stdout.write output.gsub!(/(<[^>]*>)|\n|\t/s) {" "}
41
+ $stdout.write output.gsub!(/(<[^>]*>)|\n|\t/m) {" "}
41
42
  $stdout.write "\n"
42
43
  end
43
44
  end
@@ -102,7 +103,11 @@ module Deployinator
102
103
  deploy_instance = deploy_class.new(options)
103
104
  deploy_instance.register_plugins(options[:stack])
104
105
 
105
- deploy_instance.lock_pushes(options[:stack], options[:username], options[:method])
106
+ locked = deploy_instance.lock_pushes(options[:stack], options[:username], options[:method])
107
+
108
+ unless locked
109
+ return deploy_instance
110
+ end
106
111
 
107
112
  @start_time = Time.now
108
113
  deploy_instance.log_and_stream "Push started at #{@start_time.to_i}\n"
@@ -115,7 +120,7 @@ module Deployinator
115
120
  state = deploy_instance.send(options[:method], options)
116
121
  rescue Exception => e
117
122
  deploy_instance.log_error("There was an exception during this deploy. Aborted!", e)
118
- deploy_instance.raise_event(:deploy_error)
123
+ deploy_instance.raise_event(:deploy_error, {:exception => e})
119
124
  end
120
125
 
121
126
  if state.nil? || !state.is_a?(Hash)
@@ -75,11 +75,37 @@ module Deployinator
75
75
  #
76
76
  # Returns nothing
77
77
  def log_and_stream(output)
78
- write_file output, @filename if @filename
78
+ write_file output, runlog_filename if runlog_filename
79
79
  return @block.call(output) unless @block.nil?
80
80
  ""
81
81
  end
82
82
 
83
+ # gives the filename to send runlog to based on whether we are in the main thread or not
84
+ # We do this because we want to be able to use log_and_stream seamlessly in a
85
+ # parallel thread. So, all log_and_stream calls in all but the main thread will
86
+ # log to a seaparate file
87
+ # output - String filename to log to
88
+ def runlog_filename(name=nil)
89
+ if @filename
90
+ if Thread.main == Thread.current
91
+ @filename
92
+ elsif Thread.current[:logfile_name]
93
+ Thread.current[:logfile_name]
94
+ elsif name
95
+ Thread.current[:logfile_name] = runlog_thread_filename(name)
96
+ Thread.current[:logfile_name]
97
+ else
98
+ raise 'Logfile name not defined in thread. Expecting name parameter to be passed in.'
99
+ end
100
+ end
101
+ end
102
+
103
+ # gives us the filename to log to in thread
104
+ # output - String filename for thread
105
+ def runlog_thread_filename(name)
106
+ @filename + '-' + name.to_s
107
+ end
108
+
83
109
  # Run external command with timing information
84
110
  # streams and logs the output of the command as well
85
111
  # If the command fails, it is retried some number of times
@@ -343,7 +369,7 @@ module Deployinator
343
369
  end
344
370
  end
345
371
 
346
- # Public: gets the contes from a cache file if it hasn't expired
372
+ # Public: gets the contents from a cache file if it hasn't expired
347
373
  #
348
374
  # Paramaters:
349
375
  # cache_file: path to a cache file
@@ -370,14 +396,30 @@ module Deployinator
370
396
  end
371
397
  end
372
398
 
399
+ # Public: writes the supplied contents to the cache file, ensuring that
400
+ # encoding is correct
401
+ #
402
+ # Parameters:
403
+ # cache_file: path to the cache file
404
+ # content: the data to write to the file
405
+ #
406
+ # Returns nothing
407
+ def write_to_cache(cache_file, contents)
408
+ File.open(cache_file, 'w:UTF-8') do |f|
409
+ f.write(contents.force_encoding('UTF-8'))
410
+ end
411
+ end
412
+
373
413
  def lock_pushes(stack, who, method)
374
- log_and_stream("LOCKING #{stack}")
414
+ log_and_stream("LOCKING #{stack}<br>")
375
415
  if lock_info = push_lock_info(stack)
376
- return log_and_stream("Pushes locked by #{lock_info[:who]} - #{lock_info[:method]}")
416
+ log_and_stream("Pushes locked by #{lock_info[:who]} - #{lock_info[:method]}<br>")
417
+ return false
377
418
  end
378
419
 
379
420
  dt = Time.now.strftime("%m/%d/%Y %H:%M")
380
421
  log_string_to_file("#{who}|#{method}|#{dt}", push_lock_path(stack))
422
+ return true
381
423
  end
382
424
 
383
425
  def unlock_pushes(stack)
@@ -424,16 +466,19 @@ module Deployinator
424
466
 
425
467
  # Public: wrap a block into a timeout
426
468
  #
427
- # seconds - timeout in seconds
428
- # description - optional description for logging (default:"")
429
- # &block - block to call
469
+ # seconds - timeout in seconds
470
+ # description - optional description for logging (default:"")
471
+ # throw_exception - options param to throw exception back up stack
472
+ # quiet - optional boolean for logging as a big red warning using the stderr div class
473
+ # extra_opts - optional hash to pass along to plugins
474
+ # &block - block to call
430
475
  #
431
476
  # Example
432
477
  # with_timeout(20){system("curl -s http://google.com")}
433
478
  # with_timeout 30 do; system("curl -s http://google.com"); end
434
479
  #
435
480
  # Returns nothing
436
- def with_timeout(seconds, description=nil, &block)
481
+ def with_timeout(seconds, description=nil, throw_exception=false, quiet=false, extra_opts={}, &block)
437
482
  begin
438
483
  Timeout.timeout(seconds) do
439
484
  yield
@@ -443,13 +488,22 @@ module Deployinator
443
488
  info += " for #{description}" unless description.nil?
444
489
  # log and stream if log filename is not undefined
445
490
  if (/undefined/ =~ @filename).nil?
446
- log_and_stream "<div class=\"stderr\">#{info}</div>"
491
+ if quiet
492
+ log_and_stream "#{info}<br>"
493
+ else
494
+ log_and_stream "<div class=\"stderr\">#{info}</div>"
495
+ end
447
496
  end
448
497
  state = {
449
498
  :seconds => seconds,
450
- :info => info
499
+ :info => info,
500
+ :stack => stack,
501
+ :extra_opts => extra_opts
451
502
  }
452
503
  raise_event(:timeout, state)
504
+ if throw_exception
505
+ raise e
506
+ end
453
507
  ""
454
508
  end
455
509
  end
@@ -492,7 +546,7 @@ module Deployinator
492
546
  raise "Must have stack" unless options[:stack]
493
547
 
494
548
  options[:env] ||= "PROD"
495
- options[:nice_env] = nicify_env(options[:env])
549
+ options[:nice_env] ||= nicify_env(options[:env])
496
550
  options[:user] ||= @username
497
551
  options[:start] = @start_time unless options[:start] || ! @start_time
498
552
 
@@ -548,7 +602,7 @@ module Deployinator
548
602
  # stack should be the last part of the log line from the last pipe to the end
549
603
  # modified this to take into account the run_log entry at the end
550
604
  unless stacks.empty? && env.empty?
551
- grep = "| egrep '#{env}.*\\|\(#{stacks.join("|")}\)(|(\\||$))?'"
605
+ grep = "| egrep '#{env}.*\\|\(#{stacks.join("|")}\)\\|'"
552
606
  end
553
607
 
554
608
  # extra grep does another filter to the line, needed to get CONFIG PRODUCTION
@@ -600,11 +654,53 @@ module Deployinator
600
654
  log_msg = e.nil? ? msg : "#{msg} (#{e.message})"
601
655
  log_and_stream "<div class=\"stderr\">#{log_msg}</div>"
602
656
  if !e.nil?
603
- log_and_stream e.backtrace.inspect
657
+ begin
658
+ template = open("#{File.dirname(__FILE__)}/templates/exception.mustache").read
659
+
660
+ regex = /(?<file>.*?):(?<line>\d+):.*?`(?<method>.*)'/
661
+ context = e.backtrace.map do |line|
662
+ match = regex.match(line)
663
+ {
664
+ :file => match['file'],
665
+ :line => match['line'],
666
+ :method => match['method']
667
+ }
668
+ end
669
+
670
+ output = Mustache.render(template, {:exceptions => context})
671
+ log_and_stream output
672
+ rescue
673
+ log_and_stream e.backtrace.inspect
674
+ end
604
675
  end
605
676
  # This is so we have something in the log if/when this fails
606
677
  puts log_msg
607
678
  end
608
679
 
680
+ # Public: helper method to check if the current user is part of
681
+ # an group. This method let's you pass in groups for explicitness and
682
+ # testing but will fall back on @group and Deployinator.admin_groups.
683
+ #
684
+ # Param:
685
+ # user_groups - groups a user is part of
686
+ # admin_groups - admin groups
687
+ #
688
+ # Returns true if the current user is part of and admin group and false
689
+ # otherwise
690
+ def is_admin?(user_groups=nil, admin_groups=nil)
691
+ user_groups ||= @groups
692
+ admin_groups ||= Deployinator.admin_groups
693
+
694
+ return false if user_groups.nil?
695
+ return false if admin_groups.nil?
696
+
697
+ intersected_groups = user_groups & admin_groups
698
+
699
+ # if we found an intersection between admin and user groups, the current
700
+ # user is an admin
701
+ return (intersected_groups.length > 0)
702
+
703
+ end
704
+
609
705
  end
610
706
  end