sunshine 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,15 @@
1
+ === 1.1.1 / 2010-04-05
2
+
3
+ * Improvements:
4
+
5
+ * Added Repo subclass registration for greater expandability.
6
+
7
+ * Added support for checking out codebases locally then rsync-ing them.
8
+
9
+ * Bugfixes:
10
+
11
+ * Fixed RemoteShell login script lingering after disconnect.
12
+
1
13
  === 1.1.0 / 2010-04-02
2
14
 
3
15
  * Improvements:
data/README.txt CHANGED
@@ -492,6 +492,9 @@ defaults to :development.
492
492
  'max_deploy_versions' -> The maximum number of deploys to keep on a server;
493
493
  defaults to 5.
494
494
 
495
+ 'remote_checkouts' -> Use remote servers to checkout the codebase;
496
+ defaults to false.
497
+
495
498
  'require' -> Require external ruby libs or gems; defaults to nil.
496
499
 
497
500
  'trace' -> Show detailed output messages; defaults to false.
@@ -19,7 +19,7 @@ module Sunshine
19
19
 
20
20
  ##
21
21
  # Sunshine version.
22
- VERSION = '1.1.0'
22
+ VERSION = '1.1.1'
23
23
 
24
24
  ##
25
25
  # Path to the list of installed sunshine apps.
@@ -41,7 +41,8 @@ module Sunshine
41
41
  'auto' => false,
42
42
  'max_deploy_versions' => 5,
43
43
  'web_directory' => '/var/www',
44
- 'auto_dependencies' => true
44
+ 'auto_dependencies' => true,
45
+ 'remote_checkouts' => false
45
46
  }
46
47
 
47
48
  ##
@@ -88,11 +89,20 @@ module Sunshine
88
89
  @config['auto_dependencies']
89
90
  end
90
91
 
91
- ##
92
- # Returns the main Sunshine dependencies library.
93
92
 
94
- def self.dependencies
93
+ ##
94
+ # Returns the main Sunshine dependencies library. If passed a block,
95
+ # evaluates the block within the dependency lib instance:
96
+ #
97
+ # Sunshine.dependencies do
98
+ # yum 'new_dep'
99
+ # gem 'commander'
100
+ # end
101
+
102
+ def self.dependencies(&block)
95
103
  @dependency_lib ||= DependencyLib.new
104
+ @dependency_lib.instance_eval(&block) if block_given?
105
+ @dependency_lib
96
106
  end
97
107
 
98
108
 
@@ -140,6 +150,15 @@ module Sunshine
140
150
  end
141
151
 
142
152
 
153
+ ##
154
+ # Check if the codebase should be checked out remotely, or checked out
155
+ # locally and rsynced up. Overridden in the ~/.sunshine config file.
156
+
157
+ def self.remote_checkouts?
158
+ @config['remote_checkouts']
159
+ end
160
+
161
+
143
162
  ##
144
163
  # Check if trace log should be output at all.
145
164
  # This value can be assigned by default in ~/.sunshine
@@ -151,6 +151,7 @@ module Sunshine
151
151
  attr_reader :name, :repo, :server_apps, :sudo
152
152
  attr_reader :root_path, :checkout_path, :current_path, :deploys_path
153
153
  attr_reader :shared_path, :log_path, :deploy_name, :deploy_env
154
+ attr_accessor :remote_checkout
154
155
 
155
156
  ##
156
157
  # App instantiation can be done in several ways:
@@ -183,6 +184,8 @@ module Sunshine
183
184
 
184
185
  @server_apps = server_apps_from_config options[:remote_shells]
185
186
 
187
+ @remote_checkout = options[:remote_checkout] || Sunshine.remote_checkouts?
188
+
186
189
  self.sudo = options[:sudo] || Sunshine.sudo
187
190
 
