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,241 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Keeps an SSH connection open to a server the app will be deployed to.
5
+ # Deploy servers use the ssh command and support any ssh feature.
6
+ # By default, deploy servers use the ControlMaster feature to share
7
+ # socket connections, with the ControlPath = ~/.ssh/sunshine-%r%h:%p
8
+ #
9
+ # Setting session-persistant environment variables is supported by
10
+ # accessing the @env attribute.
11
+
12
+ class RemoteShell < Shell
13
+
14
+ class ConnectionError < FatalDeployError; end
15
+
16
+ ##
17
+ # The loop to keep the ssh connection open.
18
+ LOGIN_LOOP = "echo connected; echo ready; for (( ; ; )); do sleep 10; done"
19
+
20
+ LOGIN_TIMEOUT = 30
21
+
22
+
23
+ ##
24
+ # Closes all remote shell connections.
25
+
26
+ def self.disconnect_all
27
+ return unless defined?(@remote_shells)
28
+ @remote_shells.each{|rs| rs.disconnect}
29
+ end
30
+
31
+
32
+ ##
33
+ # Registers a remote shell for global access from the class.
34
+ # Handled automatically on initialization.
35
+
36
+ def self.register remote_shell
37
+ (@remote_shells ||= []) << remote_shell
38
+ end
39
+
40
+
41
+ attr_reader :host, :user
42
+ attr_accessor :ssh_flags, :rsync_flags
43
+
44
+
45
+ ##
46
+ # Remote shells essentially need a host and optional user.
47
+ # Typical instantiation is done through either of these methods:
48
+ # RemoteShell.new "user@host"
49
+ # RemoteShell.new "host", :user => "user"
50
+ #
51
+ # The constructor also supports the following options:
52
+ # :env:: hash - hash of environment variables to set for the ssh session
53
+ # :password:: string - password for ssh login; if missing the deploy server
54
+ # will attempt to prompt the user for a password.
55
+
56
+ def initialize host, options={}
57
+ super $stdout, options
58
+
59
+ @host, @user = host.split("@").reverse
60
+
61
+ @user ||= options[:user]
62
+
63
+ @rsync_flags = ["-azP"]
64
+ @rsync_flags.concat [*options[:rsync_flags]] if options[:rsync_flags]
65
+
66
+ @ssh_flags = [
67
+ "-o ControlMaster=auto",
68
+ "-o ControlPath=~/.ssh/sunshine-%r@%h:%p"
69
+ ]
70
+ @ssh_flags.concat ["-l", @user] if @user
71
+ @ssh_flags.concat [*options[:ssh_flags]] if options[:ssh_flags]
72
+
73
+ @pid, @inn, @out, @err = nil
74
+
75
+ self.class.register self
76
+ end
77
+
78
+
79
+ ##
80
+ # Runs a command via SSH. Optional block is passed the
81
+ # stream(stderr, stdout) and string data
82
+
83
+ def call command_str, options={}, &block
84
+ Sunshine.logger.info @host, "Running: #{command_str}" do
85
+ execute ssh_cmd(command_str, options), &block
86
+ end
87
+ end
88
+
89
+
90
+ ##
91
+ # Connect to host via SSH and return process pid
92
+
93
+ def connect
94
+ return @pid if connected?
95
+
96
+ cmd = ssh_cmd LOGIN_LOOP, :sudo => false
97
+
98
+ @pid, @inn, @out, @err = popen4(cmd.join(" "))
99
+ @inn.sync = true
100
+
101
+ data = ""
102
+ ready = nil
103
+ start_time = Time.now.to_i
104
+
105
+ until ready || @out.eof?
106
+ data << @out.readpartial(1024)
107
+ ready = data =~ /ready/
108
+
109
+ raise TimeoutError if timed_out?(start_time, LOGIN_TIMEOUT)
110
+ end
111
+
112
+ unless connected?
113
+ disconnect
114
+ host_info = [@user, @host].compact.join("@")
115
+ raise ConnectionError, "Can't connect to #{host_info}"
116
+ end
117
+
118
+ @inn.close
119
+ @pid
120
+ end
121
+
122
+
123
+ ##
124
+ # Check if SSH session is open and returns process pid
125
+
126
+ def connected?
127
+ Process.kill(0, @pid) && @pid rescue false
128
+ end
129
+
130
+
131
+ ##
132
+ # Disconnect from host
133
+
134
+ def disconnect
135
+ return unless connected?
136
+
137
+ @inn.close rescue nil
138
+ @out.close rescue nil
139
+ @err.close rescue nil
140
+
141
+ kill_process @pid, "HUP"
142
+
143
+ @pid = nil
144
+ end
145
+
146
+
147
+ ##
148
+ # Download a file via rsync
149
+
150
+ def download from_path, to_path, options={}, &block
151
+ from_path = "#{@host}:#{from_path}"
152
+ Sunshine.logger.info @host, "Downloading #{from_path} -> #{to_path}" do
153
+ execute rsync_cmd(from_path, to_path, options), &block
154
+ end
155
+ end
156
+
157
+
158
+ ##
159
+ # Expand a path:
160
+ # shell.expand_path "~user/thing"
161
+ # #=> "/home/user/thing"
162
+
163
+ def expand_path path
164
+ dir = File.dirname path
165
+ full_dir = call "cd #{dir} && pwd"
166
+ File.join full_dir, File.basename(path)
167
+ end
168
+
169
+
170
+ ##
171
+ # Checks if the given file exists
172
+
173
+ def file? filepath
174
+ call("test -f #{filepath}") && true rescue false
175
+ end
176
+
177
+
178
+ ##
179
+ # Create a file remotely
180
+
181
+ def make_file filepath, content, options={}
182
+
183
+ temp_filepath =
184
+ "#{TMP_DIR}/#{File.basename(filepath)}_#{Time.now.to_i}#{rand(10000)}"
185
+
186
+ File.open(temp_filepath, "w+"){|f| f.write(content)}
187
+
188
+ self.upload temp_filepath, filepath, options
189
+
190
+ File.delete(temp_filepath)
191
+ end
192
+
193
+
194
+ ##
195
+ # Uploads a file via rsync
196
+
197
+ def upload from_path, to_path, options={}, &block
198
+ to_path = "#{@host}:#{to_path}"
199
+ Sunshine.logger.info @host, "Uploading #{from_path} -> #{to_path}" do
200
+ execute rsync_cmd(from_path, to_path, options), &block
201
+ end
202
+ end
203
+
204
+
205
+ private
206
+
207
+ def build_rsync_flags options
208
+ flags = @rsync_flags.dup
209
+
210
+ remote_rsync = 'rsync'
211
+ rsync_sudo = sudo_cmd remote_rsync, options
212
+
213
+ unless rsync_sudo == remote_rsync
214
+ flags << "--rsync-path='#{ rsync_sudo.join(" ") }'"
215
+ end
216
+
217
+ flags << "-e \"ssh #{@ssh_flags.join(' ')}\"" if @ssh_flags
218
+
219
+ flags.concat [*options[:flags]] if options[:flags]
220
+
221
+ flags
222
+ end
223
+
224
+
225
+ def rsync_cmd from_path, to_path, options={}
226
+ cmd = ["rsync", build_rsync_flags(options), from_path, to_path]
227
+ cmd.flatten.compact.join(" ")
228
+ end
229
+
230
+
231
+ def ssh_cmd string, options={}
232
+ cmd = sh_cmd string
233
+ cmd = env_cmd cmd
234
+ cmd = sudo_cmd cmd, options
235
+
236
+ flags = [*options[:flags]].concat @ssh_flags
237
+
238
+ ["ssh", flags, @host, cmd].flatten.compact
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,128 @@
1
+ module Sunshine
2
+
3
+ class RepoError < Exception; end
4
+
5
+ ##
6
+ # An abstract class to wrap simple basic scm features. The primary function
7
+ # of repo objects is to get information about the scm branch that is being
8
+ # deployed and to check it out on remote deploy servers:
9
+ # svn = SvnRepo.new "svn://path/to/repo", :flags => "--ignore-externals"
10
+ #
11
+ # The :flags option can be a String or an Array and supports any scm
12
+ # checkout (or clone for git) options.
13
+
14
+ class Repo
15
+
16
+ ##
17
+ # Creates a new repo subclass object:
18
+ # Repo.new_of_type :svn, "https://path/to/repo/tags/releasetag"
19
+ # Repo.new_of_type :git, "user@gitbox.com:repo/path"
20
+
21
+ def self.new_of_type repo_type, url, options={}
22
+ repo = "#{repo_type.to_s.capitalize}Repo"
23
+ Sunshine.const_get(repo).new(url, options)
24
+ end
25
+
26
+
27
+ ##
28
+ # Looks for .git and .svn directories and determines if the passed path
29
+ # is a recognized repo. Does not check for RsyncRepo since it's a
30
+ # special case. Returns the appropriate repo object:
31
+ # Repo.detect "path/to/svn/repo/dir"
32
+ # #=> <SvnRepo @url="svn://url/of/checked/out/repo">
33
+ # Repo.detect "path/to/git/repo/dir"
34
+ # #=> <GitRepo, @url="git://url/of/git/repo", @branch="master">
35
+ # Repo.detect "invalid/repo/path"
36
+ # #=> nil
37
+
38
+ def self.detect path=".", shell=nil
39
+
40
+ if SvnRepo.valid? path, shell
41
+ info = SvnRepo.get_info path, shell
42
+ SvnRepo.new info[:url], info
43
+
44
+ elsif GitRepo.valid? path, shell
45
+ info = GitRepo.get_info path, shell
46
+ GitRepo.new info[:url], info
47
+ end
48
+ end
49
+
50
+
51
+ ##
52
+ # Gets repo information for the specified dir - Implemented by subclass
53
+
54
+ def self.get_info path=".", shell=nil
55
+ raise RepoError,
56
+ "The 'get_info' method must be implemented by child classes"
57
+ end
58
+
59
+
60
+ attr_reader :url
61
+
62
+ def initialize url, options={}
63
+ @scm = self.class.name.split("::").last.sub('Repo', '').downcase
64
+
65
+ @url = url
66
+ @flags = [*options[:flags]].compact
67
+ end
68
+
69
+
70
+ ##
71
+ # Checkout code to a shell and return an info log hash:
72
+ # repo.chekout_to server, "some/path"
73
+ # #=> {:revision => 123, :committer => 'someone', :date => time_obj ...}
74
+
75
+ def checkout_to path, shell=nil
76
+ shell ||= Sunshine.shell
77
+
78
+ Sunshine.logger.info @scm,
79
+ "Checking out to #{shell.host} #{path}" do
80
+
81
+ Sunshine.dependencies.install @scm, :call => shell if
82
+ Sunshine.dependencies.exist? @scm
83
+
84
+ shell.call "test -d #{path} && rm -rf #{path} || echo false"
85
+ shell.call "mkdir -p #{path}"
86
+
87
+ do_checkout path, shell
88
+ get_repo_info path, shell
89
+ end
90
+ end
91
+
92
+
93
+ ##
94
+ # Checkout the repo - implemented by subclass
95
+
96
+ def do_checkout path, shell
97
+ raise RepoError,
98
+ "The 'do_checkout' method must be implemented by child classes"
99
+ end
100
+
101
+
102
+ ##
103
+ # Get the name of the specified repo - implemented by subclass
104
+
105
+ def name
106
+ raise RepoError,
107
+ "The 'name' method must be implemented by child classes"
108
+ end
109
+
110
+
111
+ ##
112
+ # Returns the set scm flags as a string
113
+
114
+ def scm_flags
115
+ @flags.join(" ")
116
+ end
117
+
118
+
119
+ ##
120
+ # Returns the repo information as a hash.
121
+
122
+ def get_repo_info path=".", shell=nil
123
+ defaults = {:type => @scm, :url => @url, :path => path}
124
+
125
+ defaults.merge self.class.get_info(path, shell)
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,122 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Simple wrapper for git repos. Constructor supports :tree option to
5
+ # specify either a branch or tree-ish to checkout:
6
+ # git = GitRepo.new "git://mygitrepo.git", :tree => "tags/release001"
7
+
8
+ class GitRepo < Repo
9
+
10
+ LOG_FORMAT = [
11
+ ":revision: %H",
12
+ ":committer: %cn",
13
+ ":date: %cd",
14
+ ":message: %s",
15
+ ":refs: '%d'",
16
+ ":tree: %t"
17
+ ].join("%n")
18
+
19
+
20
+ ##
21
+ # Check if this is an svn repo
22
+
23
+ def self.valid? path="."
24
+ File.exist? File.join(path, ".git")
25
+ end
26
+
27
+
28
+ ##
29
+ # Get the repo info from the path to a checked out git repo
30
+
31
+ def self.get_info path=".", shell=nil
32
+ shell ||= Sunshine.shell
33
+
34
+ info = YAML.load git_log(path, shell)
35
+
36
+ info[:date] = Time.parse info[:date]
37
+ info[:branch] = parse_branch info
38
+ info[:url] = git_origin path, shell
39
+
40
+ info
41
+ rescue => e
42
+ raise RepoError, e
43
+ end
44
+
45
+
46
+ ##
47
+ # Returns the git logs for a path, formatted as yaml.
48
+
49
+ def self.git_log path, shell
50
+ git_options = "-1 --no-color --format=\"#{LOG_FORMAT}\""
51
+ shell.call "cd #{path} && git log #{git_options}"
52
+ end
53
+
54
+
55
+ ##
56
+ # Returns the fetch origin of the current git repo. Returns the path to a
57
+ # public git repo by default:
58
+ # GitRepo.git_origin "/some/path", Sunshine.shell
59
+ # #=> "git://myrepo/path/to/repo.git"
60
+ # GitRepo.git_origin "/some/path", Sunshine.shell, false
61
+ # #=> "user@myrepo:path/to/repo.git"
62
+
63
+ def self.git_origin path, shell, public_url=true
64
+ get_origin_cmd = "cd #{path} && git remote -v | grep \\(fetch\\)"
65
+
66
+ origin = shell.call get_origin_cmd
67
+ origin = origin.split(/\t|\s/)[1]
68
+
69
+ origin = make_public_url origin if public_url
70
+
71
+ origin
72
+ end
73
+
74
+
75
+ ##
76
+ # Returns the git url for a public checkout
77
+
78
+ def self.make_public_url git_url
79
+ url, protocol = git_url.split("://").reverse
80
+ url, user = url.split("@").reverse
81
+
82
+ url.gsub!(":", "/") if !protocol
83
+
84
+ "git://#{url}"
85
+ end
86
+
87
+
88
+ attr_accessor :tree
89
+
90
+ def initialize url, options={}
91
+ super
92
+ @tree = options[:branch] || options[:tree] || "master"
93
+ end
94
+
95
+
96
+ def do_checkout path, shell
97
+ cmd = "cd #{path} && git clone #{@url} #{scm_flags} . && "+
98
+ "git checkout #{@tree}"
99
+ shell.call cmd
100
+ end
101
+
102
+
103
+ NAME_MATCH = /\/([^\/]+)\.git/
104
+
105
+ def name
106
+ @url.match(NAME_MATCH)[1]
107
+ rescue
108
+ raise RepoError, "Git url must match #{NAME_MATCH.inspect}"
109
+ end
110
+
111
+
112
+ private
113
+
114
+ def self.parse_branch response
115
+ refs = response[:refs]
116
+ return response[:tree] unless refs && !refs.strip.empty?
117
+
118
+ ref_names = refs.delete('()').gsub('/', '_')
119
+ ref_names.split(',').last.strip
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,29 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Allows uploading code directly using rsync, instead of a scm.
5
+
6
+ class RsyncRepo < Repo
7
+
8
+ def self.get_info path=".", shell=nil
9
+ {}
10
+ end
11
+
12
+
13
+ def initialize url, options={}
14
+ super
15
+ @flags << "-r"
16
+ end
17
+
18
+
19
+ def do_checkout path, shell
20
+ shell.upload @url, path, :flags => @flags
21
+ end
22
+
23
+
24
+ def name
25
+ File.basename @url
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,78 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Simple scm wrapper for subversion control.
5
+
6
+ class SvnRepo < Repo
7
+
8
+ ##
9
+ # Check if this is an svn repo
10
+
11
+ def self.valid? path="."
12
+ git_svn?(path) || File.exist?(File.join(path, ".svn"))
13
+ end
14
+
15
+
16
+ ##
17
+ # Get the repo info from the path to a checked out svn repo
18
+
19
+ def self.get_info path=".", shell=nil
20
+ shell ||= Sunshine.shell
21
+
22
+ svn_url = get_svn_url path, shell
23
+ response = svn_log svn_url, shell
24
+
25
+ info = {}
26
+
27
+ info[:url] = svn_url
28
+ info[:revision] = response.match(/revision="(.*)">/)[1]
29
+ info[:committer] = response.match(/<author>(.*)<\/author>/)[1]
30
+ info[:date] = Time.parse response.match(/<date>(.*)<\/date>/)[1]
31
+ info[:message] = response.match(/<msg>(.*)<\/msg>/m)[1]
32
+ info[:branch] = svn_url.split("/").last
33
+
34
+ info
35
+ rescue => e
36
+ raise RepoError, e
37
+ end
38
+
39
+
40
+ ##
41
+ # Returns the svn logs as xml.
42
+
43
+ def self.svn_log path, shell
44
+ shell.call "svn log #{path} --limit 1 --xml"
45
+ end
46
+
47
+
48
+ ##
49
+ # Check if this is a git-svn repo.
50
+
51
+ def self.git_svn? path="."
52
+ File.exist? File.join(path, ".git/svn")
53
+ end
54
+
55
+
56
+ ##
57
+ # Get the svn url from a svn or git-svn checkout.
58
+
59
+ def self.get_svn_url path, shell
60
+ cmd = git_svn?(path) ? "git svn" : "svn"
61
+ shell.call("cd #{path} && #{cmd} info | grep ^URL:").split(" ")[1]
62
+ end
63
+
64
+
65
+ def do_checkout path, shell
66
+ shell.call "svn checkout #{scm_flags} #{@url} #{path}"
67
+ end
68
+
69
+
70
+ NAME_MATCH = /([^\/]+\/)+([^\/]+)\/(trunk|branches|tags)/
71
+
72
+ def name
73
+ @url.match(NAME_MATCH)[2]
74
+ rescue
75
+ raise RepoError, "Svn url must match #{NAME_MATCH.inspect}"
76
+ end
77
+ end
78
+ end