sunshine 1.0.0.pre

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.
Files changed (71) hide show
  1. data/History.txt +237 -0
  2. data/Manifest.txt +70 -0
  3. data/README.txt +277 -0
  4. data/Rakefile +46 -0
  5. data/bin/sunshine +5 -0
  6. data/examples/deploy.rb +61 -0
  7. data/examples/deploy_tasks.rake +112 -0
  8. data/examples/standalone_deploy.rb +31 -0
  9. data/lib/commands/add.rb +96 -0
  10. data/lib/commands/default.rb +169 -0
  11. data/lib/commands/list.rb +322 -0
  12. data/lib/commands/restart.rb +62 -0
  13. data/lib/commands/rm.rb +83 -0
  14. data/lib/commands/run.rb +151 -0
  15. data/lib/commands/start.rb +72 -0
  16. data/lib/commands/stop.rb +61 -0
  17. data/lib/sunshine/app.rb +876 -0
  18. data/lib/sunshine/binder.rb +70 -0
  19. data/lib/sunshine/crontab.rb +143 -0
  20. data/lib/sunshine/daemon.rb +380 -0
  21. data/lib/sunshine/daemons/ar_sendmail.rb +28 -0
  22. data/lib/sunshine/daemons/delayed_job.rb +30 -0
  23. data/lib/sunshine/daemons/nginx.rb +104 -0
  24. data/lib/sunshine/daemons/rainbows.rb +35 -0
  25. data/lib/sunshine/daemons/server.rb +66 -0
  26. data/lib/sunshine/daemons/unicorn.rb +26 -0
  27. data/lib/sunshine/dependencies.rb +103 -0
  28. data/lib/sunshine/dependency_lib.rb +200 -0
  29. data/lib/sunshine/exceptions.rb +54 -0
  30. data/lib/sunshine/healthcheck.rb +83 -0
  31. data/lib/sunshine/output.rb +131 -0
  32. data/lib/sunshine/package_managers/apt.rb +48 -0
  33. data/lib/sunshine/package_managers/dependency.rb +349 -0
  34. data/lib/sunshine/package_managers/gem.rb +54 -0
  35. data/lib/sunshine/package_managers/yum.rb +62 -0
  36. data/lib/sunshine/remote_shell.rb +241 -0
  37. data/lib/sunshine/repo.rb +128 -0
  38. data/lib/sunshine/repos/git_repo.rb +122 -0
  39. data/lib/sunshine/repos/rsync_repo.rb +29 -0
  40. data/lib/sunshine/repos/svn_repo.rb +78 -0
  41. data/lib/sunshine/server_app.rb +554 -0
  42. data/lib/sunshine/shell.rb +384 -0
  43. data/lib/sunshine.rb +391 -0
  44. data/templates/logrotate/logrotate.conf.erb +11 -0
  45. data/templates/nginx/nginx.conf.erb +109 -0
  46. data/templates/nginx/nginx_optimize.conf +23 -0
  47. data/templates/nginx/nginx_proxy.conf +13 -0
  48. data/templates/rainbows/rainbows.conf.erb +18 -0
  49. data/templates/tasks/sunshine.rake +114 -0
  50. data/templates/unicorn/unicorn.conf.erb +6 -0
  51. data/test/fixtures/app_configs/test_app.yml +11 -0
  52. data/test/fixtures/sunshine_test/test_upload +0 -0
  53. data/test/mocks/mock_object.rb +179 -0
  54. data/test/mocks/mock_open4.rb +117 -0
  55. data/test/test_helper.rb +188 -0
  56. data/test/unit/test_app.rb +489 -0
  57. data/test/unit/test_binder.rb +20 -0
  58. data/test/unit/test_crontab.rb +128 -0
  59. data/test/unit/test_git_repo.rb +26 -0
  60. data/test/unit/test_healthcheck.rb +70 -0
  61. data/test/unit/test_nginx.rb +107 -0
  62. data/test/unit/test_rainbows.rb +26 -0
  63. data/test/unit/test_remote_shell.rb +102 -0
  64. data/test/unit/test_repo.rb +42 -0
  65. data/test/unit/test_server.rb +324 -0
  66. data/test/unit/test_server_app.rb +425 -0
  67. data/test/unit/test_shell.rb +97 -0
  68. data/test/unit/test_sunshine.rb +157 -0
  69. data/test/unit/test_svn_repo.rb +55 -0
  70. data/test/unit/test_unicorn.rb +22 -0
  71. metadata +217 -0