188
191
  @shell_env = {
@@ -401,11 +404,27 @@ module Sunshine
401
404
 
402
405
  ##
403
406
  # Checks out the app's codebase to one or all deploy servers.
407
+ # Supports all App#find options, plus:
408
+ # :copy:: Bool - Checkout locally and rsync; defaults to false.
404
409
 
405
410
  def checkout_codebase options=nil
406
- with_server_apps options,
407
- :msg => "Checking out codebase",
408
- :send => [:checkout_repo, @repo]
411
+ copy_option = options && options.has_key?(:copy) && options[:copy]
412
+
413
+ if @remote_checkout && !copy_option
414
+ with_server_apps options,
415
+ :msg => "Checking out codebase (remotely)",
416
+ :send => [:checkout_repo, @repo]
417
+
418
+ else
419
+ Sunshine.logger.info :app, "Checking out codebase (locally)" do
420
+
421
+ tmp_path = File.join Sunshine::TMP_DIR, "#{@name}_checkout"
422
+ scm_info = @repo.checkout_to tmp_path
423
+
424
+ with_server_apps options,
425
+ :send => [:upload_codebase, tmp_path, scm_info]
426
+ end
427
+ end
409
428
 
410
429
  rescue => e
411
430
  raise CriticalDeployError, e
@@ -691,7 +710,9 @@ module Sunshine
691
710
  # See #after_user_script.
692
711
 
693
712
  def run_post_user_lambdas
694
- @post_user_lambdas.each{|l| l.call self}
713
+ Sunshine.logger.info :app, "Running post deploy lambdas" do
714
+ @post_user_lambdas.each{|l| l.call self}
715
+ end
695
716
  end
696
717
 
697
718
 
@@ -1,8 +1,7 @@
1
1
  ##
2
2
  # Defines Sunshine deploy server dependencies.
3
3
 
4
- #class Sunshine::Dependencies < Settler
5
- Sunshine.dependencies.instance_eval do
4
+ Sunshine.dependencies do
6
5
 
7
6
  apt 'svn', :pkg => 'subversion'
8
7
  yum 'svn', :pkg => 'subversion'
@@ -15,7 +15,8 @@ module Sunshine
15
15
 
16
16
  ##
17
17
  # The loop to keep the ssh connection open.
18
- LOGIN_LOOP = "echo connected; echo ready; for (( ; ; )); do sleep 10; done"
18
+ LOGIN_LOOP = "echo ok; echo ready; "+
19
+ "for (( ; ; )); do kill -0 $PPID && sleep 10 || exit; done;"
19
20
 
20
21
  LOGIN_TIMEOUT = 30
21
22
 
@@ -38,7 +39,7 @@ module Sunshine
38
39
  end
39
40
 
40
41
 
41
- attr_reader :host, :user
42
+ attr_reader :host, :user, :pid
42
43
  attr_accessor :ssh_flags, :rsync_flags
43
44
 
44
45
 
@@ -82,7 +83,7 @@ module Sunshine
82
83
 
83
84
  def call command_str, options={}, &block
84
85
  Sunshine.logger.info @host, "Running: #{command_str}" do
85
- execute ssh_cmd(command_str, options), &block
86
+ execute build_remote_cmd(command_str, options), &block
86
87
  end
87
88
  end
88
89
 
@@ -91,11 +92,11 @@ module Sunshine
91
92
  # Connect to host via SSH and return process pid
92
93
 
93
94
  def connect
94
- return @pid if connected?
95
+ return true if connected?
95
96
 
96
- cmd = ssh_cmd LOGIN_LOOP, :sudo => false
97
+ cmd = ssh_cmd quote_cmd(LOGIN_LOOP), :sudo => false
97
98
 
98
- @pid, @inn, @out, @err = popen4(cmd.join(" "))
99
+ @pid, @inn, @out, @err = popen4 cmd.join(" ")
99
100
  @inn.sync = true
100
101
 
101
102
  data = ""
@@ -132,13 +133,11 @@ module Sunshine
132
133
  # Disconnect from host
133
134
 
134
135
  def disconnect
135
- return unless connected?
136
-
137
136
  @inn.close rescue nil
138
137
  @out.close rescue nil
139
138
  @err.close rescue nil
140
139
 
141
- kill_process @pid, "HUP"
140
+ kill_process @pid, "HUP" rescue nil
142
141
 
143
142
  @pid = nil
144
143
  end
@@ -191,6 +190,17 @@ module Sunshine
191
190
  end
192
191
 
193
192
 
193
+ ##
194
+ # Builds an ssh command with permissions, env, etc.
195
+
196
+ def build_remote_cmd cmd, options={}
197
+ cmd = sh_cmd cmd
198
+ cmd = env_cmd cmd
199
+ cmd = sudo_cmd cmd, options
200
+ cmd = ssh_cmd cmd, options
201
+ end
202
+
203
+
194
204
  ##
195
205
  # Uploads a file via rsync
196
206
 
@@ -237,13 +247,9 @@ module Sunshine
237
247
  ##
238
248
  # Wraps the command in an ssh call.
239
249
 
240
- def ssh_cmd string, options=nil
250
+ def ssh_cmd cmd, options=nil
241
251
  options ||= {}
242
252
 
243
- cmd = sh_cmd string
244
- cmd = env_cmd cmd
245
- cmd = sudo_cmd cmd, options
246
-
247
253
  flags = [*options[:flags]].concat @ssh_flags
248
254
 
249
255
  ["ssh", flags, @host, cmd].flatten.compact
@@ -13,14 +13,33 @@ module Sunshine
13
13
 
14
14
  class Repo
15
15
 
16
+ ##
17
+ # Adds subclasses to a repo_types hash for easy
18
+
19
+ def self.inherited subclass
20
+ @@repo_types ||= {}
21
+
22
+ # Turn Sunshine::ScmNameRepo into :scm_name
23
+ class_key = subclass.to_s.split("::").last
24
+ class_key = $1 if class_key =~ /(\w+)Repo$/
25
+ class_key.gsub! /([a-z0-9])([A-Z])/, '\1_\2'
26
+ class_key = class_key.downcase
27
+
28
+ @@repo_types[class_key] = subclass
29
+ end
30
+
31
+
16
32
  ##
17
33
  # Creates a new repo subclass object:
18
34
  # Repo.new_of_type :svn, "https://path/to/repo/tags/releasetag"
19
35
  # Repo.new_of_type :git, "user@gitbox.com:repo/path"
20
36
 
21
37
  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)
