engineyard-migrate 1.0.0
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.
- data/.gitignore +7 -0
- data/.rvmrc +47 -0
- data/ChangeLog.md +10 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +61 -0
- data/Rakefile +54 -0
- data/bin/ey-migrate +10 -0
- data/docs/migration-options.graffle/QuickLook/Preview.pdf +0 -0
- data/docs/migration-options.graffle/QuickLook/Thumbnail.tiff +0 -0
- data/docs/migration-options.graffle/data.plist +351 -0
- data/docs/migration-options.graffle/image1.tiff +0 -0
- data/docs/migration-options.graffle/image2.tiff +0 -0
- data/engineyard-migrate.gemspec +37 -0
- data/features/heroku.feature +36 -0
- data/features/migration_errors.feature +107 -0
- data/features/step_definitions/application_setup_steps.rb +102 -0
- data/features/step_definitions/common_steps.rb +208 -0
- data/features/step_definitions/web_steps.rb +15 -0
- data/features/support/appcloud_restore_folders.rb +26 -0
- data/features/support/common.rb +55 -0
- data/features/support/env.rb +24 -0
- data/features/support/matchers.rb +10 -0
- data/features/support/web.rb +6 -0
- data/fixtures/data/simple-app.sqlite3 +0 -0
- data/heroku-todo.md +100 -0
- data/lib/engineyard-migrate.rb +4 -0
- data/lib/engineyard-migrate/cli.rb +212 -0
- data/lib/engineyard-migrate/version.rb +5 -0
- data/spec/spec_helper.rb +4 -0
- metadata +327 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
When /^I visit the application at "([^"]*)"$/ do |host|
|
2
|
+
Net::HTTP.start(host) do |http|
|
3
|
+
req = http.get("/")
|
4
|
+
@response_body = req.body
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
Then /^I should see table$/ do |table|
|
9
|
+
doc_table = tableish('table#people tr', 'td,th')
|
10
|
+
doc_table.should == table.raw
|
11
|
+
end
|
12
|
+
|
13
|
+
Then /^port "([^"]*)" on "([^"]*)" should be closed$/ do |port, host|
|
14
|
+
pending
|
15
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module AppcloudRestoreFolder
|
2
|
+
# In scenarios like 'I remove AppCloud application "my_app_name" folder'
|
3
|
+
# a folder is removed from AppCloud; but it is required for all other scenarios
|
4
|
+
# unless explicitly deleted
|
5
|
+
# This helper ensures that the folder is restored
|
6
|
+
def remove_from_appcloud(path, environment)
|
7
|
+
@stdout = File.expand_path(File.join(@tmp_root, "eyssh.remove.out"))
|
8
|
+
@stderr = File.expand_path(File.join(@tmp_root, "eyssh.remove.err"))
|
9
|
+
path = path.gsub(%r{/$}, '')
|
10
|
+
path_tmp = "#{path}.tmp"
|
11
|
+
@restore_paths ||= []
|
12
|
+
@restore_paths << [path_tmp, path, environment]
|
13
|
+
cmd = "mv #{Escape.shell_command(path)} #{Escape.shell_command(path_tmp)}"
|
14
|
+
system "ey ssh #{Escape.shell_command(cmd)} -e #{environment} > #{@stdout.inspect} 2> #{@stderr.inspect}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
World(AppcloudRestoreFolder)
|
18
|
+
|
19
|
+
After do
|
20
|
+
if @restore_paths
|
21
|
+
@restore_paths.each do |path_tmp, path, environment|
|
22
|
+
cmd = "mv #{Escape.shell_command(path_tmp)} #{Escape.shell_command(path)}"
|
23
|
+
system "ey ssh #{Escape.shell_command(cmd)} -e #{environment} > #{@stdout.inspect} 2> #{@stderr.inspect}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module CommonHelpers
|
2
|
+
def get_command_output
|
3
|
+
strip_color_codes(File.read(@stdout)).chomp
|
4
|
+
end
|
5
|
+
|
6
|
+
def strip_color_codes(text)
|
7
|
+
text.gsub(/\e\[\d+m/, '')
|
8
|
+
end
|
9
|
+
|
10
|
+
def in_tmp_folder(&block)
|
11
|
+
FileUtils.chdir(@tmp_root, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def in_project_folder(&block)
|
15
|
+
project_folder = @active_project_folder || @tmp_root
|
16
|
+
FileUtils.chdir(project_folder, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def in_mock_world_path(&block)
|
20
|
+
FileUtils.chdir(@mock_world_path, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def in_home_folder(&block)
|
24
|
+
FileUtils.chdir(@home_path, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def force_local_lib_override(project_name = @project_name)
|
28
|
+
rakefile = File.read(File.join(project_name, 'Rakefile'))
|
29
|
+
File.open(File.join(project_name, 'Rakefile'), "w+") do |f|
|
30
|
+
f << "$:.unshift('#{@lib_path}')\n"
|
31
|
+
f << rakefile
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup_active_project_folder project_name
|
36
|
+
@active_project_folder = File.join(@tmp_root, project_name)
|
37
|
+
@project_name = project_name
|
38
|
+
end
|
39
|
+
|
40
|
+
# capture both [stdout, stderr] as well as stdin
|
41
|
+
def capture_stdios(input = nil, &block)
|
42
|
+
require 'stringio'
|
43
|
+
org_stdin, $stdin = $stdin, StringIO.new(input) if input
|
44
|
+
org_stdout, $stdout = $stdout, StringIO.new
|
45
|
+
org_stderr, $stderr = $stdout, StringIO.new
|
46
|
+
yield
|
47
|
+
return [$stdout.string, $stderr.string]
|
48
|
+
ensure
|
49
|
+
$stderr = org_stderr
|
50
|
+
$stdout = org_stdout
|
51
|
+
$stdin = org_stdin
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
World(CommonHelpers)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
$:.unshift(File.expand_path(File.dirname(__FILE__) + '/../../lib'))
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'net/http'
|
4
|
+
require 'escape'
|
5
|
+
|
6
|
+
require 'cucumber/web/tableish'
|
7
|
+
|
8
|
+
[$stdout, $stderr].each { |pipe| pipe.sync = true }
|
9
|
+
|
10
|
+
Before do
|
11
|
+
@tmp_root = File.dirname(__FILE__) + "/../../tmp"
|
12
|
+
@active_project_folder = @tmp_root
|
13
|
+
@home_path = File.expand_path(File.join(@tmp_root, "home"))
|
14
|
+
@lib_path = File.expand_path(File.dirname(__FILE__) + "/../../lib")
|
15
|
+
@fixtures_path = File.expand_path(File.dirname(__FILE__) + "/../../fixtures")
|
16
|
+
@mock_world_path = File.expand_path(File.dirname(__FILE__) + "/../../fixtures/mock_world")
|
17
|
+
|
18
|
+
@repos_path = File.expand_path(File.dirname(__FILE__) + "/../../fixtures/repos")
|
19
|
+
FileUtils.mkdir_p @repos_path
|
20
|
+
|
21
|
+
FileUtils.rm_rf @tmp_root
|
22
|
+
FileUtils.mkdir_p @home_path
|
23
|
+
ENV['HOME'] = @home_path
|
24
|
+
end
|
Binary file
|
data/heroku-todo.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
### Custom domains
|
2
|
+
|
3
|
+
Example:
|
4
|
+
|
5
|
+
custom_domains:basic, wildcard
|
6
|
+
|
7
|
+
There are no restrictions on domains associated with your AppCloud account.
|
8
|
+
|
9
|
+
### Cron [todo]
|
10
|
+
|
11
|
+
Examples:
|
12
|
+
|
13
|
+
heroku addons:add cron:daily
|
14
|
+
heroku addons:add cron:hourly
|
15
|
+
|
16
|
+
Heroku's `cron` addon ran your `rake cron` task, either daily or hourly.
|
17
|
+
|
18
|
+
A corresponding cron job will be created for you on AppCloud:
|
19
|
+
|
20
|
+
cd /data/appname/current && RAILS_ENV=production rake cron
|
21
|
+
|
22
|
+
### Logging
|
23
|
+
|
24
|
+
Example:
|
25
|
+
|
26
|
+
logging:advanced, basic, expanded
|
27
|
+
|
28
|
+
AppCloud implements its own logging system.
|
29
|
+
|
30
|
+
### Memcached
|
31
|
+
|
32
|
+
Example:
|
33
|
+
|
34
|
+
memcache:100mb, 10gb, 1gb, 250mb, 50gb...
|
35
|
+
|
36
|
+
AppCloud applications automatically have memcached enabled.
|
37
|
+
|
38
|
+
### New Relic
|
39
|
+
|
40
|
+
Example:
|
41
|
+
|
42
|
+
newrelic:bronze, gold, silver
|
43
|
+
|
44
|
+
You can enable New Relic for your AppCloud account through the https://cloud.engineyard.com dashboard.
|
45
|
+
|
46
|
+
### Release management
|
47
|
+
|
48
|
+
Example:
|
49
|
+
|
50
|
+
releases:basic, advanced
|
51
|
+
|
52
|
+
AppCloud implements its release management system.
|
53
|
+
|
54
|
+
### SSL
|
55
|
+
|
56
|
+
Example:
|
57
|
+
|
58
|
+
ssl:hostname, ip, piggyback, sni
|
59
|
+
|
60
|
+
There is no cost for installing SSL for your AppCloud application through the https://cloud.engineyard.com dashboard.
|
61
|
+
|
62
|
+
### Other addons
|
63
|
+
|
64
|
+
The remaining known Heroku addons are:
|
65
|
+
|
66
|
+
amazon_rds
|
67
|
+
apigee:basic
|
68
|
+
apigee_facebook:basic
|
69
|
+
bundles:single, unlimited
|
70
|
+
cloudant:argon, helium, krypton...
|
71
|
+
cloudmailin:test
|
72
|
+
custom_error_pages
|
73
|
+
deployhooks:basecamp, campfire...
|
74
|
+
exceptional:basic, premium
|
75
|
+
heroku-postgresql:baku, fugu, ika...
|
76
|
+
hoptoad:basic, plus
|
77
|
+
indextank:plus, premium, pro...
|
78
|
+
mongohq:free, large, micro, small
|
79
|
+
moonshadosms:basic, free, max, plus...
|
80
|
+
pandastream:duo, quad, sandbox, solo
|
81
|
+
pgbackups:basic, plus
|
82
|
+
pusher:test
|
83
|
+
redistogo:large, medium, mini, nano...
|
84
|
+
sendgrid:free, premium, pro
|
85
|
+
websolr:gold, platinum, silver...
|
86
|
+
zencoder:100k, 10k, 1k, 20k, 2k, 40k, 4k...
|
87
|
+
zerigo_dns:basic, tier1, tier2
|
88
|
+
|
89
|
+
--- beta ---
|
90
|
+
chargify:test
|
91
|
+
docraptor:test
|
92
|
+
heroku-postgresql:...
|
93
|
+
jasondb:test
|
94
|
+
memcached:basic
|
95
|
+
pgbackups:daily, hourly
|
96
|
+
recurly:test
|
97
|
+
releases:advanced
|
98
|
+
ticketly:test
|
99
|
+
|
100
|
+
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'uri'
|
3
|
+
require 'net/http'
|
4
|
+
require 'net/sftp'
|
5
|
+
require 'POpen4'
|
6
|
+
require 'engineyard/thor'
|
7
|
+
require "engineyard/cli"
|
8
|
+
require "engineyard/cli/ui"
|
9
|
+
require "engineyard/error"
|
10
|
+
|
11
|
+
module Engineyard::Migrate
|
12
|
+
class CLI < Thor
|
13
|
+
include EY::UtilityMethods
|
14
|
+
attr_reader :verbose
|
15
|
+
|
16
|
+
desc "heroku PATH", "Migrate this Heroku app to Engine Yard AppCloud"
|
17
|
+
method_option :verbose, :aliases => ["-V"], :desc => "Display more output"
|
18
|
+
method_option :environment, :aliases => ["-e"], :desc => "Environment in which to deploy this application", :type => :string
|
19
|
+
method_option :account, :aliases => ["-c"], :desc => "Name of the account you want to deploy in"
|
20
|
+
def heroku(path)
|
21
|
+
@verbose = options[:verbose]
|
22
|
+
error "Path '#{path}' does not exist" unless File.exists? path
|
23
|
+
FileUtils.chdir(path) do
|
24
|
+
begin
|
25
|
+
heroku_repo = `git config remote.heroku.url`.strip
|
26
|
+
if heroku_repo.empty?
|
27
|
+
error "Not a Salesforce Heroku application."
|
28
|
+
end
|
29
|
+
heroku_repo =~ /git@heroku\.com:(.*)\.git/
|
30
|
+
heroku_app_name = $1
|
31
|
+
|
32
|
+
say "Requesting Heroku account information..."; $stdout.flush
|
33
|
+
say "Heroku app: "; say heroku_app_name, :green
|
34
|
+
|
35
|
+
heroku_credentials = File.expand_path("~/.heroku/credentials")
|
36
|
+
unless File.exists?(heroku_credentials)
|
37
|
+
error "Please setup your Salesforce Heroku credentials first."
|
38
|
+
end
|
39
|
+
|
40
|
+
say `heroku info`
|
41
|
+
say ""
|
42
|
+
|
43
|
+
repo = `git config remote.origin.url`.strip
|
44
|
+
if repo.empty?
|
45
|
+
error "Please host your Git repo externally and add as remote 'origin'.", <<-SUGGESTION.gsub(/^\s{12}/, '')
|
46
|
+
You can create a GitHub repository using 'github' gem:
|
47
|
+
$ gem install github
|
48
|
+
$ gh create-from-local --private
|
49
|
+
SUGGESTION
|
50
|
+
end
|
51
|
+
unless EY::API.read_token
|
52
|
+
error "Please create, boot and deploy an AppCloud application for #{repo}."
|
53
|
+
end
|
54
|
+
|
55
|
+
say "Requesting AppCloud account information..."; $stdout.flush
|
56
|
+
@app, @environment = fetch_app_and_environment(options[:app], options[:environment], options[:account])
|
57
|
+
|
58
|
+
unless @app.repository_uri == repo
|
59
|
+
error "Please create, boot and deploy an AppCloud application for #{repo}."
|
60
|
+
end
|
61
|
+
unless @environment.app_master
|
62
|
+
error "Please boot your AppCloud environment and then deploy your application."
|
63
|
+
end
|
64
|
+
|
65
|
+
@app.name = @app.name
|
66
|
+
app_master_host = @environment.app_master.public_hostname
|
67
|
+
app_master_user = @environment.username
|
68
|
+
|
69
|
+
say "Application: "; say "#{@app.name}", :green
|
70
|
+
say "Account: "; say "#{@environment.account.name}", :green
|
71
|
+
say "Environment: "; say "#{@environment.name}", :green
|
72
|
+
say "Cluster size: "; say "#{@environment.instances_count}"
|
73
|
+
say "Hostname: "; say "#{app_master_host}"
|
74
|
+
debug "$RACK_ENV: "; debug "#{@environment.framework_env}"
|
75
|
+
say ""
|
76
|
+
|
77
|
+
# TODO - what if no application deployed yet?
|
78
|
+
# bash: line 0: cd: /data/heroku2eysimpleapp/current: No such file or directory
|
79
|
+
|
80
|
+
# TODO - to test for cron setup:
|
81
|
+
# dna_env["cron"] - list of:
|
82
|
+
# [0] {
|
83
|
+
# "minute" => "0",
|
84
|
+
# "name" => "rake cron",
|
85
|
+
# "command" => "cd /data/heroku2eysimpleapp/current && RAILS_ENV=production rake cron",
|
86
|
+
# "month" => "*",
|
87
|
+
# "hour" => "1",
|
88
|
+
# "day" => "*/1",
|
89
|
+
# "user" => "deploy",
|
90
|
+
# "weekday" => "*"
|
91
|
+
# }
|
92
|
+
|
93
|
+
say "Testing AppCloud application status..."
|
94
|
+
|
95
|
+
deploy_path_found = ssh_appcloud "test -d #{@app.name}/current && echo 'found'",
|
96
|
+
:path => '/data', :return_output => true
|
97
|
+
error "Please deploy your AppCloud application before running migration." unless deploy_path_found =~ /found/
|
98
|
+
|
99
|
+
say "Setting up Heroku on AppCloud..."
|
100
|
+
|
101
|
+
ssh_appcloud "sudo gem install heroku taps --no-ri --no-rdoc -q"
|
102
|
+
ssh_appcloud "git remote rm heroku 2> /dev/null; git remote add heroku #{heroku_repo} 2> /dev/null"
|
103
|
+
|
104
|
+
say "Uploading Heroku credential file..."
|
105
|
+
home_path = ssh_appcloud("pwd", :path => "~", :return_output => true)
|
106
|
+
debug "AppCloud $HOME: "; debug home_path, :yellow
|
107
|
+
ssh_appcloud "mkdir -p .heroku; chmod 700 .heroku", :path => home_path
|
108
|
+
|
109
|
+
Net::SFTP.start(app_master_host, app_master_user) do |sftp|
|
110
|
+
sftp.upload!(heroku_credentials, "#{home_path}/.heroku/credentials")
|
111
|
+
end
|
112
|
+
say ""
|
113
|
+
|
114
|
+
say "Migrating data from Heroku '#{heroku_app_name}' to AppCloud '#{@app.name}'..."
|
115
|
+
env_vars = %w[RAILS_ENV RACK_ENV MERB_ENV].map {|var| "#{var}=#{@environment.framework_env}" }.join(" ")
|
116
|
+
ssh_appcloud "#{env_vars} heroku db:pull --confirm #{heroku_app_name} 2>&1"
|
117
|
+
say ""
|
118
|
+
|
119
|
+
say "Migration complete!", :green
|
120
|
+
rescue SystemExit
|
121
|
+
rescue EY::MultipleMatchesError => e
|
122
|
+
envs = []
|
123
|
+
e.message.split(/\n/).map do |line|
|
124
|
+
env = {}
|
125
|
+
line.scan(/--([^=]+)='([^']+)'/) do
|
126
|
+
env[$1] = $2
|
127
|
+
end
|
128
|
+
envs << env unless env.empty?
|
129
|
+
end
|
130
|
+
too_many_environments_discovered 'heroku', envs, path
|
131
|
+
rescue Net::SSH::AuthenticationFailed => e
|
132
|
+
error "Please setup your SSH credentials for AppCloud."
|
133
|
+
rescue Net::SFTP::StatusException => e
|
134
|
+
error e.description + ": " + e.text
|
135
|
+
rescue Exception => e
|
136
|
+
say "Migration failed", :red
|
137
|
+
puts e.inspect
|
138
|
+
puts e.backtrace
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
map "-v" => :version, "--version" => :version, "-h" => :help, "--help" => :help
|
144
|
+
|
145
|
+
private
|
146
|
+
def ssh_appcloud(cmd, options = {})
|
147
|
+
path = options[:path] || "/data/#{@app.name}/current/"
|
148
|
+
flags = " #{options[:flags]}" || "" if options[:flags] # app master by default
|
149
|
+
full_cmd = "cd #{path}; #{cmd}"
|
150
|
+
ssh_cmd = "ey ssh #{Escape.shell_command(full_cmd)}#{flags} -e #{@environment.name} -c #{@environment.account.name}"
|
151
|
+
debug options[:return_output] ? "Capturing: " : "Running: "
|
152
|
+
debug ssh_cmd, :yellow; $stdout.flush
|
153
|
+
out = ""
|
154
|
+
status =
|
155
|
+
POpen4::popen4(ssh_cmd) do |stdout, stderr, stdin, pid|
|
156
|
+
if options[:return_output]
|
157
|
+
out += stdout.read.strip
|
158
|
+
err = stderr.read.strip; say err unless err.empty?
|
159
|
+
else
|
160
|
+
while line = stdout.gets("\n") || stderr.gets("\n")
|
161
|
+
say line
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
puts "exitstatus : #{ status.exitstatus }" unless status.exitstatus == 0
|
167
|
+
out if options[:return_output]
|
168
|
+
end
|
169
|
+
|
170
|
+
def say(msg, color = nil)
|
171
|
+
color ? shell.say(msg, color) : shell.say(msg)
|
172
|
+
end
|
173
|
+
|
174
|
+
def debug(msg, color = nil)
|
175
|
+
say(msg, color) if verbose
|
176
|
+
end
|
177
|
+
|
178
|
+
def display(text)
|
179
|
+
shell.say text
|
180
|
+
exit
|
181
|
+
end
|
182
|
+
|
183
|
+
def error(text, suggestion = nil)
|
184
|
+
shell.say "ERROR: #{text}", :red
|
185
|
+
if suggestion
|
186
|
+
shell.say ""
|
187
|
+
shell.say suggestion
|
188
|
+
end
|
189
|
+
exit
|
190
|
+
end
|
191
|
+
|
192
|
+
# TODO - not being used yet
|
193
|
+
def no_environments_discovered
|
194
|
+
say "No AppCloud environments found for this application.", :red
|
195
|
+
say "Either:"
|
196
|
+
say " * Create an AppCloud environment for this application/git URL"
|
197
|
+
say " * Use --environment/--account flags to select an AppCloud environment"
|
198
|
+
end
|
199
|
+
|
200
|
+
def too_many_environments_discovered(task, environments, *args)
|
201
|
+
return no_environments_discovered if environments.empty?
|
202
|
+
say "Multiple environments possible, please be more specific:", :red
|
203
|
+
say ""
|
204
|
+
environments.each do |env|
|
205
|
+
flags = env.map { |key, value| "--#{key}='#{value}'"}.join(" ")
|
206
|
+
say " ey-migrate #{task} #{args.join(' ')} #{flags}"
|
207
|
+
end
|
208
|
+
exit 1
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|