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