deployml 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/.document +3 -0
  2. data/.rspec +1 -0
  3. data/.yardopts +1 -0
  4. data/ChangeLog.md +22 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +107 -0
  7. data/Rakefile +35 -0
  8. data/bin/deployml +5 -0
  9. data/deployml.gemspec +10 -0
  10. data/gemspec.yml +21 -0
  11. data/lib/deployml/cli.rb +192 -0
  12. data/lib/deployml/configuration.rb +149 -0
  13. data/lib/deployml/environment.rb +273 -0
  14. data/lib/deployml/exceptions/config_not_found.rb +4 -0
  15. data/lib/deployml/exceptions/invalid_config.rb +4 -0
  16. data/lib/deployml/exceptions/missing_option.rb +6 -0
  17. data/lib/deployml/exceptions/unknown_environment.rb +4 -0
  18. data/lib/deployml/exceptions/unknown_framework.rb +6 -0
  19. data/lib/deployml/exceptions/unknown_server.rb +6 -0
  20. data/lib/deployml/frameworks/rails2.rb +9 -0
  21. data/lib/deployml/frameworks/rails3.rb +20 -0
  22. data/lib/deployml/frameworks.rb +2 -0
  23. data/lib/deployml/local_shell.rb +54 -0
  24. data/lib/deployml/options/mongrel.rb +46 -0
  25. data/lib/deployml/options/thin.rb +72 -0
  26. data/lib/deployml/options.rb +1 -0
  27. data/lib/deployml/project.rb +304 -0
  28. data/lib/deployml/remote_shell.rb +130 -0
  29. data/lib/deployml/servers/apache.rb +19 -0
  30. data/lib/deployml/servers/mongrel.rb +41 -0
  31. data/lib/deployml/servers/thin.rb +41 -0
  32. data/lib/deployml/servers.rb +3 -0
  33. data/lib/deployml/shell.rb +46 -0
  34. data/lib/deployml/version.rb +4 -0
  35. data/lib/deployml.rb +2 -0
  36. data/spec/configuration_spec.rb +73 -0
  37. data/spec/deployml_spec.rb +11 -0
  38. data/spec/environment_spec.rb +34 -0
  39. data/spec/helpers/projects/bad_config/config/deploy.yml +1 -0
  40. data/spec/helpers/projects/basic/config/deploy.yml +3 -0
  41. data/spec/helpers/projects/invalid_server/config/deploy.yml +4 -0
  42. data/spec/helpers/projects/missing_config/.gitkeep +0 -0
  43. data/spec/helpers/projects/missing_dest/config/deploy.yml +2 -0
  44. data/spec/helpers/projects/missing_source/config/deploy.yml +2 -0
  45. data/spec/helpers/projects/rails/config/deploy/production.yml +10 -0
  46. data/spec/helpers/projects/rails/config/deploy/staging.yml +10 -0
  47. data/spec/helpers/projects/rails/config/deploy.yml +3 -0
  48. data/spec/helpers/projects.rb +11 -0
  49. data/spec/project_spec.rb +66 -0
  50. data/spec/spec_helper.rb +8 -0
  51. metadata +207 -0
