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.
@@ -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