@@ -0,0 +1,876 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # App objects are the core of sunshine deployment. The sunshine paradygm
5
+ # is to construct an app object, and run custom deploy code by passing
6
+ # a block to its deploy method:
7
+ #
8
+ # options = {
9
+ # :name => 'myapp',
10
+ # :repo => {:type => :svn, :url => 'svn://blah...'},
11
+ # :root_path => '/usr/local/myapp',
12
+ # :remote_shells => ['user@someserver.com']
13
+ # }
14
+ #
15
+ # app = Sunshine::App.new(options)
16
+ #
17
+ # app.deploy do |app|
18
+ #
19
+ # app_server = Sunshine::Rainbows.new(app)
20
+ # app_server.restart
21
+ #
22
+ # Sunshine::Nginx.new(app, :point_to => app_server).restart
23
+ #
24
+ # end
25
+ #
26
+ # Multiple apps can be defined, and deployed from a single deploy script.
27
+ # The constructor also supports passing a yaml file path:
28
+ #
29
+ # Sunshine::App.new("path/to/config.yml")
30
+ #
31
+ # Deployment can be expressed more concisely by calling App::deploy:
32
+ #
33
+ # App.deploy("path/to/config.yml") do |app|
34
+ # Sunshine::Rainbows.new(app).restart
35
+ # end
36
+
37
+ class App
38
+
39
+ ##
40
+ # Initialize and deploy an application.
41
+ # Takes any arguments supported by the constructor.
42
+
43
+ def self.deploy(*args, &block)
44
+ app = new(*args)
45
+ app.deploy(&block)
46
+ app
47
+ end
48
+
49
+
50
+ attr_reader :name, :repo, :server_apps, :sudo
51
+ attr_reader :root_path, :checkout_path, :current_path, :deploys_path
52
+ attr_reader :shared_path, :log_path, :deploy_name, :deploy_env
53
+
54
+
55
+ def initialize config_file=Sunshine::DATA, options={}
56
+ options, config_file = config_file, Sunshine::DATA if Hash === config_file
57
+
58
+
59
+ @deploy_env = options[:deploy_env] || Sunshine.deploy_env
60
+
61
+ binder = Binder.new self
62
+ binder.import_hash options
63
+ binder.forward :deploy_env
64
+
65
+ options = config_from_file(config_file, binder.get_binding).merge options
66
+
67
+
68
+ @repo = repo_from_config options[:repo]
69
+
70
+ @name = options[:name] || @repo.name
71
+
72
+ @deploy_name = options[:deploy_name] || Time.now.to_i.to_s
73
+
74
+ @server_app_filter = nil
75
+
76
+ set_deploy_paths options[:root_path]
77
+
78
+ @server_apps = server_apps_from_config options[:remote_shells]
79
+
80
+ self.sudo = options[:sudo] || Sunshine.sudo
81
+
82
+ @shell_env = {
83
+ "RACK_ENV" => @deploy_env.to_s,
84
+ "RAILS_ENV" => @deploy_env.to_s
85
+ }
86
+ shell_env options[:shell_env]
87
+
88
+ @post_user_lambdas = []
89
+ end
90
+
91
+
92
+ ##
93
+ # Connect server apps.
94
+
95
+ def connect options=nil
96
+ with_server_apps options,
97
+ :msg => "Connecting..." do |server_app|
98
+ server_app.shell.connect
99
+ end
100
+ end
101
+
102
+
103
+ ##
104
+ # Check if server apps are connected.
105
+
106
+ def connected? options=nil
107
+ with_server_apps options, :no_threads => true do |server_app|
108
+ return false unless server_app.shell.connected?
109
+ end
110
+
111
+ true
112
+ end
113
+
114
+
115
+ ##
116
+ # Disconnect server apps.
117
+
118
+ def disconnect options=nil
119
+ with_server_apps options,
120
+ :msg => "Disconnecting..." do |server_app|
121
+ server_app.shell.disconnect
122
+ end
123
+ end
124
+
125
+
126
+ ##
127
+ # Deploy the application to deploy servers and
128
+ # call user's post-deploy code. Supports any App#find options.
129
+
130
+ def deploy options=nil
131
+ Sunshine.logger.info :app, "Beginning deploy of #{@name}" do
132
+ connect options
133
+ end
134
+
135
+ deploy_trap = Sunshine.add_trap "Reverting deploy of #{@name}" do
136
+ revert! options
137
+ end
138
+
139
+ with_filter options do |app|
140
+ make_app_directories
141
+ checkout_codebase
142
+ symlink_current_dir
143
+
144
+ yield(self) if block_given?
145
+
146
+ run_post_user_lambdas
147
+
148
+ health :enable
149
+
150
+ build_control_scripts
151
+ build_deploy_info_file
152
+ build_crontab
153
+
154
+ register_as_deployed
155
+ remove_old_deploys
156
+ end
157
+
158
+ rescue => e
159
+ message = "#{e.class}: #{e.message}"
160
+
161
+ Sunshine.logger.error :app, message do
162
+ Sunshine.logger.error '>>', e.backtrace.join("\n")
163
+ revert!
164
+ end
165
+
166
+ ensure
167
+ Sunshine.delete_trap deploy_trap
168
+
169
+ Sunshine.logger.info :app, "Ending deploy of #{@name}" do
170
+ disconnect options
171
+ end
172
+ end
173
+
174
+
175
+ ##
176
+ # Symlink current directory to previous checkout and remove
177
+ # the current deploy directory.
178
+
179
+ def revert!(options=nil)
180
+ with_server_apps options,
181
+ :msg => "Reverting to previous deploy.",
182
+ :send => :revert!
183
+ end
184
+
185
+
186
+ ##
187
+ # Add paths the the shell $PATH env.
188
+
189
+ def add_shell_paths(*paths)
190
+ path = @shell_env["PATH"] || "$PATH"
191
+ paths << path
192
+
193
+ shell_env "PATH" => paths.join(":")
194
+ end
195
+
196
+
197
+ ##
198
+ # Add a command to the crontab to be generated remotely:
199
+ # add_to_crontab "reboot", "@reboot /path/to/app/start", :role => :web
200
+
201
+ def add_to_crontab name, cronjob, options=nil
202
+ with_server_apps options do |server_app|
203
+ server_app.crontab[name] = cronjob
204
+ end
205
+ end
206
+
207
+
208
+ ##
209
+ # Add a command to a control script to be generated remotely:
210
+ # add_to_script :start, "do this on start"
211
+ # add_to_script :start, "start_mail", :role => :mail
212
+
213
+ def add_to_script name, script, options=nil
214
+ with_server_apps options do |server_app|
215
+ server_app.scripts[name] << script
216
+ end
217
+ end
218
+
219
+
220
+ ##
221
+ # Define lambdas to run right after the user's yield.
222
+ # app.after_user_script do |app|
223
+ # ...
224
+ # end
225
+
226
+ def after_user_script &block
227
+ @post_user_lambdas << block
228
+ end
229
+
230
+
231
+ ##
232
+ # Creates and uploads all control scripts for the application.
233
+ # To add to, or define a control script, see App#add_to_script.
234
+
235
+ def build_control_scripts options=nil
236
+ with_server_apps options,
237
+ :msg => "Building control scripts",
238
+ :send => :build_control_scripts
239
+ end
240
+
241
+
242
+ ##
243
+ # Writes the crontab on all or selected server apps.
244
+ # To add or remove from the crontab, see App#add_to_crontab and
245
+ # App#remove_cronjob.
246
+
247
+ def build_crontab options=nil
248
+ with_server_apps options,
249
+ :msg => "Building the crontab" do |server_app|
250
+ server_app.crontab.write!
251
+ end
252
+ end
253
+
254
+
255
+ ##
256
+ # Creates a yaml file with deploy information. To add custom information
257
+ # to the info file, use the app's info hash attribute:
258
+ # app.info[:key] = "some value"
259
+
260
+ def build_deploy_info_file options=nil
261
+ with_server_apps options,
262
+ :msg => "Creating info file",
263
+ :send => :build_deploy_info_file
264
+ end
265
+
266
+
267
+ ##
268
+ # Parse an erb file and return the newly created string.
269
+ # Default binding is the app's binding.
270
+
271
+ def build_erb erb_file, custom_binding=nil
272
+ str = File === erb_file ? erb_file.read : File.read(erb_file)
273
+ ERB.new(str, nil, '-').result(custom_binding || binding)
274
+ end
275
+
276
+
277
+ ##
278
+ # Checks out the app's codebase to one or all deploy servers.
279
+
280
+ def checkout_codebase options=nil
281
+ with_server_apps options,
282
+ :msg => "Checking out codebase",
283
+ :send => [:checkout_repo, @repo]
284
+
285
+ rescue => e
286
+ raise CriticalDeployError, e
287
+ end
288
+
289
+
290
+ ##
291
+ # Get a hash of deploy information for each server app.
292
+ # Post-deploy only.
293
+
294
+ def deploy_details options=nil
295
+ details = {}
296
+
297
+ with_server_apps options, :msg => "Getting deploy info..." do |server_app|
298
+ details[server_app.shell.host] = server_app.deploy_details
299
+ end
300
+
301
+ details
302
+ end
303
+
304
+
305
+ ##
306
+ # Check if app has been deployed successfully.
307
+
308
+ def deployed? options=nil
309
+ with_server_apps options, :no_threads => true do |server_app|
310
+ return false unless server_app.deployed?
311
+ end
312
+
313
+ true
314
+ end
315
+
316
+
317
+ ##
318
+ # Iterate over each server app.
319
+
320
+ def each(options=nil, &block)
321
+ server_apps = find(options)
322
+ server_apps.each(&block)
323
+ end
324
+
325
+
326
+ ##
327
+ # Find server apps matching the passed requirements.
328
+ # Returns an array of server apps.
329
+ # find :user => 'db'
330
+ # find :host => 'someserver.com'
331
+ # find :role => :web
332
+
333
+ def find query=nil
334
+ if @server_app_filter
335
+ if Hash === query && Hash === @server_app_filter
336
+ query.merge! @server_app_filter
337
+ else
338
+ query = @server_app_filter
339
+ end
340
+ end
341
+
342
+ return @server_apps if query.nil? || query == :all
343
+
344
+ @server_apps.select do |sa|
345
+ next unless sa.shell.user == query[:user] if query[:user]
346
+ next unless sa.shell.host == query[:host] if query[:host]
347
+
348
+ next unless sa.has_roles?(query[:role]) if query[:role]
349
+
350
+ true
351
+ end
352
+ end
353
+
354
+
355
+ ##
356
+ # Decrypt a file using gpg. Allows all DeployServerDispatcher#find
357
+ # options, plus:
358
+ # :output:: str - the path the output file should go to
359
+ # :passphrase:: str - the passphrase gpg should use
360
+
361
+ def gpg_decrypt gpg_file, options={}
362
+ options[:passphrase] ||=
363
+ Sunshine.shell.ask("Enter gpg passphrase:") do |q|
364
+ q.echo = false
365
+ end
366
+
367
+ with_server_apps options,
368
+ :msg => "Gpg decrypt: #{gpg_file}",
369
+ :send => [:gpg_decrypt, gpg_file, options]
370
+ end
371
+
372
+
373
+ %w{gem yum apt}.each do |dep_type|
374
+ self.class_eval <<-STR, __FILE__, __LINE__ + 1
375
+ ##
376
+ # Install one or more #{dep_type} packages.
377
+ # See Settler::#{dep_type.capitalize}#new for supported options.
378
+
379
+ def #{dep_type}_install(*names)
380
+ options = names.last if Hash === names.last
381
+ with_server_apps options,
382
+ :msg => "Installing #{dep_type} packages",
383
+ :send => [:#{dep_type}_install, *names]
384
+ end
385
+ STR
386
+ end
387
+
388
+
389
+ ##
390
+ # Gets or sets the healthcheck state. Returns a hash of host/state
391
+ # pairs. State values are :enabled, :disabled, and :down. The method
392
+ # argument can be omitted or take a value of :enable, :disable, or :remove:
393
+ # app.health
394
+ # #=> Returns the health status for all server_apps
395
+ #
396
+ # app.health :role => :web
397
+ # #=> Returns the status of all server_apps of role :web
398
+ #
399
+ # app.health :enable
400
+ # #=> Enables all health checking and returns the status
401
+ #
402
+ # app.health :disable, :role => :web
403
+ # #=> Disables health checking for :web server_apps and returns the status
404
+
405
+ def health method=nil, options=nil
406
+ valid_methods = [:enable, :disable, :remove]
407
+ options = method if options.nil? && Hash === method
408
+
409
+ valid_method = valid_methods.include? method
410
+
411
+ message = "#{method.to_s.capitalize[0..-2]}ing" if valid_method
412
+ message ||= "Getting status of"
413
+ message = "#{message} healthcheck"
414
+
415
+ statuses = {}
416
+ with_server_apps options, :msg => message do |server_app|
417
+ server_app.health.send method if valid_method
418
+
419
+ statuses[server_app.shell.host] = server_app.health.status
420
+ end
421
+
422
+ statuses
423
+ end
424
+
425
+
426
+ ##
427
+ # Install dependencies defined as a Sunshine dependency object:
428
+ # rake = Sunshine.dependencies.gem 'rake', :version => '~>0.8'
429
+ # apache = Sunshine.dependencies.yum 'apache'
430
+ # app.install_deps rake, apache
431
+ #
432
+ # Deploy servers can also be specified as a dispatcher, array, or single
433
+ # deploy server, by passing standard 'find' options:
434
+ # postgres = Sunshine.dependencies.yum 'postgresql'
435
+ # pgserver = Sunshine.dependencies.yum 'postgresql-server'
436
+ # app.install_deps postgres, pgserver, :role => 'db'
437
+ #
438
+ # If a dependency was already defined in the Sunshine dependency tree,
439
+ # the dependency name may be passed instead of the object:
440
+ # app.install_deps 'nginx', 'ruby'
441
+
442
+ def install_deps(*deps)
443
+ options = Hash === deps[-1] ? deps.delete_at(-1) : {}
444
+
445
+ with_server_apps options,
446
+ :msg => "Installing dependencies: #{deps.map{|d| d.to_s}.join(" ")}",
447
+ :send => [:install_deps, *deps]
448
+ end
449
+
450
+
451
+ ##
452
+ # Creates the required application directories.
453
+
454
+ def make_app_directories options=nil
455
+ with_server_apps options,
456
+ :msg => "Creating #{@name} directories",
457
+ :send => :make_app_directories
458
+
459
+ rescue => e
460
+ raise FatalDeployError, e
461
+ end
462
+
463
+
464
+ ##
465
+ # Assign the prefered package manager to all server_apps:
466
+ # app.prefer_pkg_manager Settler::Yum
467
+ #
468
+ # Package managers are typically detected automatically by each
469
+ # individual server_apps.
470
+
471
+ def prefer_pkg_manager pkg_manager, options=nil
472
+ with_server_apps options,
473
+ :send => [:pkg_manager=, pkg_manager]
474
+ end
475
+
476
+
477
+ ##
478
+ # Run a rake task on any or all deploy servers.
479
+
480
+ def rake command, options=nil
481
+ with_server_apps options,
482
+ :msg => "Running Rake task '#{command}'",
483
+ :send => [:rake, command]
484
+ end
485
+
486
+
487
+ ##
488
+ # Adds the app to the deploy servers deployed-apps list.
489
+
490
+ def register_as_deployed options=nil
491
+ with_server_apps options,
492
+ :msg => "Registering app with deploy servers",
493
+ :send => :register_as_deployed
494
+ end
495
+
496
+
497
+ ##
498
+ # Remove a cron job from the remote crontabs:
499
+ # remove_cronjob "reboot", :role => :web
500
+ # remove_cronjob :all
501
+ # #=> deletes all cronjobs related to this app
502
+
503
+ def remove_cronjob name, options=nil
504
+ with_server_apps options,
505
+ :msg => "Removing cronjob #{name.inspect}" do |server_app|
506
+ if name == :all
507
+ server_app.crontab.clear
508
+ else
509
+ server_app.crontab.delete(name)
510
+ end
511
+ end
512
+ end
513
+
514
+
515
+ ##
516
+ # Removes old deploys from the checkout_dir
517
+ # based on Sunshine's max_deploy_versions.
518
+
519
+ def remove_old_deploys options=nil
520
+ with_server_apps options,
521
+ :msg => "Removing old deploys (max = #{Sunshine.max_deploy_versions})",
522
+ :send => :remove_old_deploys
523
+ end
524
+
525
+
526
+ ##
527
+ # Run the restart script of a deployed app on the specified
528
+ # deploy servers.
529
+ # Post-deploy only.
530
+
531
+ def restart options=nil
532
+ with_server_apps options,
533
+ :msg => "Running restart script",
534
+ :send => :restart
535
+ end
536
+
537
+
538
+ ##
539
+ # Runs bundler on deploy servers.
540
+
541
+ def run_bundler options=nil
542
+ with_server_apps options,
543
+ :msg => "Running Bundler",
544
+ :send => :run_bundler
545
+
546
+ rescue => e
547
+ raise CriticalDeployError, e
548
+ end
549
+
550
+
551
+ ##
552
+ # Runs GemInstaller on deploy servers.
553
+
554
+ def run_geminstaller options=nil
555
+ with_server_apps options,
556
+ :msg => "Running GemInstaller",
557
+ :send => :run_geminstaller
558
+
559
+ rescue => e
560
+ raise CriticalDeployError, e
561
+ end
562
+
563
+
564
+ ##
565
+ # Run lambdas that were saved for after the user's script.
566
+ # See #after_user_script.
567
+
568
+ def run_post_user_lambdas
569
+ @post_user_lambdas.each{|l| l.call self}
570
+ end
571
+
572
+
573
+ ##
574
+ # Run a sass task on any or all deploy servers.
575
+
576
+ def sass *sass_names
577
+ options = sass_names.delete_at(-1) if Hash === sass_names.last
578
+
579
+ with_server_apps options,
580
+ :msg => "Running Sass for #{sass_names.join(' ')}",
581
+ :send => [:sass, *sass_names]
582
+ end
583
+
584
+
585
+ ##
586
+ # Set and return the remote shell env variables.
587
+ # Also assigns shell environment to the app's deploy servers.
588
+
589
+ def shell_env env_hash=nil
590
+ env_hash ||= {}
591
+
592
+ @shell_env.merge!(env_hash)
593
+
594
+ with_server_apps :all,
595
+ :msg => "Shell env: #{@shell_env.inspect}" do |server_app|
596
+ server_app.shell_env.merge!(@shell_env)
597
+ end
598
+
599
+ @shell_env.dup
600
+ end
601
+
602
+
603
+ ##
604
+ # Run the start script of a deployed app on the specified
605
+ # deploy servers.
606
+ # Post-deploy only.
607
+
608
+ def start options=nil
609
+ with_server_apps options,
610
+ :msg => "Running start script",
611
+ :send => [:start, options]
612
+ end
613
+
614
+
615
+ ##
616
+ # Get a hash of which deploy server apps are :running or :down.
617
+ # Post-deploy only.
618
+
619
+ def status options=nil
620
+ statuses = {}
621
+
622
+ with_server_apps options, :msg => "Querying app status..." do |server_app|
623
+ statuses[server_app.shell.host] = server_app.status
624
+ end
625
+
626
+ statuses
627
+ end
628
+
629
+
630
+ ##
631
+ # Run the stop script of a deployed app on the specified
632
+ # deploy servers.
633
+ # Post-deploy only.
634
+
635
+ def stop options=nil
636
+ with_server_apps options,
637
+ :msg => "Running stop script",
638
+ :send => :stop
639
+ end
640
+
641
+
642
+ ##
643
+ # Use sudo on deploy servers. Set to true/false, or
644
+ # a username to use 'sudo -u'.
645
+
646
+ def sudo=(value)
647
+ with_server_apps :all,
648
+ :msg => "Using sudo = #{value.inspect}" do |server_app|
649
+ server_app.shell.sudo = value
650
+ end
651
+
652
+ @sudo = value
653
+ end
654
+
655
+
656
+ ##
657
+ # Creates a symlink to the app's checkout path.
658
+
659
+ def symlink_current_dir options=nil
660
+ with_server_apps options,
661
+ :msg => "Symlinking #{@checkout_path} -> #{@current_path}",
662
+ :send => :symlink_current_dir
663
+
664
+ rescue => e
665
+ raise CriticalDeployError, e
666
+ end
667
+
668
+
669
+ ##
670
+ # Iterate over all deploy servers but create a thread for each
671
+ # deploy server. Means you can't return from the passed block!
672
+
673
+ def threaded_each(options=nil, &block)
674
+ mutex = Mutex.new
675
+ threads = []
676
+
677
+ return_val = each(options) do |server_app|
678
+
679
+ thread = Thread.new do
680
+ server_app.shell.with_mutex mutex do
681
+ yield server_app
682
+ end
683
+ end
684
+
685
+ # We don't want deploy servers to keep doing things if one fails
686
+ thread.abort_on_exception = true
687
+
688
+ threads << thread
689
+ end
690
+
691
+ threads.each{|t| t.join }
692
+
693
+ return_val
694
+ end
695
+
696
+
697
+ ##
698
+ # Upload common rake tasks from the sunshine lib.
699
+ # app.upload_tasks
700
+ # #=> upload all tasks
701
+ # app.upload_tasks 'app', 'common', :role => :web
702
+ # #=> upload app and common rake files
703
+ #
704
+ # Allows standard DeployServerDispatcher#find options, plus:
705
+ # :remote_path:: str - the remote absolute path to upload the files to
706
+
707
+ def upload_tasks *files
708
+ options = Hash === files.last ? files.last.dup : {}
709
+
710
+ options.delete(:remote_path)
711
+ options = :all if options.empty?
712
+
713
+ with_server_apps options,
714
+ :msg => "Uploading tasks: #{files.join(" ")}",
715
+ :send => [:upload_tasks, *files]
716
+ end
717
+
718
+
719
+ ##
720
+ # Execute a block with a specified server app filter:
721
+ # app.with_filter :role => :cdn do |app|
722
+ # app.sass 'file1', 'file2', 'file3'
723
+ # app.rake 'asset:packager:build_all'
724
+ # end
725
+
726
+ def with_filter filter_hash
727
+ old_filter, @server_app_filter = @server_app_filter, filter_hash
728
+
729
+ yield self
730
+
731
+ @server_app_filter = old_filter
732
+ end
733
+
734
+
735
+ ##
736
+ # Calls a method for server_apps found with the passed options,
737
+ # and with an optional log message. Supports all App#find
738
+ # options, plus:
739
+ # :no_threads:: bool - disable threaded execution
740
+ # :msg:: "some message" - log message
741
+ #
742
+ # app.with_server_apps :all, :msg => "doing something" do |server_app|
743
+ # # do something here
744
+ # end
745
+ #
746
+ # app.with_server_apps :role => :db, :user => "bob" do |server_app|
747
+ # # do something here
748
+ # end
749
+
750
+ def with_server_apps search_options, options={}
751
+ options = search_options.merge options if Hash === search_options
752
+
753
+ message = options[:msg]
754
+ method = options[:no_threads] ? :each : :threaded_each
755
+
756
+ block = lambda do
757
+ send(method, search_options) do |server_app|
758
+
759
+ if block_given?
760
+ yield(server_app)
761
+
762
+ elsif options[:send]
763
+ server_app.send(*options[:send])
764
+ end
765
+ end
766
+ end
767
+
768
+
769
+ if message
770
+ Sunshine.logger.info(:app, message, &block)
771
+
772
+ else
773
+ block.call
774
+ end
775
+ end
776
+
777
+
778
+ private
779
+
780
+
781
+ ##
782
+ # Set all the app paths based on the root deploy path.
783
+
784
+ def set_deploy_paths path
785
+ @root_path = path || File.join(Sunshine.web_directory, @name)
786
+ @current_path = "#{@root_path}/current"
787
+ @deploys_path = "#{@root_path}/deploys"
788
+ @shared_path = "#{@root_path}/shared"
789
+ @log_path = "#{@shared_path}/log"
790
+ @checkout_path = "#{@deploys_path}/#{@deploy_name}"
791
+ end
792
+
793
+
794
+ ##
795
+ # Set the app's deploy servers:
796
+ # server_apps_from_config "some_server"
797
+ # #=> [<ServerApp @host="some_server"...>]
798
+ #
799
+ # server_apps_from_config ["svr1", "svr2", "svr3"]
800
+ # #=> [<ServerApp @host="svr1">,<ServerApp @host="svr2">, ...]
801
+ #
802
+ # d_servers = [["svr1", {:roles => "web db app"}], "svr2", "svr3"]
803
+ # server_apps_from_config d_servers
804
+ # #=> [<ServerApp @host="svr1">,<ServerApp @host="svr2">, ...]
805
+
806
+ def server_apps_from_config d_servers
807
+ d_servers = [*d_servers].compact
808
+ d_servers.map{|ds| ServerApp.new(*[self,*ds]) }
809
+ end
810
+
811
+
812
+ ##
813
+ # Set the app's repo:
814
+ # repo_from_config SvnRepo.new("myurl")
815
+ # repo_from_config :type => :svn, :url => "myurl"
816
+
817
+ def repo_from_config repo_def
818
+ case repo_def
819
+ when Sunshine::Repo
820
+ repo_def
821
+ when Hash
822
+ Sunshine::Repo.new_of_type repo_def[:type],
823
+ repo_def[:url], repo_def
824
+ else
825
+ Sunshine::Repo.detect Sunshine::PATH
826
+ end
827
+ end
828
+
829
+
830
+ ##
831
+ # Load a yml config file, parses it with erb and resolves deploy env
832
+ # inheritance.
833
+
834
+ def config_from_file config_file, erb_binding=binding, env=@deploy_env
835
+ return {} unless config_file
836
+
837
+ config_data = YAML.load build_erb(config_file, erb_binding)
838
+
839
+ load_config_for config_data, env
840
+ end
841
+
842
+
843
+ ##
844
+ # Loads an app yml config file, gets the default config
845
+ # and the current deploy env and returns a merged config hash.
846
+
847
+ def load_config_for config_hash, env
848
+ return {} unless config_hash
849
+
850
+ deploy_env_config = (config_hash[env] || {}).dup
851
+ deploy_env_config[:inherits] ||= []
852
+ deploy_env_config[:inherits].unshift(:default) if
853
+ :default != env && config_hash[:default]
854
+
855
+ merge_config_inheritance deploy_env_config, config_hash
856
+ end
857
+
858
+
859
+ ##
860
+ # Recursively merges config hashes based on the value at :inherits
861
+
862
+ def merge_config_inheritance main_config, all_configs
863
+ new_config = {}
864
+ parents = [*main_config[:inherits]].compact
865
+
866
+ parents.each do |config_name|
867
+ parent = all_configs[config_name]
868
+ parent = merge_config_inheritance parent, all_configs
869
+
870
+ new_config = new_config.merge parent
871
+ end
872
+
873
+ new_config.merge main_config # Two merges important for inheritance order
874
+ end
875
+ end
876
+ end