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