38
+ repo_class = @@repo_types[repo_type.to_s]
39
+
40
+ raise RepoError, "Invalid type #{repo_type.inspect}" unless repo_class
41
+
42
+ repo_class.new(url, options)
24
43
  end
25
44
 
26
45
 
@@ -36,15 +55,16 @@ module Sunshine
36
55
  # #=> nil
37
56
 
38
57
  def self.detect path=".", shell=nil
58
+ @@repo_types.values.each do |repo|
59
+ next if Sunshine::RsyncRepo === repo
39
60
 
40
- if SvnRepo.valid? path
41
- info = SvnRepo.get_info path, shell
42
- SvnRepo.new info[:url], info
43
-
44
- elsif GitRepo.valid? path
45
- info = GitRepo.get_info path, shell
46
- GitRepo.new info[:url], info
61
+ if repo.valid? path
62
+ info = repo.get_info path, shell
63
+ return repo.new(info[:url], info)
64
+ end
47
65
  end
66
+
67
+ nil
48
68
  end
49
69
 
50
70
 
@@ -69,21 +89,17 @@ module Sunshine
69
89
 
70
90
  ##
71
91
  # Checkout code to a shell and return an info log hash:
72
- # repo.chekout_to server, "some/path"
92
+ # repo.chekout_to "some/path", remote_shell
73
93
  # #=> {:revision => 123, :committer => 'someone', :date => time_obj ...}
74
94
 
75
95
  def checkout_to path, shell=nil
76
96
  shell ||= Sunshine.shell
77
97
 
78
- Sunshine.logger.info @scm,
79
- "Checking out to #{shell.host} #{path}" do
80
-
81
- shell.call "test -d #{path} && rm -rf #{path} || echo false"
82
- shell.call "mkdir -p #{path}"
98
+ shell.call "test -d #{path} && rm -rf #{path} || echo false"
99
+ shell.call "mkdir -p #{path}"
83
100
 
84
- do_checkout path, shell
85
- get_repo_info path, shell
86
- end
101
+ do_checkout path, shell
102
+ get_repo_info path, shell
87
103
  end
88
104
 
89
105
 
@@ -97,7 +113,7 @@ module Sunshine
97
113
 
98
114
 
99
115
  ##
100
- # Get the name of the specified repo - implemented by subclass
116
+ # Get the project name of the specified repo - implemented by subclass
101
117
 
102
118
  def name
103
119
  raise RepoError,
