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