engineyard-migrate 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,10 @@
1
+
2
+ module Matchers
3
+ RSpec::Matchers.define :contain do |expected_text|
4
+ match do |text|
5
+ text.index expected_text
6
+ end
7
+ end
8
+ end
9
+
10
+ World(Matchers)
@@ -0,0 +1,6 @@
1
+ module Web
2
+ def response_body
3
+ @response_body
4
+ end
5
+ end
6
+ World(Web)
@@ -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,4 @@
1
+ module Engineyard
2
+ module Migrate
3
+ end
4
+ end
@@ -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