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,179 @@
1
+ require 'cgi'
2
+
3
+ module MockObject
4
+
5
+ ##
6
+ # Setup a method mock
7
+
8
+ def mock method, options={}, &block
9
+ mock_key = mock_key_for method, options
10
+ method_mocks[mock_key] = block_given? ? block : options[:return]
11
+ end
12
+
13
+
14
+ ##
15
+ # Get the value a mocked method was setup to return
16
+
17
+ def method_mock_return mock_key
18
+ return_val = method_mocks[mock_key] rescue method_mocks[[mock_key.first]]
19
+ if Proc === return_val
20
+ args = mock_key[1..-1]
21
+ return_val.call(*args)
22
+ else
23
+ return_val
24
+ end
25
+ end
26
+
27
+
28
+ ##
29
+ # Create a mock key based on :method, :args => [args_passed_to_method]
30
+
31
+ def mock_key_for method, options={}
32
+ mock_key = [method.to_s]
33
+ mock_key.concat [*options[:args]] if options.has_key?(:args)
34
+ mock_key
35
+ end
36
+
37
+
38
+ ##
39
+ # Check if a method was called. Supports options:
40
+ # :exactly:: num - exact number of times the method should have been called
41
+ # :count:: num - minimum number of times the method should have been called
42
+ # Defaults to :count => 1
43
+
44
+ def method_called? method, options={}
45
+ target_count = options[:count] || options[:exactly] || 1
46
+
47
+ count = method_call_count method, options
48
+
49
+ options[:exactly] ? count == target_count : count >= target_count
50
+ end
51
+
52
+
53
+ ##
54
+ # Count the number of times a method was called:
55
+ # obj.method_call_count :my_method, :args => [1,2,3]
56
+
57
+ def method_call_count method, options={}
58
+ count = 0
59
+
60
+ mock_def_arr = mock_key_for method, options
61
+
62
+ each_mock_key_matching(mock_def_arr) do |mock_key|
63
+ count = count + method_log[mock_key]
64
+ end
65
+
66
+ count
67
+ end
68
+
69
+
70
+ ##
71
+ # Do something with every instance of a mock key.
72
+ # Used to retrieve all lowest common denominators of method calls:
73
+ #
74
+ # each_mock_key_matching [:my_method] do |mock_key|
75
+ # puts mock_key.inspect
76
+ # end
77
+ #
78
+ # # Outputs #
79
+ # [:my_method, 1, 2, 3]
80
+ # [:my_method, 1, 2]
81
+ # [:my_method, 1]
82
+ # [:my_method]
83
+
84
+ def each_mock_key_matching mock_key
85
+ index = mock_key.length - 1
86
+
87
+ method_log.keys.each do |key|
88
+ yield(key) if block_given? && key[0..index] == mock_key
89
+ end
90
+ end
91
+
92
+
93
+ def method_mocks
94
+ @method_mocks ||= Hash.new do |h, k|
95
+ raise "Mock for #{k.inspect} does not exist."
96
+ end
97
+ end
98
+
99
+
100
+ def method_log
101
+ @method_log ||= Hash.new(0)
102
+ end
103
+
104
+
105
+ ##
106
+ # Hook into the object
107
+
108
+ def self.included base
109
+ hook_instance_methods base
110
+ end
111
+
112
+
113
+ def self.extended base
114
+ hook_instance_methods base, true
115
+ end
116
+
117
+
118
+ def self.hook_instance_methods base, instance=false
119
+ unhook_instance_methods base, instance
120
+
121
+ eval_each_method_of(base, instance) do |m|
122
+ m_def = m =~ /[^\]]=$/ ? "args" : "*args, &block"
123
+ new_m = escape_unholy_method_name "hooked_#{m}"
124
+ %{
125
+ alias #{new_m} #{m}
126
+ undef #{m}
127
+
128
+ def #{m}(#{m_def})
129
+ mock_key = mock_key_for '#{m}', :args => args
130
+
131
+ count = method_log[mock_key]
132
+ method_log[mock_key] = count.next
133
+
134
+ method_mock_return(mock_key) rescue self.send(:#{new_m}, #{m_def})
135
+ end
136
+ }
137
+ end
138
+ end
139
+
140
+
141
+ def self.unhook_instance_methods base, instance=false
142
+ eval_each_method_of(base, instance) do |m|
143
+ new_m = escape_unholy_method_name "hooked_#{m}"
144
+ #puts m + " -> " + new_m
145
+ %{
146
+ m = '#{new_m}'.to_sym
147
+ defined = method_defined?(m) rescue self.class.method_defined?(m)
148
+
149
+ if defined
150
+ undef #{m}
151
+ alias #{m} #{new_m}
152
+ end
153
+ }
154
+ end
155
+ end
156
+
157
+
158
+ def self.escape_unholy_method_name name
159
+ CGI.escape(name).gsub('%','').gsub('-','MNS')
160
+ end
161
+
162
+
163
+ def self.eval_each_method_of base, instance=false, &block
164
+ eval_method, affect_methods = if instance
165
+ [:instance_eval, base.methods]
166
+ else
167
+ [:class_eval, base.instance_methods]
168
+ end
169
+
170
+ banned_methods = self.instance_methods
171
+ banned_methods.concat Object.instance_methods
172
+
173
+ affect_methods.sort.each do |m|
174
+ next if banned_methods.include?(m)
175
+ #puts m
176
+ base.send eval_method, block.call(m)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,117 @@
1
+ module MockOpen4
2
+
3
+ LOGIN_CMD = "echo ready;"
4
+
5
+ CMD_RETURN = {
6
+ LOGIN_CMD => [:out, "ready\n"]
7
+ }
8
+
9
+ attr_reader :cmd_log
10
+
11
+ def popen4(*args)
12
+ cmd = args.join(" ")
13
+ @cmd_log ||= []
14
+ @cmd_log << cmd
15
+
16
+
17
+ pid = "test_pid"
18
+ inn_w = StringIO.new
19
+ out_r, out_w = IO.pipe
20
+ err_r, err_w = IO.pipe
21
+
22
+ ios = {:inn => inn_w, :out => out_w, :err => err_w}
23
+ stream, string = output_for cmd
24
+
25
+ ios[stream].write string
26
+ out_w.write nil
27
+ err_w.write nil
28
+
29
+ out_w.close
30
+ err_w.close
31
+
32
+ if block_given?
33
+ yield
34
+ inn_w.close
35
+ out_r.close
36
+ err_r.close
37
+ end
38
+
39
+ return pid, inn_w, out_r, err_r
40
+ end
41
+
42
+ def output_for(cmd)
43
+ @mock_output ||= {}
44
+ if @mock_output
45
+ output = @mock_output[cmd]
46
+ output ||= @mock_output[nil].shift if Array === @mock_output[nil]
47
+ @mock_output.delete(cmd)
48
+ if output
49
+ Process.set_exitcode output.delete(output.last)
50
+ return output
51
+ end
52
+ end
53
+
54
+ CMD_RETURN.each do |cmd_key, return_val|
55
+ return return_val if cmd.include? cmd_key
56
+ end
57
+ return :out, "some_value"
58
+ end
59
+
60
+ def set_mock_response code, stream_vals={}, options={}
61
+ @mock_output ||= {}
62
+ @mock_output[nil] ||= []
63
+ new_stream_vals = {}
64
+
65
+ stream_vals.each do |key, val|
66
+ if Symbol === key
67
+ @mock_output[nil] << [key, val, code]
68
+ next
69
+ end
70
+
71
+ key = ssh_cmd(key, options).join(" ") if Sunshine::RemoteShell === self
72
+
73
+ new_stream_vals[key] = (val.dup << code)
74
+ end
75
+ @mock_output.merge! new_stream_vals
76
+ end
77
+
78
+ end
79
+
80
+
81
+ class StatusStruct < Struct.new("Status", :exitstatus)
82
+ def success?
83
+ self.exitstatus == 0
84
+ end
85
+ end
86
+
87
+
88
+ Process.class_eval do
89
+ class << self
90
+
91
+ def set_exitcode(code)
92
+ @exit_code = code
93
+ end
94
+
95
+ alias old_waitpid2 waitpid2
96
+ undef waitpid2
97
+
98
+ def waitpid2(*args)
99
+ pid = args[0]
100
+ if pid == "test_pid"
101
+ exitcode = @exit_code ||= 0
102
+ @exit_code = 0
103
+ return [StatusStruct.new(exitcode)]
104
+ else
105
+ return old_waitpid2(*args)
106
+ end
107
+ end
108
+
109
+ alias old_kill kill
110
+ undef kill
111
+
112
+ def kill(type, pid)
113
+ return true if type == 0 && pid == "test_pid"
114
+ old_kill(type, pid)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,188 @@
1
+ require 'sunshine'
2
+ require 'test/unit'
3
+
4
+ def no_mocks
5
+ ENV['mocks'] == "false"
6
+ end
7
+
8
+ unless no_mocks
9
+ require 'test/mocks/mock_object'
10
+ require 'test/mocks/mock_open4'
11
+ end
12
+
13
+ unless defined? TEST_APP_CONFIG_FILE
14
+ TEST_APP_CONFIG_FILE = "test/fixtures/app_configs/test_app.yml"
15
+ end
16
+
17
+
18
+ def mock_app
19
+ Sunshine::App.new(TEST_APP_CONFIG_FILE).extend MockObject
20
+ end
21
+
22
+
23
+ def mock_remote_shell host=nil
24
+ host ||= "user@some_server.com"
25
+ remote_shell = Sunshine::RemoteShell.new host
26
+
27
+ remote_shell.extend MockOpen4
28
+ remote_shell.extend MockObject
29
+
30
+ use_remote_shell remote_shell
31
+
32
+ remote_shell.connect
33
+ remote_shell
34
+ end
35
+
36
+
37
+ def mock_svn_response url=nil
38
+ url ||= "svn://subversion/path/to/my_app/trunk"
39
+
40
+ svn_response = <<-STR
41
+ <?xml version="1.0"?>
42
+ <log>
43
+ <logentry
44
+ revision="777">
45
+ <author>user</author>
46
+ <date>2010-01-26T01:49:17.372152Z</date>
47
+ <msg>finished testing server.rb</msg>
48
+ </logentry>
49
+ </log>
50
+ STR
51
+
52
+ Sunshine::SvnRepo.extend(MockObject) unless
53
+ Sunshine::SvnRepo.is_a?(MockObject)
54
+
55
+ Sunshine::SvnRepo.mock :svn_log, :return => svn_response
56
+ Sunshine::SvnRepo.mock :get_svn_url, :return => url
57
+ end
58
+
59
+
60
+ def mock_remote_shell_popen4
61
+ return if no_mocks
62
+ Sunshine::RemoteShell.class_eval{ include MockOpen4 }
63
+ end
64
+
65
+
66
+ def set_mock_response_for obj, code, stream_vals={}, options={}
67
+ case obj
68
+ when Sunshine::App then
69
+ obj.each do |sa|
70
+ sa.shell.set_mock_response code, stream_vals, options
71
+ end
72
+ when Sunshine::ServerApp then
73
+ obj.shell.set_mock_response code, stream_vals, options
74
+ when Sunshine::RemoteShell then
75
+ obj.set_mock_response code, stream_vals, options
76
+ end
77
+ end
78
+
79
+
80
+ def assert_dep_install dep_name, type=Sunshine::Yum
81
+ prefered = type rescue nil
82
+ args = [{:call => @remote_shell, :prefer => prefered}]
83
+
84
+ dep = if Sunshine::Dependency === dep_name
85
+ dep_name
86
+ else
87
+ Sunshine.dependencies.get(dep_name, :prefer => prefered)
88
+ end
89
+
90
+
91
+ assert dep.method_called?(:install!, :args => args),
92
+ "Dependency '#{dep_name}' install was not called."
93
+ end
94
+
95
+
96
+ def assert_not_called *args
97
+ assert !@remote_shell.method_called?(:call, :args => [*args]),
98
+ "Command called by #{@remote_shell.host} but should't have:\n #{args[0]}"
99
+ end
100
+
101
+
102
+ def assert_server_call *args
103
+ assert @remote_shell.method_called?(:call, :args => [*args]),
104
+ "Command was not called by #{@remote_shell.host}:\n #{args[0]}"
105
+ end
106
+
107
+
108
+ def assert_bash_script name, cmds, check_value
109
+ cmds = cmds.map{|cmd| "(#{cmd})" }
110
+ cmds << "echo true"
111
+
112
+ bash = <<-STR
113
+ #!/bin/bash
114
+ if [ "$1" == "--no-env" ]; then
115
+ #{cmds.flatten.join(" && ")}
116
+ else
117
+ #{@app.root_path}/env #{@app.root_path}/#{name} --no-env
118
+ fi
119
+ STR
120
+
121
+ assert_equal bash, check_value
122
+ end
123
+
124
+
125
+ def assert_ssh_call expected, ds=@remote_shell, options={}
126
+ expected = ds.send(:ssh_cmd, expected, options).join(" ")
127
+
128
+ error_msg = "No such command in remote_shell log [#{ds.host}]\n#{expected}"
129
+ error_msg << "\n\n#{ds.cmd_log.select{|c| c =~ /^ssh/}.join("\n\n")}"
130
+
131
+ assert ds.cmd_log.include?(expected), error_msg
132
+ end
133
+
134
+
135
+ def assert_rsync from, to, ds=@remote_shell, sudo=false
136
+ received = ds.cmd_log.last
137
+
138
+ rsync_path = if sudo
139
+ path = ds.sudo_cmd('rsync', sudo).join(' ')
140
+ "--rsync-path='#{ path }' "
141
+ end
142
+
143
+ rsync_cmd = "rsync -azP #{rsync_path}-e \"ssh #{ds.ssh_flags.join(' ')}\""
144
+
145
+ error_msg = "No such command in remote_shell log [#{ds.host}]\n#{rsync_cmd}"
146
+ error_msg << "#{from.inspect} #{to.inspect}"
147
+ error_msg << "\n\n#{ds.cmd_log.select{|c| c =~ /^rsync/}.join("\n\n")}"
148
+
149
+ if Regexp === from
150
+ found = ds.cmd_log.select do |cmd|
151
+
152
+ cmd_from = cmd.split(" ")[-2]
153
+ cmd_to = cmd.split(" ").last
154
+
155
+ cmd_from =~ from && cmd_to == to && cmd.index(rsync_cmd) == 0
156
+ end
157
+
158
+ assert !found.empty?, error_msg
159
+ else
160
+ expected = "#{rsync_cmd} #{from} #{to}"
161
+ assert ds.cmd_log.include?(expected), error_msg
162
+ end
163
+ end
164
+
165
+
166
+ def use_remote_shell remote_shell
167
+ @remote_shell = remote_shell
168
+ end
169
+
170
+
171
+ def each_remote_shell app=@app
172
+ app.server_apps.each do |sa|
173
+ use_remote_shell sa.shell
174
+ yield(sa.shell) if block_given?
175
+ end
176
+ end
177
+
178
+ Sunshine.setup({}, true)
179
+
180
+ unless MockObject === Sunshine.shell
181
+ Sunshine.shell.extend MockObject
182
+ Sunshine.shell.mock :<<, :return => nil
183
+ Sunshine.shell.mock :write, :return => nil
184
+ end
185
+
186
+ unless Sunshine::Dependency.include? MockObject
187
+ Sunshine::Dependency.send(:include, MockObject)
188
+ end