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,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
+