@@ -12,7 +12,7 @@ module Sunshine
12
12
 
13
13
  def initialize url, options={}
14
14
  super
15
- @flags << "-r"
15
+ @flags << '-r' << '--exclude .svn/' << '--exclude .git/'
16
16
  @url << "/" unless @url[-1..-1] == "/"
17
17
  end
18
18
 
@@ -145,9 +145,15 @@ module Sunshine
145
145
  ##
146
146
  # Checks out the app's codebase to the checkout path.
147
147
 
148
- def checkout_repo repo
148
+ def checkout_repo repo, scm_info={}
149
149
  install_deps repo.scm
150
- @info[:scm] = repo.checkout_to self.checkout_path, @shell
150
+
151
+ Sunshine.logger.info repo.scm,
152
+ "Checking out to #{@shell.host} #{self.checkout_path}" do
153
+
154
+ @info[:scm] = repo.checkout_to self.checkout_path, @shell
155
+ @info[:scm].merge! scm_info
156
+ end
151
157
  end
152
158
 
153
159
 
@@ -480,6 +486,16 @@ fi
480
486
  end
481
487
 
482
488
 
489
+ ##
490
+ # Assumes the passed code_dir is the root directory of the checked out
491
+ # codebase and uploads it to the checkout_path.
492
+
493
+ def upload_codebase code_dir, scm_info={}
494
+ RsyncRepo.new(code_dir).checkout_to self.checkout_path, @shell
495
+ @info[:scm] = scm_info
496
+ end
497
+
498
+
483
499
  ##
484
500
  # Upload common rake tasks from a local path or the sunshine lib.
485
501
  # app.upload_tasks
@@ -172,12 +172,19 @@ module Sunshine
172
172
 
173
173
 
174
174
  ##
175
- # Build an sh -c command
175
+ # Wrap command in quotes and escape as needed.
176
+
177
+ def quote_cmd cmd
178
+ cmd = [*cmd].join(" ")
179
+ "'#{cmd.gsub(/'/){|s| "'\\''"}}'"
180
+ end
176
181
 
