sunshine 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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,554 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Handles App deployment functionality for a single deploy server.
5
+ #
6
+ # Server apps can be assigned any number of roles for classification.
7
+ # :roles:: sym|array - roles assigned (web, db, app, etc...)
8
+ # By default server apps get the special :all role which will
9
+ # always return true when calling:
10
+ # server_app.has_roles? :some_role
11
+ #
12
+ # ServerApp objects can be instantiated several ways:
13
+ # ServerApp.new app_instance, shell_instance, options_hash
14
+ #
15
+ # When passing an App instance, the new ServerApp will keep an active link
16
+ # to the app's properties. Name, deploy, and path attributes will be
17
+ # actively linked.
18
+ #
19
+ # Rely on ServerApp to create a RemoteShell instance to use:
20
+ # ServerApp.new app_instance, "host.com", options_hash
21
+ #
22
+ # Instantiate with app name and rely on Sunshine defaults for app paths:
23
+ # ServerApp.new "app_name", shell_instance, options_hash
24
+ #
25
+ # Explicitely assign the app's root path:
26
+ # ServerApp.new "app_name", ..., :root_path => "/path/to/app_root"
27
+ #
28
+ # Assigning a specific deploy name to use can be done with the
29
+ # :deploy_name option:
30
+ # ServerApp.new "app_name", ..., :deploy_name => "deploy"
31
+
32
+ class ServerApp
33
+
34
+
35
+ ##
36
+ # Define an attribue that will get a value from app, or locally if
37
+ # @app isn't set.
38
+
39
+ def self.app_attr *attribs
40
+ attribs.each do |attrib|
41
+ class_eval <<-STR, __FILE__, __LINE__ + 1
42
+ def #{attrib}
43
+ @app ? @app.send(:#{attrib}) : @#{attrib}
44
+ end
45
+ STR
46
+ end
47
+ end
48
+
49
+
50
+ app_attr :name, :deploy_name
51
+ app_attr :root_path, :checkout_path, :current_path
52
+ app_attr :deploys_path, :log_path, :shared_path
53
+
54
+ attr_accessor :app, :roles, :scripts, :info, :shell, :crontab, :health
55
+ attr_writer :pkg_manager
56
+
57
+ def initialize app, host, options={}
58
+
59
+ @app = App === app ? app : nil
60
+
61
+ name = @app && @app.name || app
62
+ assign_local_app_attr name, options
63
+
64
+ @deploy_details = nil
65
+
66
+ @roles = options[:roles] || [:all]
67
+ @roles = @roles.split(" ") if String === @roles
68
+ @roles = [*@roles].compact.map{|r| r.to_sym }
69
+
70
+ @scripts = Hash.new{|h, k| h[k] = []}
71
+ @info = {:ports => {}}
72
+
73
+ @pkg_manager = nil
74
+
75
+ @shell = case host
76
+ when String then RemoteShell.new host, options
77
+ when Shell then host
78
+ else
79
+ raise "Could not get remote shell '#{host}'"
80
+ end
81
+
82
+ @crontab = Crontab.new name, @shell
83
+ @health = Healthcheck.new shared_path, @shell
84
+ end
85
+
86
+
87
+ ##
88
+ # Add paths the the shell $PATH env.
89
+
90
+ def add_shell_paths(*paths)
91
+ path = shell_env["PATH"] || "$PATH"
92
+ paths << path
93
+
94
+ shell_env.merge! "PATH" => paths.join(":")
95
+ end
96
+
97
+
98
+ ##
99
+ # Creates and uploads all control scripts for the application.
100
+ # To add to, or define a control script, see App#add_to_script.
101
+
102
+ def build_control_scripts
103
+
104
+ write_script "env", make_env_bash_script
105
+
106
+ build_scripts = @scripts.dup
107
+
108
+ if build_scripts[:restart].empty? &&
109
+ !build_scripts[:start].empty? && !build_scripts[:stop].empty?
110
+ build_scripts[:restart] << "#{self.root_path}/stop"
111
+ build_scripts[:restart] << "#{self.root_path}/start"
112
+ end
113
+
114
+ if build_scripts[:status].empty?
115
+ build_scripts[:status] << "echo 'No daemons for #{self.name}'; exit 1;"
116
+ end
117
+
118
+ build_scripts.each do |name, cmds|
119
+ if cmds.empty?
120
+ Sunshine.logger.warn @shell.host, "#{name} script is empty"
121
+ end
122
+
123
+ bash = make_bash_script name, cmds
124
+
125
+ write_script name, bash
126
+ end
127
+ end
128
+
129
+
130
+ ##
131
+ # Creates a yaml file with deploy information. To add custom information
132
+ # to the info file, use the app's info hash attribute:
133
+ # app.info[:key] = "some value"
134
+
135
+ def build_deploy_info_file
136
+
137
+ deploy_info = get_deploy_info.to_yaml
138
+
139
+ @shell.make_file "#{self.checkout_path}/info", deploy_info
140
+
141
+ @shell.symlink "#{self.current_path}/info", "#{self.root_path}/info"
142
+ end
143
+
144
+
145
+
146
+ ##
147
+ # Checks out the app's codebase to the checkout path.
148
+
149
+ def checkout_repo repo
150
+ @info[:scm] = repo.checkout_to self.checkout_path, @shell
151
+ end
152
+
153
+
154
+ ##
155
+ # Get post-mortum information about the app's deploy, from the
156
+ # generated deploy info file.
157
+ # Post-deploy only.
158
+
159
+ def deploy_details reload=false
160
+ return @deploy_details if @deploy_details && !reload
161
+ @deploy_details =
162
+ YAML.load @shell.call("cat #{self.current_path}/info") rescue nil
163
+ end
164
+
165
+
166
+ ##
167
+ # Checks if the server_app's current info file deploy_name matches
168
+ # the server_app's deploy_name attribute.
169
+
170
+ def deployed?
171
+ success =
172
+ @deploy_details[:deploy_name] == self.deploy_name if @deploy_details
173
+
174
+ return success if success
175
+
176
+ deploy_details(true)[:deploy_name] == self.deploy_name rescue false
177
+ end
178
+
179
+
180
+ ##
181
+ # An array of all directories used by the app.
182
+ # Does not include symlinked directories.
183
+
184
+ def directories
185
+ [root_path, deploys_path, shared_path, log_path, checkout_path]
186
+ end
187
+
188
+
189
+ ##
190
+ # Returns information about the deploy at hand.
191
+
192
+ def get_deploy_info
193
+ { :deployed_at => Time.now.to_s,
194
+ :deployed_as => @shell.call("whoami"),
195
+ :deployed_by => Sunshine.shell.user,
196
+ :deploy_name => File.basename(self.checkout_path),
197
+ :roles => @roles,
198
+ :path => self.root_path
199
+ }.merge @info
200
+ end
201
+
202
+
203
+ ##
204
+ # Decrypt a file using gpg. Allows options:
205
+ # :output:: str - the path the output file should go to
206
+ # :passphrase:: str - the passphrase gpg should use
207
+
208
+ def gpg_decrypt gpg_file, options={}
209
+ output_file = options[:output] || gpg_file.gsub(/\.gpg$/, '')
210
+
211
+ passphrase = options[:passphrase]
212
+ passphrase_file = "#{self.root_path}/tmp/gpg_passphrase"
213
+
214
+ gpg_cmd = "gpg --batch --no-tty --yes --output #{output_file} "+
215
+ "--passphrase-file #{passphrase_file} --decrypt #{gpg_file}"
216
+
217
+ @shell.call "mkdir -p #{File.dirname(passphrase_file)}"
218
+
219
+ @shell.make_file passphrase_file, passphrase
220
+
221
+ @shell.call "cd #{self.checkout_path} && #{gpg_cmd}"
222
+ @shell.call "rm -f #{passphrase_file}"
223
+ end
224
+
225
+
226
+ ##
227
+ # Check if this server app includes the specified roles:
228
+
229
+ def has_roles? *roles
230
+ return true if @roles.include? :all
231
+
232
+ roles.each do |role|
233
+ return false unless @roles.include? role
234
+ end
235
+
236
+ true
237
+ end
238
+
239
+
240
+ %w{gem yum apt}.each do |dep_type|
241
+ self.class_eval <<-STR, __FILE__, __LINE__ + 1
242
+ ##
243
+ # Install one or more #{dep_type} packages.
244
+ # See #{dep_type.capitalize}#new for supported options.
245
+
246
+ def #{dep_type}_install(*names)
247
+ options = Hash === names.last ? names.delete_at(-1) : Hash.new
248
+
249
+ names.each do |name|
250
+ dep = #{dep_type.capitalize}.new(name, options)
251
+ dep.install! :call => @shell
252
+ end
253
+ end
254
+ STR
255
+ end
256
+
257
+
258
+ ##
259
+ # Install dependencies previously defined in Sunshine.dependencies.
260
+
261
+ def install_deps(*deps)
262
+ options = {:call => @shell, :prefer => pkg_manager}
263
+ options.merge! deps.delete_at(-1) if Hash === deps.last
264
+
265
+ args = deps << options
266
+ Sunshine.dependencies.install(*args)
267
+ end
268
+
269
+
270
+ ##
271
+ # Creates the required application directories.
272
+
273
+ def make_app_directories
274
+ @shell.call "mkdir -p #{self.directories.join(" ")}"
275
+ end
276
+
277
+
278
+ ##
279
+ # Makes an array of bash commands into a script that
280
+ # echoes 'true' on success.
281
+
282
+ def make_bash_script name, cmds
283
+ cmds = cmds.map{|cmd| "(#{cmd})" }
284
+
285
+ cmds << "echo true"
286
+
287
+ bash = <<-STR
288
+ #!/bin/bash
289
+ if [ "$1" == "--no-env" ]; then
290
+ #{cmds.flatten.join(" && ")}
291
+ else
292
+ #{self.root_path}/env #{self.root_path}/#{name} --no-env
293
+ fi
294
+ STR
295
+ end
296
+
297
+
298
+ ##
299
+ # Creates the one-off env script that will be used by other scripts
300
+ # to correctly set their env variables.
301
+
302
+ def make_env_bash_script
303
+ env_str = shell_env.map{|e| e.join("=")}.join(" ")
304
+ "#!/bin/bash\nenv #{env_str} \"$@\""
305
+ end
306
+
307
+
308
+ ##
309
+ # Returns the type of package management system to use.
310
+
311
+ def pkg_manager
312
+ @pkg_manager ||=
313
+ (@shell.call("yum --version") && Yum) rescue Apt
314
+ end
315
+
316
+
317
+ ##
318
+ # Run a rake task the deploy server.
319
+
320
+ def rake command
321
+ install_deps 'rake', :type => Gem
322
+ @shell.call "cd #{self.checkout_path} && rake #{command}"
323
+ end
324
+
325
+
326
+ ##
327
+ # Adds the app to the deploy server's deployed-apps list
328
+
329
+ def register_as_deployed
330
+ AddCommand.exec self.root_path, 'servers' => [@shell]
331
+ end
332
+
333
+
334
+ ##
335
+ # Removes old deploys from the checkout_dir
336
+ # based on Sunshine's max_deploy_versions.
337
+
338
+ def remove_old_deploys
339
+ deploys = @shell.call("ls -1 #{self.deploys_path}").split("\n")
340
+
341
+ return unless deploys.length > Sunshine.max_deploy_versions
342
+
343
+ lim = Sunshine.max_deploy_versions + 1
344
+
345
+ rm_deploys = deploys[0..-lim]
346
+ rm_deploys.map!{|d| "#{self.deploys_path}/#{d}"}
347
+
348
+ @shell.call("rm -rf #{rm_deploys.join(" ")}")
349
+ end
350
+
351
+
352
+ ##
353
+ # Run the app's restart script. Returns false on failure.
354
+ # Post-deploy only.
355
+
356
+ def restart
357
+ @shell.call "#{self.root_path}/restart" rescue false
358
+ end
359
+
360
+
361
+ ##
362
+ # Symlink current directory to previous checkout and remove
363
+ # the current deploy directory.
364
+
365
+ def revert!
366
+ @shell.call "rm -rf #{self.checkout_path}"
367
+
368
+ last_deploy = @shell.call("ls -rc1 #{self.deploys_path}").split("\n").last
369
+
370
+ if last_deploy && !last_deploy.empty?
371
+ @shell.symlink "#{self.deploys_path}/#{last_deploy}", self.current_path
372
+
373
+ Sunshine.logger.info @shell.host, "Reverted to #{last_deploy}"
374
+
375
+ unless start :force => true
376
+ Sunshine.logger.error @shell.host, "Failed #{@name} startup"
377
+ end
378
+
379
+ else
380
+ @crontab.delete!
381
+
382
+ Sunshine.logger.info @shell.host, "No previous deploy to revert to."
383
+ end
384
+ end
385
+
386
+
387
+ ##
388
+ # Runs bundler. Installs the bundler gem if missing.
389
+
390
+ def run_bundler
391
+ install_deps 'bundler', :type => Gem
392
+ @shell.call "cd #{self.checkout_path} && gem bundle"
393
+ end
394
+
395
+
396
+ ##
397
+ # Runs geminstaller. :(
398
+ # Deprecated: use bundler
399
+
400
+ def run_geminstaller
401
+ install_deps 'geminstaller', :type => Gem
402
+ @shell.call "cd #{self.checkout_path} && geminstaller -e"
403
+ end
404
+
405
+
406
+ ##
407
+ # Check if the app pids are present.
408
+ # Post-deploy only.
409
+
410
+ def running?
411
+ @shell.call "#{self.root_path}/status" rescue false
412
+ end
413
+
414
+
415
+ ##
416
+ # Run a sass task on any or all deploy servers.
417
+
418
+ def sass *sass_names
419
+ install_deps 'haml', :type => Gem
420
+
421
+ sass_names.flatten.each do |name|
422
+ sass_file = "public/stylesheets/sass/#{name}.sass"
423
+ css_file = "public/stylesheets/#{name}.css"
424
+ sass_cmd = "cd #{self.checkout_path} && sass #{sass_file} #{css_file}"
425
+
426
+ @shell.call sass_cmd
427
+ end
428
+ end
429
+
430
+
431
+ ##
432
+ # Get the deploy server's shell environment.
433
+
434
+ def shell_env
435
+ @shell.env
436
+ end
437
+
438
+
439
+ ##
440
+ # Run the app's start script. Returns false on failure.
441
+ # Post-deploy only.
442
+
443
+ def start options=nil
444
+ options ||= {}
445
+
446
+ if running?
447
+ return unless options[:force]
448
+ stop
449
+ end
450
+
451
+ @shell.call "#{self.root_path}/start" rescue false
452
+ end
453
+
454
+
455
+ ##
456
+ # Get the app's status: :running or :down.
457
+
458
+ def status
459
+ running? ? :running : :down
460
+ end
461
+
462
+
463
+ ##
464
+ # Run the app's stop script. Returns false on failure.
465
+ # Post-deploy only.
466
+
467
+ def stop
468
+ @shell.call "#{self.root_path}/stop" rescue false
469
+ end
470
+
471
+
472
+ ##
473
+ # Creates a symlink to the app's checkout path.
474
+
475
+ def symlink_current_dir
476
+ @shell.symlink self.checkout_path, self.current_path
477
+ end
478
+
479
+
480
+ ##
481
+ # Upload common rake tasks from a local path or the sunshine lib.
482
+ # app.upload_tasks
483
+ # #=> upload all tasks
484
+ # app.upload_tasks 'app', 'common', ...
485
+ # #=> upload app and common rake files
486
+ #
487
+ # File paths may also be used instead of the file's base name but
488
+ # directory structures will not be followed:
489
+ # app.upload_tasks 'lib/common/app.rake', 'lib/do_thing.rake'
490
+ #
491
+ # Allows options:
492
+ # :local_path:: str - the path to get rake tasks from
493
+ # :remote_path:: str - the remote absolute path to upload the files to
494
+
495
+ def upload_tasks *files
496
+ options = Hash === files[-1] ? files.delete_at(-1) : {}
497
+ remote_path = options[:remote_path] || "#{self.checkout_path}/lib/tasks"
498
+ local_path = options[:local_path] || "#{Sunshine::ROOT}/templates/tasks"
499
+
500
+ @shell.call "mkdir -p #{remote_path}"
501
+
502
+ files.map! do |file|
503
+ if File.basename(file) == file
504
+ File.join(local_path, "#{file}.rake")
505
+ else
506
+ file
507
+ end
508
+ end
509
+
510
+ files = Dir.glob("#{Sunshine::ROOT}/templates/tasks/*") if files.empty?
511
+
512
+ files.each do |file|
513
+ remote_file = File.join remote_path, File.basename(file)
514
+ @shell.upload file, remote_file
515
+ end
516
+ end
517
+
518
+
519
+ ##
520
+ # Write an executable bash script to the app's checkout dir
521
+ # on the deploy server, and symlink them to the current dir.
522
+
523
+ def write_script name, contents
524
+
525
+ @shell.make_file "#{self.checkout_path}/#{name}", contents,
526
+ :flags => '--chmod=ugo=rwx'
527
+
528
+ @shell.symlink "#{self.current_path}/#{name}",
529
+ "#{self.root_path}/#{name}"
530
+ end
531
+
532
+
533
+ private
534
+
535
+
536
+ ##
537
+ # Set all the app paths based on the root app path.
538
+
539
+ def assign_local_app_attr name, options={}
540
+ @name = name
541
+ @deploy_name = options[:deploy_name] || Time.now.to_i
542
+
543
+ default_root = File.join(Sunshine.web_directory, @name)
544
+ @root_path = options[:root_path] || default_root
545
+
546
+ @current_path = "#{@root_path}/current"
547
+ @deploys_path = "#{@root_path}/deploys"
548
+ @shared_path = "#{@root_path}/shared"
549
+ @log_path = "#{@shared_path}/log"
550
+ @checkout_path = "#{@deploys_path}/#{@deploy_name}"
551
+ end
552
+ end
553
+ end
554
+