@@ -0,0 +1,304 @@
1
+ require 'deployml/exceptions/config_not_found'
2
+ require 'deployml/exceptions/invalid_config'
3
+ require 'deployml/exceptions/unknown_environment'
4
+ require 'deployml/environment'
5
+ require 'deployml/remote_shell'
6
+
7
+ require 'yaml'
8
+
9
+ module DeploYML
10
+ class Project
11
+
12
+ # The general configuration directory.
13
+ CONFIG_DIR = 'config'
14
+
15
+ # The configuration file name.
16
+ CONFIG_FILE = 'deploy.yml'
17
+
18
+ # The configuration directory.
19
+ ENVIRONMENTS_DIR = 'deploy'
20
+
21
+ # The name of the directory to stage deployments in.
22
+ STAGING_DIR = '.deploy'
23
+
24
+ # The root directory of the project
25
+ attr_reader :root
26
+
27
+ # The deployment environments of the project
28
+ attr_reader :environments
29
+
30
+ #
31
+ # Creates a new project using the given configuration file.
32
+ #
33
+ # @param [String] root
34
+ # The root directory of the project.
35
+ #
36
+ # @raise [ConfigNotFound]
37
+ # The configuration file for the project could not be found
38
+ # in any of the common directories.
39
+ #
40
+ def initialize(root)
41
+ @root = File.expand_path(root)
42
+ @config_file = File.join(@root,CONFIG_DIR,CONFIG_FILE)
43
+ @environments_dir = File.join(@root,CONFIG_DIR,ENVIRONMENTS_DIR)
44
+
45
+ unless (File.file?(@config_file) || File.directory?(@environments_dir))
46
+ raise(ConfigNotFound,"could not find '#{CONFIG_FILE}' or '#{ENVIRONMENTS_DIR}' in #{root}")
47
+ end
48
+
49
+ load_environments!
50
+ end
51
+
52
+ #
53
+ # @param [Symbol, String] name
54
+ # The name of the environment to use.
55
+ #
56
+ # @return [Environment]
57
+ # The environment with the given name.
58
+ #
59
+ # @raise [UnknownEnvironment]
60
+ # No environment was configured with the given name.
61
+ #
62
+ # @since 0.3.0
63
+ #
64
+ def environment(name=:production)
65
+ name = name.to_sym
66
+
67
+ unless @environments[name]
68
+ raise(UnknownEnvironment,"unknown environment: #{name}")
69
+ end
70
+
71
+ return @environments[name]
72
+ end
73
+
74
+ #
75
+ # Conveniance method for accessing the development environment.
76
+ #
77
+ # @return [Environment]
78
+ # The development environment.
79
+ #
80
+ # @since 0.3.0
81
+ #
82
+ def development
83
+ environment(:development)
84
+ end
85
+
86
+ #
87
+ # Conveniance method for accessing the staging environment.
88
+ #
89
+ # @return [Environment]
90
+ # The staging environment.
91
+ #
92
+ # @since 0.3.0
93
+ #
94
+ def staging
95
+ environment(:staging)
96
+ end
97
+
98
+ #
99
+ # Conveniance method for accessing the production environment.
100
+ #
101
+ # @return [Environment]
102
+ # The production environment.
103
+ #
104
+ # @since 0.3.0
105
+ #
106
+ def production
107
+ environment(:production)
108
+ end
109
+
110
+ #
111
+ # Deploys the project.
112
+ #
113
+ # @param [Array<Symbol>] tasks
114
+ # The tasks to run during the deployment.
115
+ #
116
+ # @param [Symbol, String] env
117
+ # The environment to deploy to.
118
+ #
119
+ # @return [true]
120
+ #
121
+ # @since 0.2.0
122
+ #
123
+ def invoke(tasks,env=:production)
124
+ env = environment(env)
125
+
126
+ env.remote_shell do |shell|
127
+ # setup the deployment repository
128
+ env.setup(shell) if tasks.include?(:setup)
129
+
130
+ # cd into the deployment repository
131
+ shell.cd env.dest.path
132
+
133
+ # update the deployment repository
134
+ env.update(shell) if tasks.include?(:update)
135
+
136
+ # framework tasks
137
+ env.install(shell) if tasks.include?(:install)
138
+ env.migrate(shell) if tasks.include?(:migrate)
139
+
140
+ # server tasks
141
+ if tasks.include?(:config)
142
+ env.server_config(shell)
143
+ elsif tasks.include?(:start)
144
+ env.server_start(shell)
145
+ elsif tasks.include?(:stop)
146
+ env.server_stop(shell)
147
+ elsif tasks.include?(:restart)
148
+ env.server_restart(shell)
149
+ end
150
+ end
151
+
152
+ return true
153
+ end
154
+
155
+ #
156
+ # Sets up the deployment repository for the project.
157
+ #
158
+ # @param [Symbol, String] env
159
+ # The environment to deploy to.
160
+ #
161
+ def setup!(env=:production)
162
+ invoke [:setup], env
163
+ end
164
+
165
+ #
166
+ # Updates the deployed repository of the project.
167
+ #
168
+ # @param [Symbol, String] env
169
+ # The environment to deploy to.
170
+ #
171
+ def update!(env=:production)
172
+ invoke [:update], env
173
+ end
174
+
175
+ #
176
+ # Installs the project on the destination server.
177
+ #
178
+ # @param [Symbol, String] env
179
+ # The environment to deploy to.
180
+ #
181
+ def install!(env=:production)
182
+ invoke [:install], env
183
+ end
184
+
185
+ #
186
+ # Migrates the database used by the project.
187
+ #
188
+ # @param [Symbol, String] env
189
+ # The environment to deploy to.
190
+ #
191
+ def migrate!(env=:production)
192
+ invoke [:migrate], env
193
+ end
194
+
195
+ #
196
+ # Configures the Web server to be ran on the destination server.
197
+ #
198
+ # @param [Symbol, String] env
199
+ # The environment to deploy to.
200
+ #
201
+ def config!(env=:production)
202
+ invoke [:config], env
203
+ end
204
+
205
+ #
206
+ # Starts the Web server for the project.
207
+ #
208
+ # @param [Symbol, String] env
209
+ # The environment to deploy to.
210
+ #
211
+ def start!(env=:production)
212
+ invoke [:start], env
213
+ end
214
+
215
+ #
216
+ # Stops the Web server for the project.
217
+ #
218
+ # @param [Symbol, String] env
219
+ # The environment to deploy to.
220
+ #
221
+ def stop!(env=:production)
222
+ invoke [:stop], env
223
+ end
224
+
225
+ #
226
+ # Restarts the Web server for the project.
227
+ #
228
+ # @param [Symbol, String] env
229
+ # The environment to deploy to.
230
+ #
231
+ def restart!(env=:production)
232
+ invoke [:restart], env
233
+ end
234
+
235
+ #
236
+ # Deploys a new project.
237
+ #
238
+ # @param [Symbol, String] env
239
+ # The environment to deploy to.
240
+ #
241
+ # @since 0.2.0
242
+ #
243
+ def deploy!(env=:production)
244
+ invoke [:setup, :install, :migrate, :config, :start], env
245
+ end
246
+
247
+ #
248
+ # Redeploys a project.
249
+ #
250
+ # @param [Symbol, String] env
251
+ # The environment to deploy to.
252
+ #
253
+ # @since 0.2.0
254
+ #
255
+ def redeploy!(env=:production)
256
+ invoke [:update, :install, :migrate, :restart], env
257
+ end
258
+
259
+ protected
260
+
261
+ #
262
+ # Loads the project configuration.
263
+ #
264
+ # @raise [InvalidConfig]
265
+ # The YAML configuration file did not contain a Hash.
266
+ #
267
+ # @raise [MissingOption]
268
+ # The `source` or `dest` options were not specified.
269
+ #
270
+ # @since 0.3.0
271
+ #
272
+ def load_environments!
273
+ base_config = {}
274
+
275
+ load_config_data = lambda { |path|
276
+ config_data = YAML.load_file(path)
277
+
278
+ unless config_data.kind_of?(Hash)
279
+ raise(InvalidConfig,"DeploYML file '#{path}' does not contain a Hash")
280
+ end
281
+
282
+ config_data
283
+ }
284
+
285
+ if File.file?(@config_file)
286
+ base_config.merge!(load_config_data[@config_file])
287
+ end
288
+
289
+ @environments = {}
290
+
291
+ if File.directory?(@environments_dir)
292
+ Dir.glob(File.join(@environments_dir,'*.yml')) do |path|
293
+ config_data = base_config.merge(load_config_data[path])
294
+ name = File.basename(path).sub(/\.yml$/,'').to_sym
295
+
296
+ @environments[name] = Environment.new(name,config_data)
297
+ end
298
+ else
299
+ @environments[:production] = Environment.new(:production,base_config)
300
+ end
301
+ end
302
+
303
+ end
304
+ end
@@ -0,0 +1,130 @@
1
+ require 'deployml/shell'
2
+
3
+ module DeploYML
4
+ class RemoteShell
5
+
6
+ include Shell
7
+
8
+ #
9
+ # Initializes a remote shell session.
10
+ #
11
+ # @param [Addressable::URI, String] uri
12
+ # The URI of the host to connect to.
13
+ #
14
+ # @yield [session]
15
+ # If a block is given, it will be passed the new remote shell session.
16
+ #
17
+ # @yieldparam [ShellSession] session
18
+ # The remote shell session.
19
+ #
20
+ def initialize(uri,&block)
21
+ case uri
22
+ when Addressable::URI
23
+ @uri = uri
24
+ else
25
+ @uri = Addressable::URI.parse(uri.to_s)
26
+ end
27
+
28
+ @history = []
29
+
30
+ super(&block)
31
+
32
+ replay if block
33
+ end
34
+
35
+ #
36
+ # Enqueues a program to be ran in the session.
37
+ #
38
+ # @param [String] program
39
+ # The name or path of the program to run.
40
+ #
41
+ # @param [Array<String>] args
42
+ # Additional arguments for the program.
43
+ #
44
+ def run(program,*args)
45
+ @history << [program, *args]
46
+ end
47
+
48
+ #
49
+ # Enqueues an `echo` command to be ran in the session.
50
+ #
51
+ # @param [String] message
52
+ # The message to echo.
53
+ #
54
+ def each(message)
55
+ run 'echo', message
56
+ end
57
+
58
+ #
59
+ # Enqueues a directory change for the session.
60
+ #
61
+ # @param [String] path
62
+ # The path of the new current working directory to use.
63
+ #
64
+ # @yield []
65
+ # If a block is given, then the directory will be changed back after
66
+ # the block has returned.
67
+ #
68
+ def cd(path,&block)
69
+ @history << ['cd', path]
70
+
71
+ if block
72
+ block.call() if block
73
+
74
+ @history << ['cd', '-']
75
+ end
76
+ end
77
+
78
+ #
79
+ # Joins the command history together with ` && `, to form a
80
+ # single command.
81
+ #
82
+ # @return [String]
83
+ # A single command string.
84
+ #
85
+ def join
86
+ @history.map { |command| command.join(' ') }.join(' && ')
87
+ end
88
+
89
+ #
90
+ # Converts the URI to one compatible with SSH.
91
+ #
92
+ # @return [String]
93
+ # The SSH compatible URI.
94
+ #
95
+ def ssh_uri
96
+ new_uri = @uri.host
97
+ new_uri = "#{@uri.user}@#{new_uri}" if @uri.user
98
+
99
+ return new_uri
100
+ end
101
+
102
+ #
103
+ # Starts a SSH session with the destination server.
104
+ #
105
+ # @param [Array] args
106
+ # Additional arguments to pass to SSH.
107
+ #
108
+ def ssh(*args)
109
+ options = []
110
+
111
+ # Add the -p option if an alternate destination port is given
112
+ if @uri.port
113
+ options += ['-p', @uri.port.to_s]
114
+ end
115
+
116
+ options << ssh_uri
117
+ options += args
118
+
119
+ return system('ssh',*options)
120
+ end
121
+
122
+ #
123
+ # Replays the command history on the remote server.
124
+ #
125
+ def replay
126
+ ssh(self.join) unless @history.empty?
127
+ end
128
+
129
+ end
130
+ end
@@ -0,0 +1,19 @@
1
+ require 'deployml/exceptions/invalid_config'
2
+
3
+ module DeploYML
4
+ module Servers
5
+ module Apache
6
+ def server_start(shell)
7
+ shell.run 'apachectl', 'start'
8
+ end
9
+
10
+ def server_restart(shell)
11
+ shell.run 'apachectl', 'restart'
12
+ end
13
+
14
+ def server_stop(shell)
15
+ shell.run 'apachectl', 'stop'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ require 'deployml/exceptions/missing_option'
2
+ require 'deployml/options/mongrel'
3
+
4
+ module DeploYML
5
+ module Servers
6
+ module Mongrel
7
+ def initialize_server
8
+ @mongrel = Options::Mongrel.new(@server_options)
9
+ @mongrel.environment ||= @name
10
+ end
11
+
12
+ def mongrel_cluster(shell,*args)
13
+ options = args + ['-c', @mongrel.config]
14
+
15
+ shell.run 'mongrel_rails', *options
16
+ end
17
+
18
+ def server_config(shell)
19
+ unless @mongrel.config
20
+ raise(MissingOption,"No 'config' option specified under server options",caller)
21
+ end
22
+
23
+ options = ['-c', dest.path] + @mongrel.arguments
24
+
25
+ shell.run 'mongrel_rails', 'cluster::configure', *options
26
+ end
27
+
28
+ def server_start(shell)
29
+ mongrel_cluster 'cluster::start'
30
+ end
31
+
32
+ def server_stop(shell)
33
+ mongrel_cluster 'cluster::stop'
34
+ end
35
+
36
+ def server_restart(shell)
37
+ mongrel_cluster 'cluster::restart'
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ require 'deployml/exceptions/missing_option'
2
+ require 'deployml/options/thin'
3
+
4
+ module DeploYML
5
+ module Servers
6
+ module Thin
7
+ def initialize_server
8
+ @thin = Options::Thin.new(@server_options)
9
+ @thin.environment ||= @name
10
+ end
11
+
12
+ def thin(shell,*args)
13
+ options = args + ['-C', @thin.config, '-s', @thin.servers]
14
+
15
+ shell.run 'thin', *options
16
+ end
17
+
18
+ def server_config(shell)
19
+ unless @thin.config
20
+ raise(MissingOption,"No 'config' option specified under the server options",caller)
21
+ end
22
+
23
+ options = ['-c', dest.path] + @thin.arguments
24
+
25
+ shell.run 'thin', 'config', *options
26
+ end
27
+
28
+ def server_start(shell)
29
+ thin shell, 'start'
30
+ end
31
+
32
+ def server_stop(shell)
33
+ thin shell, 'stop'
34
+ end
35
+
36
+ def server_restart(shell)
37
+ thin shell, 'restart'
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ require 'deployml/servers/apache'
2
+ require 'deployml/servers/mongrel'
3
+ require 'deployml/servers/thin'
@@ -0,0 +1,46 @@
1
+ module DeploYML
2
+ module Shell
3
+
4
+ def initialize(&block)
5
+ block.call(self) if block
6
+ end
7
+
8
+ #
9
+ # Place-holder method.
10
+ #
11
+ # @param [String] program
12
+ # The name or path of the program to run.
13
+ #
14
+ # @param [Array<String>] args
15
+ # Additional arguments for the program.
16
+ #
17
+ def run(program,*args)
18
+ end
19
+
20
+ #
21
+ # Executes a Rake task.
22
+ #
23
+ # @param [Symbol, String] task
24
+ # Name of the Rake task to run.
25
+ #
26
+ # @param [Array<String>] args
27
+ # Additional arguments for the Rake task.
28
+ #
29
+ def rake(task,*args)
30
+ run 'rake', rake_task(task,*args)
31
+ end
32
+
33
+ protected
34
+
35
+ def rake_task(name,*args)
36
+ name = name.to_s
37
+
38
+ unless args.empty?
39
+ name += ('[' + args.join(',') + ']')
40
+ end
41
+
42
+ return name
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+ module DeploYML
2
+ # deploYML version
3
+ VERSION = '0.3.0'
4
+ end
data/lib/deployml.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'deployml/project'
2
+ require 'deployml/version'
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ require 'deployml/configuration'
4
+
5
+ describe Configuration do
6
+ it "should accept String keys" do
7
+ config = Configuration.new('orm' => :datamapper)
8
+
9
+ config.orm.should == :datamapper
10
+ end
11
+
12
+ it "should accept Symbol keys" do
13
+ config = Configuration.new(:orm => :datamapper)
14
+
15
+ config.orm.should == :datamapper
16
+ end
17
+
18
+ it "should parse 'dest' String URIs" do
19
+ config = Configuration.new(
20
+ :dest => 'ssh://user@www.example.com/srv/project'
21
+ )
22
+
23
+ config.dest.scheme.should == 'ssh'
24
+ config.dest.user.should == 'user'
25
+ config.dest.host.should == 'www.example.com'
26
+ config.dest.path.should == '/srv/project'
27
+ end
28
+
29
+ it "should parse 'dest' Hash URIs" do
30
+ config = Configuration.new(:dest => {
31
+ 'scheme' => 'ssh',
32
+ 'user' => 'user',
33
+ 'host' => 'www.example.com',
34
+ 'path' => '/srv/project'
35
+ })
36
+
37
+ config.dest.scheme.should == 'ssh'
38
+ config.dest.user.should == 'user'
39
+ config.dest.host.should == 'www.example.com'
40
+ config.dest.path.should == '/srv/project'
41
+ end
42
+
43
+ it "should default the 'debug' option to false" do
44
+ config = Configuration.new
45
+
46
+ config.debug.should == false
47
+ end
48
+
49
+ it "should default the environment to nil" do
50
+ config = Configuration.new
51
+
52
+ config.environment.should be_nil
53
+ end
54
+
55
+ it "should accept a Symbol for the 'server' option" do
56
+ config = Configuration.new(:server => :thin)
57
+
58
+ config.server_name.should == :thin
59
+ config.server_options.should be_empty
60
+ end
61
+
62
+ it "should accept a Hash for the 'server' option" do
63
+ config = Configuration.new(
64
+ :server => {
65
+ :name => :thin,
66
+ :options => {:address => '127.0.0.1'}
67
+ }
68
+ )
69
+
70
+ config.server_name.should == :thin
71
+ config.server_options.should == {:address => '127.0.0.1'}
72
+ end
73
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ require 'deployml/version'
4
+
5
+ describe DeploYML do
6
+ it "should have a version" do
7
+ @version = DeploYML.const_get('VERSION')
8
+ @version.should_not be_nil
9
+ @version.should_not be_empty
10
+ end
11
+ end