-
Notifications
You must be signed in to change notification settings - Fork 1
/
deploy.rb
351 lines (304 loc) · 12.3 KB
/
deploy.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
#
# Author:: Daniel DeLeo (<dan@kallistec.com>)
# Copyright:: Copyright (c) 2008 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require "chef/mixin/command"
require "chef/mixin/from_file"
require "chef/provider/git"
require "chef/provider/subversion"
class Chef
class Provider
class Deploy < Chef::Provider
include Chef::Mixin::FromFile
include Chef::Mixin::Command
attr_reader :scm_provider, :release_path
def initialize(new_resource, run_context)
super(new_resource, run_context)
@scm_provider = new_resource.scm_provider.new(new_resource, run_context)
# @configuration is not used by Deploy, it is only for backwards compat with
# chef-deploy or capistrano hooks that might use it to get environment information
@configuration = @new_resource.to_hash
@configuration[:environment] = @configuration[:environment] && @configuration[:environment]["RAILS_ENV"]
end
def load_current_resource
@release_path = @new_resource.deploy_to + "/releases/#{release_slug}"
end
def sudo(command,&block)
execute(command, &block)
end
def run(command, &block)
exec = execute(command, &block)
exec.user(@new_resource.user) if @new_resource.user
exec.group(@new_resource.group) if @new_resource.group
exec.cwd(release_path) unless exec.cwd
exec.environment(@new_resource.environment) unless exec.environment
exec
end
def action_deploy
if deployed?(release_path)
if current_release?(release_path)
Chef::Log.debug("#{@new_resource} is the latest version")
else
action_rollback
end
else
deploy
@new_resource.updated_by_last_action(true)
end
end
def action_force_deploy
if deployed?(release_path)
Chef::Log.info("Already deployed app at #{release_path}, forcing.")
FileUtils.rm_rf(release_path)
Chef::Log.info("#{@new_resource} forcing deploy of already deployed app at #{release_path}")
end
deploy
@new_resource.updated_by_last_action(true)
end
def action_rollback
if release_path
rp_index = all_releases.index(release_path)
raise RuntimeError, "There is no release to rollback to!" unless rp_index
rp_index += 1
releases_to_nuke = all_releases[rp_index..-1]
else
@release_path = all_releases[-2]
raise RuntimeError, "There is no release to rollback to!" unless @release_path
releases_to_nuke = [ all_releases.last ]
end
Chef::Log.info "#{@new_resource} rolling back to previous release #{release_path}"
symlink
Chef::Log.info "#{@new_resource} restarting with previous release"
restart
releases_to_nuke.each do |i|
Chef::Log.info "#{@new_resource} removing release: #{i}"
FileUtils.rm_rf i
release_deleted(i)
end
@new_resource.updated_by_last_action(true)
end
def deploy
enforce_ownership
update_cached_repo
copy_cached_repo
install_gems
enforce_ownership
callback(:before_migrate, @new_resource.before_migrate)
migrate
callback(:before_symlink, @new_resource.before_symlink)
symlink
callback(:before_restart, @new_resource.before_restart)
restart
callback(:after_restart, @new_resource.after_restart)
cleanup!
Chef::Log.info "#{@new_resource} deployed to #{@new_resource.deploy_to}"
end
def callback(what, callback_code=nil)
@collection = Chef::ResourceCollection.new
case callback_code
when Proc
Chef::Log.info "#{@new_resource} running callback #{what}"
recipe_eval(&callback_code)
when String
callback_file = "#{release_path}/#{callback_code}"
unless ::File.exist?(callback_file)
raise RuntimeError, "Can't find your callback file #{callback_file}"
end
run_callback_from_file(callback_file)
when nil
run_callback_from_file("#{release_path}/deploy/#{what}.rb")
else
raise RuntimeError, "You gave me a callback I don't know what to do with: #{callback_code.inspect}"
end
end
def migrate
run_symlinks_before_migrate
if @new_resource.migrate
enforce_ownership
environment = @new_resource.environment
env_info = environment && environment.map do |key_and_val|
"#{key_and_val.first}='#{key_and_val.last}'"
end.join(" ")
Chef::Log.info "#{@new_resource} migrating #{@new_resource.user} with environment #{env_info}"
run_command(run_options(:command => @new_resource.migration_command, :cwd=>release_path, :command_log_level => :info))
end
end
def symlink
purge_tempfiles_from_current_release
link_tempfiles_to_current_release
link_current_release_to_production
Chef::Log.info "#{@new_resource} updated symlinks"
end
def restart
if restart_cmd = @new_resource.restart_command
if restart_cmd.kind_of?(Proc)
Chef::Log.info("#{@new_resource} restarting app with embedded recipe")
recipe_eval(&restart_cmd)
else
Chef::Log.info("#{@new_resource} restarting app")
run_command(run_options(:command => @new_resource.restart_command, :cwd => @new_resource.current_path))
end
end
end
def cleanup!
all_releases[0..-6].each do |old_release|
Chef::Log.info "#{@new_resource} removing old release #{old_release}"
FileUtils.rm_rf(old_release)
release_deleted(old_release)
end
end
def all_releases
Dir.glob(@new_resource.deploy_to + "/releases/*").sort
end
def update_cached_repo
@scm_provider.load_current_resource
if @new_resource.svn_force_export
svn_force_export
else
run_scm_sync
end
end
def run_scm_sync
@scm_provider.action_sync
end
def svn_force_export
Chef::Log.info "#{@new_resource} exporting source repository"
@scm_provider.action_force_export
end
def copy_cached_repo
FileUtils.mkdir_p(@new_resource.deploy_to + "/releases")
run_command(:command => "cp -RPp #{::File.join(@new_resource.destination, ".")} #{release_path}")
Chef::Log.info "#{@new_resource} copied the cached checkout to #{release_path}"
release_created(release_path)
end
def enforce_ownership
FileUtils.chown_R(@new_resource.user, @new_resource.group, @new_resource.deploy_to)
Chef::Log.info("#{@new_resource} set user to #{@new_resource.user}") if @new_resource.user
Chef::Log.info("#{@new_resource} set group to #{@new_resource.group}") if @new_resource.group
end
def link_current_release_to_production
FileUtils.rm_f(@new_resource.current_path)
begin
FileUtils.ln_sf(release_path, @new_resource.current_path)
rescue => e
raise Chef::Exceptions::FileNotFound.new("Cannot symlink current release to production: #{e.message}")
end
Chef::Log.info "#{@new_resource} linked release #{release_path} into production at #{@new_resource.current_path}"
enforce_ownership
end
def run_symlinks_before_migrate
links_info = @new_resource.symlink_before_migrate.map { |src, dst| "#{src} => #{dst}" }.join(", ")
@new_resource.symlink_before_migrate.each do |src, dest|
begin
FileUtils.ln_sf(@new_resource.shared_path + "/#{src}", release_path + "/#{dest}")
rescue => e
raise Chef::Exceptions::FileNotFound.new("Cannot symlink #{@new_resource.shared_path}/#{src} to #{release_path}/#{dest} before migrate: #{e.message}")
end
end
Chef::Log.info "#{@new_resource} made pre-migration symlinks"
end
def link_tempfiles_to_current_release
dirs_info = @new_resource.create_dirs_before_symlink.join(",")
@new_resource.create_dirs_before_symlink.each do |dir|
begin
FileUtils.mkdir_p(release_path + "/#{dir}")
rescue => e
raise Chef::Exceptions::FileNotFound.new("Cannot create directory #{dir}: #{e.message}")
end
end
Chef::Log.info("#{@new_resource} created directories before symlinking #{dirs_info}")
links_info = @new_resource.symlinks.map { |src, dst| "#{src} => #{dst}" }.join(", ")
@new_resource.symlinks.each do |src, dest|
begin
FileUtils.ln_sf(@new_resource.shared_path + "/#{src}", release_path + "/#{dest}")
rescue => e
raise Chef::Exceptions::FileNotFound.new("Cannot symlink shared data #{@new_resource.shared_path}/#{src} to #{release_path}/#{dest}: #{e.message}")
end
end
Chef::Log.info("#{@new_resource} linked shared paths into current release: #{links_info}")
run_symlinks_before_migrate
enforce_ownership
end
def create_dirs_before_symlink
end
def purge_tempfiles_from_current_release
log_info = @new_resource.purge_before_symlink.join(", ")
@new_resource.purge_before_symlink.each { |dir| FileUtils.rm_rf(release_path + "/#{dir}") }
Chef::Log.info("#{@new_resource} purged directories in checkout #{log_info}")
end
protected
# Internal callback, called after copy_cached_repo.
# Override if you need to keep state externally.
def release_created(release_path)
end
# Internal callback, called during cleanup! for each old release removed.
# Override if you need to keep state externally.
def release_deleted(release_path)
end
def release_slug
raise Chef::Exceptions::Override, "You must override release_slug in #{self.to_s}"
end
def install_gems
gem_resource_collection_runner.converge
end
def gem_resource_collection_runner
gems_collection = Chef::ResourceCollection.new
gem_packages.each { |rbgem| gems_collection << rbgem }
gems_run_context = run_context.dup
gems_run_context.resource_collection = gems_collection
Chef::Runner.new(gems_run_context)
end
def gem_packages
return [] unless ::File.exist?("#{release_path}/gems.yml")
gems = YAML.load(IO.read("#{release_path}/gems.yml"))
gems.map do |g|
r = Chef::Resource::GemPackage.new(g[:name], run_context)
r.version g[:version]
r.action :install
r.source "http://gems.github.com"
r
end
end
def run_options(run_opts={})
run_opts[:user] = @new_resource.user if @new_resource.user
run_opts[:group] = @new_resource.group if @new_resource.group
run_opts[:environment] = @new_resource.environment if @new_resource.environment
run_opts[:command_log_prepend] = @new_resource.to_s
run_opts[:command_log_level] ||= :debug
if run_opts[:command_log_level] == :info
if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.info?
run_opts[:live_stream] = STDOUT
end
end
run_opts
end
def run_callback_from_file(callback_file)
if ::File.exist?(callback_file)
Dir.chdir(release_path) do
Chef::Log.info "#{@new_resource} running deploy hook #{callback_file}"
recipe_eval { from_file(callback_file) }
end
end
end
def deployed?(release)
all_releases.include?(release)
end
def current_release?(release)
all_releases[-1] == release
end
end
end
end