sunshine 1.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +237 -0
- data/Manifest.txt +70 -0
- data/README.txt +277 -0
- data/Rakefile +46 -0
- data/bin/sunshine +5 -0
- data/examples/deploy.rb +61 -0
- data/examples/deploy_tasks.rake +112 -0
- data/examples/standalone_deploy.rb +31 -0
- data/lib/commands/add.rb +96 -0
- data/lib/commands/default.rb +169 -0
- data/lib/commands/list.rb +322 -0
- data/lib/commands/restart.rb +62 -0
- data/lib/commands/rm.rb +83 -0
- data/lib/commands/run.rb +151 -0
- data/lib/commands/start.rb +72 -0
- data/lib/commands/stop.rb +61 -0
- data/lib/sunshine/app.rb +876 -0
- data/lib/sunshine/binder.rb +70 -0
- data/lib/sunshine/crontab.rb +143 -0
- data/lib/sunshine/daemon.rb +380 -0
- data/lib/sunshine/daemons/ar_sendmail.rb +28 -0
- data/lib/sunshine/daemons/delayed_job.rb +30 -0
- data/lib/sunshine/daemons/nginx.rb +104 -0
- data/lib/sunshine/daemons/rainbows.rb +35 -0
- data/lib/sunshine/daemons/server.rb +66 -0
- data/lib/sunshine/daemons/unicorn.rb +26 -0
- data/lib/sunshine/dependencies.rb +103 -0
- data/lib/sunshine/dependency_lib.rb +200 -0
- data/lib/sunshine/exceptions.rb +54 -0
- data/lib/sunshine/healthcheck.rb +83 -0
- data/lib/sunshine/output.rb +131 -0
- data/lib/sunshine/package_managers/apt.rb +48 -0
- data/lib/sunshine/package_managers/dependency.rb +349 -0
- data/lib/sunshine/package_managers/gem.rb +54 -0
- data/lib/sunshine/package_managers/yum.rb +62 -0
- data/lib/sunshine/remote_shell.rb +241 -0
- data/lib/sunshine/repo.rb +128 -0
- data/lib/sunshine/repos/git_repo.rb +122 -0
- data/lib/sunshine/repos/rsync_repo.rb +29 -0
- data/lib/sunshine/repos/svn_repo.rb +78 -0
- data/lib/sunshine/server_app.rb +554 -0
- data/lib/sunshine/shell.rb +384 -0
- data/lib/sunshine.rb +391 -0
- data/templates/logrotate/logrotate.conf.erb +11 -0
- data/templates/nginx/nginx.conf.erb +109 -0
- data/templates/nginx/nginx_optimize.conf +23 -0
- data/templates/nginx/nginx_proxy.conf +13 -0
- data/templates/rainbows/rainbows.conf.erb +18 -0
- data/templates/tasks/sunshine.rake +114 -0
- data/templates/unicorn/unicorn.conf.erb +6 -0
- data/test/fixtures/app_configs/test_app.yml +11 -0
- data/test/fixtures/sunshine_test/test_upload +0 -0
- data/test/mocks/mock_object.rb +179 -0
- data/test/mocks/mock_open4.rb +117 -0
- data/test/test_helper.rb +188 -0
- data/test/unit/test_app.rb +489 -0
- data/test/unit/test_binder.rb +20 -0
- data/test/unit/test_crontab.rb +128 -0
- data/test/unit/test_git_repo.rb +26 -0
- data/test/unit/test_healthcheck.rb +70 -0
- data/test/unit/test_nginx.rb +107 -0
- data/test/unit/test_rainbows.rb +26 -0
- data/test/unit/test_remote_shell.rb +102 -0
- data/test/unit/test_repo.rb +42 -0
- data/test/unit/test_server.rb +324 -0
- data/test/unit/test_server_app.rb +425 -0
- data/test/unit/test_shell.rb +97 -0
- data/test/unit/test_sunshine.rb +157 -0
- data/test/unit/test_svn_repo.rb +55 -0
- data/test/unit/test_unicorn.rb +22 -0
- 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
|
data/test/test_helper.rb
ADDED
@@ -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
|