177
- def sh_cmd string
178
- string = string.gsub(/'/){|s| "'\\''"}
179
182
 
180
- ["sh", "-c", "'#{string}'"]
183
+ ##
184
+ # Build an sh -c command
185
+
186
+ def sh_cmd cmd
187
+ ["sh", "-c", quote_cmd(cmd)]
181
188
  end
182
189
 
183
190
 
@@ -340,6 +347,7 @@ module Sunshine
340
347
  "Execution failed with status #{status.exitstatus}: #{[*cmd].join ' '}"
341
348
  end
342
349
 
350
+
343
351
  def password_required? stream_name, data
344
352
  stream_name == :err && data =~ SUDO_PROMPT
345
353
  end
@@ -1,5 +1,6 @@
1
1
  :default:
2
2
  :name: other_app
3
+ :remote_checkout: true
3
4
  :repo:
4
5
  :type: svn
5
6
  :url: svn://subversion/path/to/other_app/tags/release001
@@ -107,7 +107,7 @@ fi
107
107
 
108
108
 
109
109
  def assert_ssh_call expected, ds=@remote_shell, options={}
110
- expected = ds.send(:ssh_cmd, expected, options).join(" ")
110
+ expected = ds.build_remote_cmd(expected, options).join(" ")
111
111
 
112
112
  error_msg = "No such command in remote_shell log [#{ds.host}]\n#{expected}"
113
113
  error_msg << "\n\n#{ds.cmd_log.select{|c| c =~ /^ssh/}.join("\n\n")}"
@@ -1,9 +1,7 @@
1
1
  module MockOpen4
2
2
 
3
- LOGIN_CMD = "echo ready;"
4
-
5
3
  CMD_RETURN = {
6
- LOGIN_CMD => [:out, "ready\n"]
4
+ Sunshine::RemoteShell::LOGIN_LOOP => [:out, "ready\n"]
7
5
  }
8
6
 
9
7
  attr_reader :cmd_log
@@ -68,7 +66,9 @@ module MockOpen4
68
66
  next
69
67
  end
70
68
 
71
- key = ssh_cmd(key, options).join(" ") if Sunshine::RemoteShell === self
69
+ if Sunshine::RemoteShell === self
70
+ key = build_remote_cmd(key, options).join(" ")
71
+ end
72
72
 
73
73
  new_stream_vals[key] = (val.dup << code)
74
74
  end
@@ -13,7 +13,7 @@ unless defined? TEST_APP_CONFIG_FILE
13
13
  end
14
14
 
15
15
 
16
- Sunshine.setup({}, true)
16
+ Sunshine.setup({'remote_checkouts' => true}, true)
17
17
 
18
18
  unless MockObject === Sunshine.shell
19
19
  Sunshine.shell.extend MockObject
@@ -6,10 +6,12 @@ class TestApp < Test::Unit::TestCase
6
6
  mock_remote_shell_popen4
7
7
  @svn_url = "svn://subversion/path/to/app_name/trunk"
8
8
 
9
- @config = {:name => "app_name",
10
- :repo => {:type => "svn", :url => @svn_url},
11
- :remote_shells => ["user@some_server.com"],
12
- :root_path => "/usr/local/my_user/app_name"}
9
+ @config = {
10
+ :name => "app_name",
11
+ :remote_checkout => true,
12
+ :repo => {:type => "svn", :url => @svn_url},
13
+ :remote_shells => ["user@some_server.com"],
14
+ :root_path => "/usr/local/my_user/app_name"}
13
15
 
14
16
  @app = Sunshine::App.new @config
15
17
  @app.each do |server_app|
@@ -277,7 +279,7 @@ class TestApp < Test::Unit::TestCase
277
279
 
278
280
  state = true
279
281
  @app.server_apps.each do |sa|
280
- assert sa.method_called? :deployed?
282
+ assert sa.method_called?(:deployed?)
281
283
 
282
284
  set_mock_response_for sa.shell, 0,
283
285
  "cat #{@app.current_path}/info" => [:out,
@@ -17,8 +17,11 @@ class TestRemoteShell < Test::Unit::TestCase
17
17
  end
18
18
 
19
19
  def test_connect
20
- assert_ssh_call \
21
- "echo connected; echo ready; for (( ; ; )); do sleep 10; done"
20
+ login_cmd = Sunshine::RemoteShell::LOGIN_LOOP
21
+ login_cmd = @remote_shell.send :quote_cmd, login_cmd
22
+ login_cmd = @remote_shell.send :ssh_cmd, login_cmd, :sudo => false
23
+
24
+ assert @remote_shell.method_called?(:popen4, :args => [login_cmd.join(" ")])
22
25
  assert @remote_shell.connected?
23
26
  end
24
27
 
@@ -42,7 +45,7 @@ class TestRemoteShell < Test::Unit::TestCase
42
45
  @remote_shell.call cmd
43
46
  raise "Didn't raise CmdError on stderr"
44
47
  rescue Sunshine::CmdError => e
45
- ssh_cmd = @remote_shell.send(:ssh_cmd, cmd).join(" ")
48
+ ssh_cmd = @remote_shell.build_remote_cmd(cmd).join(" ")
46
49
  assert_equal "Execution failed with status 1: #{ssh_cmd}", e.message
47
50
  end
48
51
 
@@ -13,8 +13,16 @@ class TestRepo < Test::Unit::TestCase
13
13
  assert_equal Sunshine::SvnRepo, repo.class
14
14
  assert_equal @svn_url, repo.url
15
15
 
16
- repo = Sunshine::Repo.new_of_type "", @svn_url
17
- assert_equal Sunshine::Repo, repo.class
16
+ repo = Sunshine::Repo.new_of_type 'git', @svn_url
17
+ assert_equal Sunshine::GitRepo, repo.class
18
+ assert_equal @svn_url, repo.url
19
+
20
+ begin
21
+ repo = Sunshine::Repo.new_of_type "", @svn_url
22
+ raise "Didn't raise RepoError for invalid repo type"
23
+ rescue Sunshine::RepoError => e
24
+ assert_equal "Invalid type \"\"", e.message
25
+ end
18
26
  end
19
27
 
20
28
 
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 1
7
7
  - 1
8
- - 0
9
- version: 1.1.0
8
+ - 1
9
+ version: 1.1.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Jeremie Castagna
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-04-02 00:00:00 -07:00
17
+ date: 2010-04-06 00:00:00 -07:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency