k3_capistrano 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,132 @@
1
+ # This is the main file that you should require. It loads everything except k3_capistrano/test_request.
2
+
3
+ require 'k3_capistrano/version'
4
+ require 'bundler/capistrano'
5
+ require 'capistrano/ext/multistage' # Note: This currently requires the capistrano-ext gem
6
+ require 'capistrano_colors'
7
+
8
+ #===================================================================================================
9
+ Capistrano::Configuration::Namespaces::Namespace.class_eval do
10
+ def capture(*args)
11
+ parent.capture *args
12
+ end
13
+ end
14
+
15
+ Capistrano::Configuration.instance(:must_exist).load do
16
+
17
+ #===================================================================================================
18
+
19
+ set :scm, 'git'
20
+ _cset :scm_verbose, false
21
+ #set :git_shallow_clone, 1
22
+
23
+ current_branch = `git symbolic-ref HEAD | sed s#^refs/heads/##`.chomp
24
+ #current_branch = `git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/'`.chomp
25
+ _cset :branch, ENV['branch'] || current_branch
26
+ puts "Warning: Deploying branch '#{branch}'" unless branch == 'master'
27
+
28
+ # Deploying to a '/u/' directory seems silly. Set a more reasonable default here.
29
+ if deploy_to.start_with? '/u/'
30
+ #_cset :deploy_to, '~'
31
+ set :deploy_to, "/apps/#{application}"
32
+ end
33
+
34
+ #set :deploy_via, :remote_cache
35
+ _cset :deploy_via, :export
36
+ _cset :rails_env, 'production'
37
+
38
+ _cset :user, application
39
+ _cset :use_sudo, false
40
+
41
+ # We can always use this for the application user, even if user is unset.
42
+ # Question: Where is app_user used?
43
+ #_cset :app_user, user
44
+
45
+ # Skip bundling cucumber and other non-production environments
46
+ _cset :bundle_without, [:development, :test, :cucumber]
47
+
48
+ # Get rid of annoying zlib error message at end of deploy. I can't imagine compression is helping much...
49
+ ssh_options[:compression] = "none"
50
+
51
+ # Enable this if you run into problems with the bundle command hanging
52
+ default_run_options[:pty] = true
53
+
54
+ set :skip, ENV['skip'] ? ENV['skip'].split(',') : []
55
+
56
+ #===================================================================================================
57
+ # Stages
58
+
59
+ _cset :stages, %w(staging production)
60
+ _cset :default_stage, "staging"
61
+
62
+ #===================================================================================================
63
+ require 'rvm/capistrano'
64
+
65
+ set :rvm_type, :system
66
+
67
+ #===================================================================================================
68
+ # Triggers
69
+
70
+ before 'deploy', 'git:check_if_needs_push'
71
+ before 'deploy:update_code', 'db:read_remote_my_cnf'
72
+ before 'deploy:update_code', 'db:database_yml:setup'
73
+ after 'deploy:update_code', 'deploy:ensure_db_is_set_up'
74
+ after 'deploy:update_code', 'deploy:migrate' unless skip.include?('migrate') || ARGV.include?('deploy:update_code')
75
+
76
+ #===================================================================================================
77
+ # Problem: release_path is set to a new path each time you run cap, and usually that path doesn't exist.
78
+ # It only exists after doing a deploy.
79
+ # TODO: Can we detect whether it exists? Like this: capture("ls -1 #{release_path} #{current_path}").split.first
80
+ # (Possibly not because release_or_current_path might get called too early and thus release_path would never exist by that point in the deploy.)
81
+ if ARGV.include? 'deploy'
82
+ # If deploying, use release_path, which *will* become current_path, but will not be symlinked from current until the deploy is done
83
+ set :release_or_current_path, -> { release_path }
84
+ else
85
+ # If not deploying, use current_path, which is the path of the last/current deploy. release_path wouldn't even exist or make sense in this case.
86
+ set :release_or_current_path, -> { current_path }
87
+ end
88
+ #puts %(release_or_current_path=#{(release_or_current_path).inspect})
89
+
90
+ #===================================================================================================
91
+ # Common tasks
92
+
93
+ _cset :http_server, 'unicorn' # Valid options: 'unicorn', 'passenger'
94
+ require 'k3_capistrano/unicorn'
95
+
96
+ require 'k3_capistrano/ensure_db_is_set_up'
97
+ require 'k3_capistrano/db'
98
+ require 'k3_capistrano/db_backups'
99
+ require 'k3_capistrano/git'
100
+ require 'k3_capistrano/ssh'
101
+ require 'k3_capistrano/logs'
102
+ require 'k3_capistrano/asset_pipeline'
103
+ require 'k3_capistrano/test_results'
104
+
105
+ task :uname do
106
+ run "uname -a"
107
+ end
108
+
109
+ #===================================================================================================
110
+
111
+ def random_password
112
+ alphanumerics = [('0'..'9'),('A'..'Z'),('a'..'z')].map {|range| range.to_a}.flatten
113
+ (0...25).map { alphanumerics[Kernel.rand(alphanumerics.size)] }.join
114
+ end
115
+
116
+ def exec_verbose(command)
117
+ puts command
118
+ exec command
119
+ end
120
+
121
+ def system_verbose(command)
122
+ puts command
123
+ system command
124
+ end
125
+
126
+ def beep
127
+ STDOUT.print "#{7.chr}"
128
+ end
129
+
130
+ #===================================================================================================
131
+
132
+ end
@@ -0,0 +1,41 @@
1
+ # Chef integration
2
+ # Reuses as much configuration data about the app from the app data bag as possible, so that the
3
+ # app's configuration is only defined once, in one canonical location.
4
+
5
+ gem 'capistrano-ext'
6
+ gem 'tylerrick-chef'
7
+ gem 'capistrano-chef'
8
+
9
+ Capistrano::Configuration.instance(:must_exist).load do
10
+
11
+ require 'chef'
12
+ ENV['chef_home'] or abort("Please set the chef_home environment variable to point to your copy of K3's deployment/kitchen/")
13
+ Chef::Config[:data_bag_path] = ENV['chef_home'] + '/data_bags'
14
+ Chef::Config[:solo] = true
15
+ require 'capistrano/chef'
16
+ set_from_data_bag
17
+
18
+ # Note: If you require 'rvm/capistrano', make sure you do that *after* requiring
19
+ # 'k3_capistrano/chef' because the 'rvm' namespace that 'rvm/capistrano' creates would shadow the
20
+ # 'rvm' variables that set_from_data_bag would create.
21
+
22
+ # Some variables from the data bag have different options for staging/production, so we set the
23
+ # corresponding cap variables here but since we know whether we're on staging/production, we only
24
+ # grab the appropriate value for the server we're deploying to instead of the full hash...
25
+ set :rails_env_from_data_bag, rails_env; unset :rails_env
26
+ _cset :domain, ->{ server_name[stage.to_s] or server_name or raise("server_name should have been set from app data bag but was not defined") }
27
+ set :database, ->{ databases[stage.to_s] or raise("databases have been set from app data bag but was not defined") }
28
+ _cset :rails_env,->{ rails_env_from_data_bag[stage.to_s] or rails_env_from_data_bag or raise("rails_env have been set from app data bag but was not defined") }
29
+
30
+ # If we wanted to set the revision/branch from the data_bag as well, we could do something like this...
31
+ #set :revision_from_data_bag, revision; unset :revision
32
+ #set :revision, ->{ revision_from_data_bag[stage.to_s] or revision_from_data_bag or raise("revision was not defined") }
33
+
34
+ set :rvm_type, :system
35
+ rvm['ruby_string'] or fail("rvm['ruby_string'] was nil. Please make sure you aren't requiring 'rvm/capistrano' before 'k3/capistrano'. rvm=#{rvm.inspect}")
36
+ set :rvm_ruby_string, rvm['ruby_string']
37
+ set :user, owner
38
+
39
+ set :repository, "#{customer}@git.k3integrations.com:#{application}.git"
40
+
41
+ end
@@ -0,0 +1,72 @@
1
+ require 'tempfile'
2
+
3
+ Capistrano::Configuration.instance(:must_exist).load do
4
+
5
+ _cset :database, {
6
+ 'adapter' => "mysql2",
7
+ 'host' => "localhost",
8
+ 'database' => application,
9
+ 'username' => application,
10
+ 'reconnect' => "true",
11
+ 'encoding' => "utf8",
12
+ }
13
+
14
+ namespace :db do
15
+ # Adapted from deploy:migrate task in capistrano-2.12.0/lib/capistrano/recipes/deploy.rb
16
+ desc <<-DESC
17
+ Run the db:reset rake task. By default, it runs this in most recently \
18
+ deployed version of the app. However, you can specify a different release \
19
+ via the migrate_target variable, which must be one of :latest (for the \
20
+ default behavior), or :current (for the release indicated by the \
21
+ `current' symlink). Strings will work for those values instead of symbols, \
22
+ too. You can also specify additional environment variables to pass to rake \
23
+ via the migrate_env variable. Finally, you can specify the full path to the \
24
+ rake executable by setting the rake variable. The defaults are:
25
+
26
+ (Note: We should actually be using `rake db:setup` instead of `rake db:reset` but `rake db:setup`
27
+ currently fails with "db/schema.rb doesn't exist yet" error due to
28
+ https://github.com/rails/rails/issues/4772. The main difference is that `rake db:reset` does a drop
29
+ first.)
30
+
31
+
32
+ set :rake, "rake"
33
+ set :rails_env, "production"
34
+ set :migrate_env, ""
35
+ set :migrate_target, :latest
36
+ DESC
37
+ task :setup, :roles => :db, :only => { :primary => true } do
38
+ rake = fetch(:rake, "rake")
39
+ rails_env = fetch(:rails_env, "production")
40
+ migrate_env = fetch(:migrate_env, "")
41
+ migrate_target = fetch(:migrate_target, :latest)
42
+
43
+ directory = case migrate_target.to_sym
44
+ when :current then current_path
45
+ when :latest then latest_release
46
+ else raise ArgumentError, "unknown migration target #{migrate_target.inspect}"
47
+ end
48
+
49
+ run "cd #{directory} && #{rake} RAILS_ENV=#{rails_env} #{migrate_env} db:reset"
50
+ end
51
+
52
+ task :read_remote_my_cnf, :roles => :app do
53
+ path = "#{deploy_to}/.my.cnf"
54
+ raw = capture("cat #{path}")
55
+ config = nil
56
+ Tempfile.new('remote_my_cnf').tap do |file|
57
+ file.write raw
58
+ file.close
59
+ config = ParseConfig.new(file.path)
60
+ end
61
+ (config.params['client']['password'] rescue nil) or raise "Couldn't find password in #{path}"
62
+ set :my_cnf, config.params
63
+ end
64
+
65
+ task :list_tables, on_error: :continue do
66
+ tables = capture(%(echo "show tables" | mysql #{database['database']})).cleanlines
67
+ #puts %(tables=#{tables.to_a.inspect})
68
+ tables
69
+ end
70
+ end
71
+
72
+ end
@@ -0,0 +1,207 @@
1
+ # Database backup/restore
2
+
3
+ Capistrano::Configuration.instance(:must_exist).load do
4
+
5
+ _cset :db_backups_dir, ->{ "#{shared_path}/db_backups" }
6
+
7
+ # Lets us expand ~'s so sftp won't complain
8
+ set :home_dir, -> { capture('echo $HOME') }
9
+
10
+ namespace :backup do
11
+
12
+ desc "Back up the database"
13
+ task :create, :roles => :db, :only => { :primary => true }, on_error: :continue do
14
+ run "#{shared_path}/script/back_up_db"
15
+ backup.locate_last
16
+ end
17
+
18
+ task :list do
19
+ run("ls -t #{db_backups_dir}")
20
+ end
21
+ task :_list do
22
+ capture("ls -1t #{db_backups_dir}")
23
+ end
24
+
25
+ desc "Locate the latest backup"
26
+ task :locate_last do
27
+ last = "#{db_backups_dir}/" + backup._list.cleanlines.grep(/.*\.sql/).first
28
+ backup_details = capture("ls -alh #{last}")
29
+ puts ' '*6 + "Last backup is: #{last}"
30
+ puts ' '*6 + "#{backup_details}"
31
+ last
32
+ end
33
+
34
+ desc "Download the latest backup"
35
+ task :download do
36
+ set :which_backup, backup.locate_last unless exists?(:which_backup)
37
+ which_backup = capture("echo #{which_backup()}").chomp # expand ~'s so sftp won't complain
38
+ get which_backup, "#{local_dir}/#{File.basename(which_backup)}"
39
+ end
40
+
41
+ desc <<-End
42
+ Upload a database backup file to the remote host. Specify filename with file=.
43
+ End
44
+ task :upload_backup, :roles => :db do
45
+ file = ENV['file'] or raise(ArgumentError, "file must be specified")
46
+ upload file db_backups_dir.gsub('~', home_dir)
47
+ end
48
+
49
+ desc <<-End
50
+ Restore a database backup file.
51
+ The backup to restore must be specified by the which_backup variable and must already exist on the server.
52
+ End
53
+ task :restore, :roles => :db do
54
+ fetch(:which_backup) or raise(ArgumentError, "which_backup must be specified")
55
+ run "mysql #{application} < #{which_backup}"
56
+ end
57
+ end
58
+
59
+ #_cset :local_dir, ->{ "#{File.dirname(__FILE__)}/../" + "db/backups/production" }
60
+ #namespace :backup do
61
+ # # Tasks for downloading a database snapshot from production and loading into localhost DB.
62
+ # namespace :local do
63
+ #
64
+ # desc "Create a new backup/snapshot of production database and populate dev database with it."
65
+ # task :from_new_backup, :roles => :db do
66
+ # puts ' '*6 + 'Current path: ' + current_path
67
+ # backup.create
68
+ # local.from_latest_backup
69
+ # end
70
+ #
71
+ # desc "Populate the database with contents of your latest backup on the server."
72
+ # task :from_latest_backup, :roles => :db do
73
+ # set :which_backup, backup.locate_last
74
+ # from_backup
75
+ # end
76
+ #
77
+ # desc "Populate the database with contents of a backup (database dump)."
78
+ # task :from_backup, :roles => :db do
79
+ # local_backup_file = Dir["#{local_dir}/#{File.basename(which_backup)}"].to_a.first
80
+ # puts "Populating local database from '#{local_backup_file}'..."
81
+ # if local_backup_file
82
+ # # Note: rails db seems to ignore file input specified with <
83
+ # #command = "rails db -p < #{local_backup_file}"
84
+ # # so I just made a ./script/dbconsole file instead which invokes mysql client directly.
85
+ # command = "./script/dbconsole < #{local_backup_file}"
86
+ # puts command; system command
87
+ # else
88
+ # backup.download
89
+ # # Try again now (recursive) that we've downloaded the file
90
+ # local.from_backup
91
+ # end
92
+ # end
93
+ # end
94
+ #end
95
+
96
+ #---------------------------------------------------------------------------------------------------
97
+ # Taps (simple database import/export app)
98
+ _cset :local_rails_env, ->{ ENV['RAILS_ENV'] || 'development' }
99
+
100
+ =begin
101
+ Ideas for improvement:
102
+
103
+ Consider using https://github.com/hsume2/cap-taffy, which tries to solve the same problem.
104
+ They really do pretty much the same thing in a pretty similar way, but cap-taffy is a bit more advanced.
105
+
106
+ Advantages of cap-taffy:
107
+ * It starts a server and client with a single command (cap db:pull)
108
+
109
+ Advantages of current system:
110
+ * Zero-configuration. Reads all the connection info it needs from chef data bag, database.yml, etc.
111
+ cap-taffy appears to read database.yml for you, but does not parse it with eRuby and doesn't know about chef.
112
+ * uses bundle exec
113
+
114
+ Idea: Fork cap-taff. Refactor it to allow us to reuse most of it and simply pass connection string
115
+ (built from chef vars) and let cap-taffy handle the rest. It looks like we should be able to set
116
+ @remote_database_url somehow.
117
+
118
+ See https://github.com/hsume2/cap-taffy/blob/master/lib/cap-taffy/db.rb for the meat of cap-taffy.
119
+ =end
120
+ namespace :db do
121
+ desc <<-End
122
+ Start up a taps server on the server to let you pull the data from production database over to another host (such as localhost, for debugging with production data)
123
+
124
+ This uses your database config from your app's data bag item and password from .my.cnf to connect to the database.
125
+
126
+ You need to add 'taps' gem to your Gemfile and deploy your app first.
127
+
128
+ Then run:
129
+ [window 1] cap production db:taps_server
130
+ [window 2] cap production db:pull
131
+
132
+ You can also pull from one server to another:
133
+ [window 1] cap production db:taps_server
134
+ [window 2] cap staging db:pull_from_production
135
+
136
+ See https://github.com/ricardochimal/taps for more info.
137
+ End
138
+ task :taps_server, roles: :db do
139
+ # TODO: write pw to tmp/
140
+ http_user, http_password = application, random_password
141
+ File.open('tmp/taps_password', 'w') {|f| f.puts http_password }
142
+ run "cd #{current_path} && bundle exec taps server #{database['adapter']}://#{my_cnf['client']['user']}:#{my_cnf['client']['password']}@localhost/#{application}?encoding=#{database['encoding']} #{http_user} #{http_password}"
143
+ end
144
+
145
+ desc <<-End
146
+ Pull data from remote database to your local app database on localhost.
147
+
148
+ You'll need to start the taps server first with 'cap {stage} db:taps_server' and leave that running in another window.
149
+
150
+ This will connect to the host named by the domain variable (via an ssh tunnel since port 5000 is unlikely to be open), which depends on whether you run 'cap staging db:pull' or 'cap production db:pull'. This is where it will 'pull' the data from.
151
+
152
+ It will connect to your local app database using the connection info in your local config/database.yml. This is where the data gets pulled *to*.
153
+ End
154
+ task :pull do
155
+ http_user, http_password = application, ENV['password'] || File.read('tmp/taps_password').chomp
156
+ http_password.to_s.length > 1 or raise("could not read password")
157
+ ActiveRecord::Base.configurations = YAML::load(Erubis::Eruby.new(File.read('config/database.yml')).result)
158
+ configs = ActiveRecord::Base.configurations
159
+ database = configs[local_rails_env]
160
+ fork do
161
+ puts "Creating ssh tunnel..."
162
+ exec_verbose "ssh -N -L5000:127.0.0.1:5000 #{domain}"
163
+ end
164
+ system_verbose "bundle exec taps pull #{database['adapter']}://#{database['username']}:#{database['password']}@localhost/#{database['database']}?encoding=#{database['encoding']} " +
165
+ "http://#{http_user}:#{http_password}@localhost:5000"
166
+ puts "Done. Ctrl-C to exit ssh."
167
+ Process.wait
168
+ end
169
+
170
+ desc <<-End
171
+ Pull data from production database to staging database.
172
+
173
+ Usage: cap staging db:pull_to_staging
174
+ End
175
+ task :pull_to_staging do
176
+ production_domain = server_name['production']
177
+
178
+ http_user, http_password = application, ENV['password'] || File.read('tmp/taps_password').chomp
179
+ http_password.to_s.length > 1 or raise("could not read password")
180
+ remote_database_yml = capture("cat #{current_path}/config/database.yml")
181
+
182
+ ActiveRecord::Base.configurations = YAML::load(Erubis::Eruby.new(remote_database_yml).result)
183
+ configs = ActiveRecord::Base.configurations
184
+ database = configs[rails_env]
185
+
186
+ fork do
187
+ puts "Creating ssh tunnel..."
188
+ #run "ssh -N -L5000:127.0.0.1:5000 #{production_domain}"
189
+ exec_verbose "ssh #{domain} 'ssh -N -L5000:127.0.0.1:5000 #{production_domain}'"
190
+ # Note: If you get this error "Host key verification failed." then manually ssh to staging and
191
+ # from there to production (answer yes when prompted) so that the host key gets saved.
192
+ end
193
+ sleep 3 # TODO: read ssh output until it's ready
194
+ run "cd #{current_path} && bundle exec taps pull #{database['adapter']}://#{database['username']}:#{database['password']}@localhost/#{database['database']}?encoding=#{database['encoding']} " +
195
+ "http://#{http_user}:#{http_password}@localhost:5000"
196
+ puts "Done. Ctrl-C to exit ssh."
197
+ Process.wait
198
+ # For some reason the ssh processes seem to stick around, so just to make sure we clean up,
199
+ # let's try to kill them.
200
+ run 'killall ssh'
201
+ end
202
+ end
203
+
204
+ before 'db:taps_server', 'db:read_remote_my_cnf'
205
+ before 'deploy:migrate', 'backup:create'
206
+
207
+ end