├── .gitignore ├── ChangeLog.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bin └── ey-migrate ├── docs └── migration-options.graffle │ ├── QuickLook │ ├── Preview.pdf │ └── Thumbnail.tiff │ ├── data.plist │ ├── image1.tiff │ └── image2.tiff ├── engineyard-migrate.gemspec ├── features ├── heroku.feature ├── migration_errors.feature ├── step_definitions │ ├── application_setup_steps.rb │ ├── common_steps.rb │ └── web_steps.rb └── support │ ├── appcloud_restore_folders.rb │ ├── common.rb │ ├── env.rb │ ├── matchers.rb │ └── web.rb ├── fixtures └── data │ └── simple-app.sqlite3 ├── heroku-todo.md ├── lib ├── engineyard-migrate.rb └── engineyard-migrate │ ├── cli.rb │ └── version.rb └── spec └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | tmp 6 | fixtures/repos 7 | fixtures/credentials -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.1.0 2012-04-30 4 | 5 | Heroku 6 | 7 | * Credentials now at ~/.netrc or ~/_netrc 8 | * Taps needs sqlite3 installed 9 | 10 | General: 11 | 12 | * A log of changes. But prettier. 13 | 14 | ## 1.0.1 2011-03-11 15 | 16 | Remove version dependencies on engineyard + heroku gems [fixes #4] 17 | 18 | ## 1.0.0 2011-03-11 19 | 20 | Initial release! 21 | 22 | * Migrate database from Heroku application to a running AppCloud application 23 | * Expansive integration test suite 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in engineyard-migrate.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Engine Yard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOa AND 17 | NONINFRINGEMENT. IN NO EVENT SaALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Migrate your Rails application to Engine Yard AppCloud 2 | 3 | Want to migrate your Ruby on Rails application from Heroku (or similar) up to Engine Yard AppCloud? This is the tool for you. 4 | 5 | Currently supported: **Migrate Heroku to AppCloud**. 6 | 7 | 8 | 9 | ## Installation 10 | 11 | Currently, it is only installable from source. 12 | 13 | bundle 14 | rake install 15 | 16 | ## Usage 17 | 18 | The tool is simple to use. If you need to do something, it will tell you. 19 | 20 | ey-migrate heroku path/to/heroku/app 21 | 22 | ## Migration from Salesforce Heroku 23 | 24 | The migration tool assumes you have: 25 | 26 | * A running Heroku application with your data in its SQL database 27 | * A Gemfile, rather than Heroku's deprecated .gems format 28 | * Added `mysql2` to your Gemfile 29 | * This upgraded application running on AppCloud without any of your data 30 | 31 | ### Database 32 | 33 | Your SQL database is automatically migrated to your AppCloud application via `ey-migrate heroku`. 34 | 35 | A MySQL database is created automatically for you for each AppCloud application. On a 1 instance environment it runs on the same instances as your web application. For dedicated databases, use a 2+ instance environment with a dedicated database instance. 36 | 37 | ### Workers 38 | 39 | Automated support for setting up delayed_job workers is coming. 40 | 41 | ### Other add-ons 42 | 43 | If you have specific Heroku Add-Ons you'd like to be automatically migrated to AppCloud, please leave a [note/request](https://github.com/engineyard/engineyard-migrate). 44 | 45 | ## Development of project 46 | 47 | ### Running tests 48 | 49 | Then to run tests: 50 | 51 | bundle 52 | rake 53 | 54 | This will install `.ssh/config` required for your SSH credentials to run the test suite. 55 | 56 | ### Credentials 57 | 58 | To run the integration tests, you either need access to the [credentials repository](https://github.com/engineyard/ey-migrate-test-credentials) 59 | 60 | Please send a Github message to `drnic` requesting access to the credentials. You'll then be able to run the test suite. 61 | 62 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | 6 | desc "Run all examples" 7 | RSpec::Core::RakeTask.new 8 | 9 | namespace :cucumber do 10 | require 'cucumber/rake/task' 11 | Cucumber::Rake::Task.new(:wip, 'Run features that are being worked on') do |t| 12 | t.cucumber_opts = "--tags @wip" 13 | end 14 | Cucumber::Rake::Task.new(:ok, 'Run features that should be working') do |t| 15 | t.cucumber_opts = "--tags ~@wip" 16 | end 17 | task :all => [:ok, :wip] 18 | 19 | desc "Download credentials" 20 | task :download_credentials do 21 | credentials = File.expand_path('../fixtures/credentials', __FILE__) 22 | unless File.exists?(credentials) 23 | sh "git clone git@github.com:engineyard/ey-migrate-test-credentials.git #{credentials}" 24 | end 25 | end 26 | 27 | desc "Setup IdentityFile for SSH keys for running tests" 28 | task :ssh_config do 29 | puts "Installing SSH credentials for running integration tests..." 30 | config_file = File.expand_path("~#{ENV['USER']}/.ssh/config") 31 | identity_file = File.expand_path("../tmp/home/.ssh/id_rsa", __FILE__) 32 | if File.exist? config_file 33 | sh "ssh-config set ec2-50-17-248-148.compute-1.amazonaws.com IdentityFile #{identity_file}" 34 | else 35 | File.open(config_file, 'w') 36 | sh "ssh-config set ec2-50-17-248-148.compute-1.amazonaws.com IdentityFile #{identity_file}" 37 | File.delete(config_file + '~') 38 | end 39 | end 40 | end 41 | 42 | desc 'Alias for cucumber:ok' 43 | task :cucumber => ['cucumber:download_credentials', 'cucumber:ssh_config', 'cucumber:ok'] 44 | 45 | desc "Start test server; Run cucumber:ok; Kill Test Server;" 46 | task :default => ["spec", "cucumber"] 47 | 48 | desc "Clean out cached git app repos" 49 | task :clean_app_repos do 50 | repos_path = File.dirname(__FILE__) + "/fixtures/repos" 51 | FileUtils.rm_rf(repos_path) 52 | puts "Removed #{repos_path}..." 53 | end 54 | 55 | -------------------------------------------------------------------------------- /bin/ey-migrate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | $:.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib")) 5 | 6 | require 'engineyard-migrate' 7 | require 'engineyard-migrate/cli' 8 | 9 | $stdout.sync = true 10 | 11 | Engineyard::Migrate::CLI.start 12 | -------------------------------------------------------------------------------- /docs/migration-options.graffle/QuickLook/Preview.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineyard/engineyard-migrate/77f7a6fb695f88eb8325cee51248f865474b2a0c/docs/migration-options.graffle/QuickLook/Preview.pdf -------------------------------------------------------------------------------- /docs/migration-options.graffle/QuickLook/Thumbnail.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineyard/engineyard-migrate/77f7a6fb695f88eb8325cee51248f865474b2a0c/docs/migration-options.graffle/QuickLook/Thumbnail.tiff -------------------------------------------------------------------------------- /docs/migration-options.graffle/data.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ActiveLayerIndex 6 | 0 7 | ApplicationVersion 8 | 9 | com.omnigroup.OmniGrafflePro 10 | 138.17.0.133677 11 | 12 | AutoAdjust 13 | 14 | BackgroundGraphic 15 | 16 | Bounds 17 | {{0, 0}, {576, 733}} 18 | Class 19 | SolidGraphic 20 | ID 21 | 2 22 | Style 23 | 24 | shadow 25 | 26 | Draws 27 | NO 28 | 29 | stroke 30 | 31 | Draws 32 | NO 33 | 34 | 35 | 36 | CanvasOrigin 37 | {0, 0} 38 | ColumnAlign 39 | 1 40 | ColumnSpacing 41 | 36 42 | CreationDate 43 | 2011-03-11 13:52:41 -0800 44 | Creator 45 | Dr Nic Williams 46 | DisplayScale 47 | 1 0/72 in = 1 0/72 in 48 | GraphDocumentVersion 49 | 6 50 | GraphicsList 51 | 52 | 53 | Bounds 54 | {{221.5, 62.5}, {74, 40}} 55 | Class 56 | ShapedGraphic 57 | ID 58 | 29 59 | Rotation 60 | 5.9071908253827132e-06 61 | Shape 62 | AdjustableArrow 63 | ShapeData 64 | 65 | ratio 66 | 0.50000017881393433 67 | width 68 | 20.000001907348633 69 | 70 | Style 71 | 72 | fill 73 | 74 | Color 75 | 76 | b 77 | 0.696626 78 | g 79 | 0.354368 80 | r 81 | 0.112333 82 | 83 | FillType 84 | 2 85 | GradientAngle 86 | 355 87 | GradientColor 88 | 89 | b 90 | 0.829482 91 | g 92 | 0.541172 93 | r 94 | 0.272927 95 | 96 | MiddleFraction 97 | 0.4523809552192688 98 | 99 | shadow 100 | 101 | Color 102 | 103 | a 104 | 0.4 105 | b 106 | 0 107 | g 108 | 0 109 | r 110 | 0 111 | 112 | ShadowVector 113 | {0, 2} 114 | 115 | stroke 116 | 117 | Color 118 | 119 | b 120 | 0.794995 121 | g 122 | 0.459724 123 | r 124 | 0 125 | 126 | 127 | 128 | TextRelativeArea 129 | {{0.125, 0.25}, {0.75, 0.5}} 130 | isConnectedShape 131 | 132 | 133 | 134 | Bounds 135 | {{311, 29}, {83, 107}} 136 | Class 137 | ShapedGraphic 138 | ID 139 | 6 140 | ImageID 141 | 2 142 | Shape 143 | Rectangle 144 | Style 145 | 146 | fill 147 | 148 | Draws 149 | NO 150 | 151 | shadow 152 | 153 | Draws 154 | NO 155 | 156 | stroke 157 | 158 | Draws 159 | NO 160 | 161 | 162 | 163 | 164 | Bounds 165 | {{41, 55}, {165, 55}} 166 | Class 167 | ShapedGraphic 168 | ID 169 | 4 170 | ImageID 171 | 1 172 | Shape 173 | Rectangle 174 | Style 175 | 176 | fill 177 | 178 | Draws 179 | NO 180 | 181 | shadow 182 | 183 | Draws 184 | NO 185 | 186 | stroke 187 | 188 | Draws 189 | NO 190 | 191 | 192 | 193 | 194 | GridInfo 195 | 196 | GuidesLocked 197 | NO 198 | GuidesVisible 199 | YES 200 | HPages 201 | 1 202 | ImageCounter 203 | 3 204 | ImageLinkBack 205 | 206 | 207 | 208 | 209 | ImageList 210 | 211 | image2.tiff 212 | image1.tiff 213 | 214 | KeepToScale 215 | 216 | Layers 217 | 218 | 219 | Lock 220 | NO 221 | Name 222 | Layer 1 223 | Print 224 | YES 225 | View 226 | YES 227 | 228 | 229 | LayoutInfo 230 | 231 | Animate 232 | NO 233 | circoMinDist 234 | 18 235 | circoSeparation 236 | 0.0 237 | layoutEngine 238 | dot 239 | neatoSeparation 240 | 0.0 241 | twopiSeparation 242 | 0.0 243 | 244 | LinksVisible 245 | NO 246 | MagnetsVisible 247 | NO 248 | MasterSheets 249 | 250 | ModificationDate 251 | 2011-03-11 13:57:10 -0800 252 | Modifier 253 | Dr Nic Williams 254 | NotesVisible 255 | NO 256 | Orientation 257 | 2 258 | OriginVisible 259 | NO 260 | PageBreaks 261 | YES 262 | PrintInfo 263 | 264 | NSBottomMargin 265 | 266 | float 267 | 41 268 | 269 | NSLeftMargin 270 | 271 | float 272 | 18 273 | 274 | NSPaperSize 275 | 276 | size 277 | {612, 792} 278 | 279 | NSRightMargin 280 | 281 | float 282 | 18 283 | 284 | NSTopMargin 285 | 286 | float 287 | 18 288 | 289 | 290 | PrintOnePage 291 | 292 | ReadOnly 293 | NO 294 | RowAlign 295 | 1 296 | RowSpacing 297 | 36 298 | SheetTitle 299 | Canvas 1 300 | SmartAlignmentGuidesActive 301 | YES 302 | SmartDistanceGuidesActive 303 | YES 304 | UniqueID 305 | 1 306 | UseEntirePage 307 | 308 | VPages 309 | 1 310 | WindowInfo 311 | 312 | CurrentSheet 313 | 0 314 | ExpandedCanvases 315 | 316 | 317 | name 318 | Canvas 1 319 | 320 | 321 | Frame 322 | {{43, 185}, {916, 591}} 323 | ListView 324 | 325 | OutlineWidth 326 | 142 327 | RightSidebar 328 | 329 | ShowRuler 330 | 331 | Sidebar 332 | 333 | SidebarWidth 334 | 120 335 | VisibleRegion 336 | {{-95, 0}, {767, 437}} 337 | Zoom 338 | 1 339 | ZoomValues 340 | 341 | 342 | Canvas 1 343 | 1 344 | 1 345 | 346 | 347 | 348 | saveQuickLookFiles 349 | YES 350 | 351 | 352 | -------------------------------------------------------------------------------- /docs/migration-options.graffle/image1.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineyard/engineyard-migrate/77f7a6fb695f88eb8325cee51248f865474b2a0c/docs/migration-options.graffle/image1.tiff -------------------------------------------------------------------------------- /docs/migration-options.graffle/image2.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineyard/engineyard-migrate/77f7a6fb695f88eb8325cee51248f865474b2a0c/docs/migration-options.graffle/image2.tiff -------------------------------------------------------------------------------- /engineyard-migrate.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "engineyard-migrate/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "engineyard-migrate" 7 | s.version = Engineyard::Migrate::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Dr Nic Williams", "Danish Khan"] 10 | s.email = ["drnicwilliams@gmail.com"] 11 | s.homepage = "https://github.com/engineyard/engineyard-migrate" 12 | s.summary = %q{Migrate up to Engine Yard AppCloud from Heroku or similar.} 13 | s.description = %q{Want to migrate your Ruby on Rails application from Heroku (or similar) up to Engine Yard AppCloud? This is the tool for you.} 14 | 15 | s.rubyforge_project = "engineyard-migrate" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | 22 | s.add_dependency("engineyard", "~>1.4.11") 23 | s.add_dependency("heroku") 24 | s.add_dependency("POpen4", ["~> 0.1.4"]) 25 | s.add_dependency("net-sftp", ["~> 2.0.5"]) 26 | 27 | s.add_development_dependency("awesome_print") 28 | s.add_development_dependency("builder", ["2.1.2"]) # for test app 29 | s.add_development_dependency("rails", ["3.0.3"]) # for test app 30 | s.add_development_dependency("rake", ["~> 0.8.7"]) 31 | s.add_development_dependency("cucumber", ["~> 0.10.0"]) 32 | s.add_development_dependency("cucumber-rails", ["~> 0.3.2"]) 33 | s.add_development_dependency("rspec", ["~> 2.2.0"]) 34 | s.add_development_dependency("nokogiri", ["~> 1.4.0"]) 35 | s.add_development_dependency("ssh-config") 36 | s.add_development_dependency("taps", ["~> 0.3.15"]) 37 | end 38 | -------------------------------------------------------------------------------- /features/heroku.feature: -------------------------------------------------------------------------------- 1 | Feature: Migration from Heroku 2 | In order to reduce cost of migrating from Heroku to AppCloud 3 | As a developer 4 | I want to migrate as much of my Heroku-hosted application to AppCloud 5 | 6 | Scenario: Migrate a simple app 7 | Given I have setup my SSH keys 8 | And I clone the application "git@github.com:engineyard/heroku2ey-simple-app.git" as "simple-app" 9 | 10 | And I have setup my Heroku credentials 11 | And I have a Heroku application "heroku2ey-simple-app" 12 | And it has production data 13 | When I visit the application at "heroku2ey-simple-app.heroku.com" 14 | Then I should see table 15 | | People | 16 | | Dr Nic | 17 | | Danish | 18 | 19 | Given I have setup my AppCloud credentials 20 | And I reset the AppCloud "heroku2eysimpleapp_production" application "heroku2eysimpleapp" database 21 | When I visit the application at "ec2-50-17-248-148.compute-1.amazonaws.com" 22 | Then I should see table 23 | | People | 24 | 25 | When I run local executable "ey-migrate" with arguments "heroku . --account heroku2ey --environment heroku2eysimpleapp_production" 26 | Then I should see "Migration complete!" 27 | When I visit the application at "ec2-50-17-248-148.compute-1.amazonaws.com" 28 | Then I should see table 29 | | People | 30 | | Dr Nic | 31 | | Danish | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /features/migration_errors.feature: -------------------------------------------------------------------------------- 1 | Feature: Migration errors 2 | I want useful error messages and prompts 3 | 4 | Scenario: Fail if application isn't on Heroku 5 | Given I clone the application "git@github.com:engineyard/heroku2ey-simple-app.git" as "simple-app" 6 | When I run local executable "ey-migrate" with arguments "heroku . --account heroku2ey --environment heroku2eysimpleapp_production" 7 | Then I should see 8 | """ 9 | Not a Salesforce Heroku application. 10 | """ 11 | 12 | Scenario: Fail if no Git 'origin' repo URI 13 | Given I clone the application "git@github.com:engineyard/heroku2ey-simple-app.git" as "simple-app" 14 | And I have a Heroku application "heroku2ey-simple-app" 15 | And I have setup my SSH keys 16 | And I have setup my Heroku credentials 17 | Given I run executable "git" with arguments "remote rm origin" 18 | When I run local executable "ey-migrate" with arguments "heroku . --account heroku2ey --environment heroku2eysimpleapp_production" 19 | Then I should see 20 | """ 21 | Please host your Git repo externally and add as remote 'origin'. 22 | """ 23 | 24 | Scenario: Fail if AppCloud credentials not available 25 | Given I clone the application "git@github.com:engineyard/heroku2ey-simple-app.git" as "simple-app" 26 | And I have a Heroku application "heroku2ey-simple-app" 27 | And I have setup my SSH keys 28 | And I have setup my Heroku credentials 29 | 30 | When I run local executable "ey-migrate" with arguments "heroku . --account heroku2ey --environment heroku2eysimpleapp_production" 31 | Then I should see 32 | """ 33 | Please create, boot and deploy an AppCloud application for git@github.com:engineyard/heroku2ey-simple-app.git. 34 | """ 35 | 36 | Scenario: Fail if no AppCloud environments/applications match this application 37 | Given I clone the application "git@github.com:engineyard/heroku2ey-simple-app.git" as "simple-app" 38 | And I have a Heroku application "heroku2ey-simple-app" 39 | And I have setup my SSH keys 40 | And I have setup my Heroku credentials 41 | 42 | Given I have setup my AppCloud credentials 43 | And I run executable "git" with arguments "remote rm origin" 44 | And I run executable "git" with arguments "remote add origin git@github.com:engineyard/UNKNOWN.git" 45 | 46 | When I run local executable "ey-migrate" with arguments "heroku . -e heroku2eysimpleapp_production" 47 | Then I should see 48 | """ 49 | Please create, boot and deploy an AppCloud application for git@github.com:engineyard/UNKNOWN.git. 50 | """ 51 | 52 | Scenario: Fail if too many AppCloud environments match 53 | Given I clone the application "git@github.com:engineyard/heroku2ey-simple-app.git" as "simple-app" 54 | And I have a Heroku application "heroku2ey-simple-app" 55 | And I have setup my SSH keys 56 | And I have setup my Heroku credentials 57 | 58 | Given I have setup my AppCloud credentials 59 | When I run local executable "ey-migrate" with arguments "heroku . -V" 60 | Then I should see "Multiple environments possible, please be more specific:" 61 | Then I should see 62 | """ 63 | ey-migrate heroku . --app='heroku2eysimpleapp' --account='heroku2ey' --environment='heroku2eysimpleapp_production' 64 | ey-migrate heroku . --app='heroku2eysimpleapp' --account='heroku2ey' --environment='heroku2ey_noinstances' 65 | """ 66 | 67 | 68 | Scenario: Fail if environment hasn't been booted yet 69 | Given I have setup my SSH keys 70 | And I clone the application "git@github.com:engineyard/heroku2ey-simple-app.git" as "simple-app" 71 | 72 | And I have setup my Heroku credentials 73 | And I have a Heroku application "heroku2ey-simple-app" 74 | 75 | Given I have setup my AppCloud credentials 76 | 77 | When I run local executable "ey-migrate" with arguments "heroku . -e heroku2ey_noinstances" 78 | Then I should see 79 | """ 80 | Please boot your AppCloud environment and then deploy your application. 81 | """ 82 | 83 | Scenario: Fail if application hasn't been deployed yet 84 | Given I have setup my SSH keys 85 | And I clone the application "git@github.com:engineyard/heroku2ey-simple-app.git" as "simple-app" 86 | 87 | And I have setup my Heroku credentials 88 | And I have a Heroku application "heroku2ey-simple-app" 89 | 90 | Given I have setup my AppCloud credentials 91 | And I remove AppCloud "heroku2eysimpleapp_production" application "heroku2eysimpleapp" folder 92 | 93 | When I run local executable "ey-migrate" with arguments "heroku . -e heroku2eysimpleapp_production" 94 | Then I should see 95 | """ 96 | Please deploy your AppCloud application before running migration. 97 | """ 98 | 99 | -------------------------------------------------------------------------------- /features/step_definitions/application_setup_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^I have setup my SSH keys$/ do 2 | in_home_folder do 3 | FileUtils.cp_r(File.join(@fixtures_path, "credentials/ssh"), ".ssh") 4 | FileUtils.chmod(0700, ".ssh") 5 | FileUtils.chmod(0600, ".ssh/id_rsa") 6 | FileUtils.chmod(0600, ".ssh/id_rsa.pub") 7 | end 8 | end 9 | 10 | Given /^I clone the application "([^"]*)" as "([^"]*)"$/ do |git_uri, app_name| 11 | @git_uri = git_uri 12 | @app_name = app_name 13 | repo_folder = File.expand_path(File.join(@repos_path, app_name)) 14 | unless File.exists?(repo_folder) 15 | @stdout = File.expand_path(File.join(@tmp_root, "git.out")) 16 | @stderr = File.expand_path(File.join(@tmp_root, "git.err")) 17 | FileUtils.chdir(@repos_path) do 18 | system "git clone #{git_uri} #{app_name} > #{@stdout.inspect} 2> #{@stderr.inspect}" 19 | end 20 | end 21 | in_home_folder do 22 | FileUtils.rm_rf(app_name) 23 | FileUtils.cp_r(repo_folder, app_name) 24 | end 25 | @active_project_folder = File.join(@home_path, app_name) 26 | @project_name = app_name 27 | @stdout = File.expand_path(File.join(@tmp_root, "bundle.out")) 28 | @stderr = File.expand_path(File.join(@tmp_root, "bundle.err")) 29 | in_project_folder do 30 | system "bundle > #{@stdout.inspect} 2> #{@stderr.inspect}" 31 | end 32 | end 33 | 34 | Given /^I have setup my Heroku credentials$/ do 35 | in_home_folder do 36 | FileUtils.cp_r(File.join(@fixtures_path, "credentials/heroku"), ".heroku") 37 | FileUtils.chmod(0700, ".heroku") 38 | end 39 | end 40 | # note, when setting up the heroku credentials for the first time: 41 | # set the new $HOME 42 | # cd $HOME 43 | # mkdir .ssh 44 | # chmod 700 .ssh 45 | # heroku list # will go through process of setting up and creating ssh keys 46 | 47 | 48 | Given /^I have a Heroku application "([^"]*)"$/ do |name| 49 | @heroku_name = name 50 | @heroku_host = "#{name}.heroku.com" 51 | in_project_folder do 52 | system "git remote rm heroku 2> /dev/null" 53 | system "git remote add heroku git@heroku.com:#{name}.git" 54 | end 55 | end 56 | 57 | Given /^it has production data$/ do 58 | unless @production_data_installed 59 | in_project_folder do 60 | # TODO - currently hard coded into fixtures/data/APPNAME.sqlite3 as the commented code below isn't working 61 | 62 | `rm -f db/development.sqlite3` 63 | `bundle exec rake db:schema:load` 64 | cmds = ['Dr Nic', 'Danish'].map do |name| 65 | "Person.create(:name => '#{name}')" 66 | end.join("; ") 67 | `bundle exec rails runner "#{cmds}"` 68 | 69 | data_file = File.expand_path(File.join(@fixtures_path, "data", "#{@app_name}.sqlite3")) 70 | raise "Missing production data for '#{@app_name}' at #{data_file}; run 'rake db:seed' in fixtures/repos/#{app_name}" unless File.exists?(data_file) 71 | FileUtils.cp_r(data_file, "db/development.sqlite3") 72 | @stdout = File.expand_path(File.join(@tmp_root, "heroku.out")) 73 | @stderr = File.expand_path(File.join(@tmp_root, "heroku.err")) 74 | system "heroku db:push --confirm #{@heroku_name} > #{@stdout.inspect} 2> #{@stderr.inspect}" 75 | @production_data_installed = true 76 | end 77 | end 78 | end 79 | 80 | Given /^I have setup my AppCloud credentials$/ do 81 | in_home_folder do 82 | FileUtils.cp_r(File.join(@fixtures_path, "credentials/eyrc"), ".eyrc") 83 | FileUtils.chmod(0700, ".eyrc") 84 | end 85 | end 86 | 87 | Given /^I reset the AppCloud "([^"]*)" application "([^"]*)" database$/ do |environment, app_name| 88 | in_project_folder do 89 | @stdout = File.expand_path(File.join(@tmp_root, "eyssh.out")) 90 | @stderr = File.expand_path(File.join(@tmp_root, "eyssh.err")) 91 | system "ey ssh 'cd /data/#{app_name}/current/; RAILS_ENV=production rake db:schema:load' -e #{environment} > #{@stdout.inspect} 2> #{@stderr.inspect}" 92 | end 93 | end 94 | 95 | # Actually moves it to .../current.bak; which is restored after the scenario 96 | Given /^I remove AppCloud "([^"]*)" application "([^"]*)" folder$/ do |environment, app_name| 97 | in_project_folder do 98 | remove_from_appcloud("/data/#{app_name}/current", environment) 99 | end 100 | end 101 | 102 | 103 | -------------------------------------------------------------------------------- /features/step_definitions/common_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^this project is active project folder/ do 2 | @active_project_folder = File.expand_path(File.dirname(__FILE__) + "/../..") 3 | end 4 | 5 | Given /^env variable \$([\w_]+) set to( project path|) "(.*)"/ do |env_var, path, value| 6 | in_project_folder { 7 | value = File.expand_path(value) 8 | } unless path.empty? 9 | ENV[env_var] = value 10 | end 11 | 12 | Given /"(.*)" folder is deleted/ do |folder| 13 | in_project_folder { FileUtils.rm_rf folder } 14 | end 15 | 16 | Given /file "(.*)" is deleted/ do |file| 17 | in_project_folder { FileUtils.rm_rf file } 18 | end 19 | 20 | When /^I invoke "(.*)" generator with arguments "(.*)"$/ do |generator, arguments| 21 | @stdout = StringIO.new 22 | in_project_folder do 23 | if Object.const_defined?("APP_ROOT") 24 | APP_ROOT.replace(FileUtils.pwd) 25 | else 26 | APP_ROOT = FileUtils.pwd 27 | end 28 | run_generator(generator, arguments.split(' '), SOURCES, :stdout => @stdout) 29 | end 30 | File.open(File.join(@tmp_root, "generator.out"), "w") do |f| 31 | @stdout.rewind 32 | f << @stdout.read 33 | end 34 | end 35 | 36 | When /^I run executable "(.*)" with arguments "(.*)"/ do |executable, arguments| 37 | @stdout = File.expand_path(File.join(@tmp_root, "executable.out")) 38 | in_project_folder do 39 | system "#{executable.inspect} #{arguments} > #{@stdout.inspect} 2> #{@stdout.inspect}" 40 | end 41 | end 42 | 43 | When /^I run project executable "(.*)" with arguments "(.*)"/ do |executable, arguments| 44 | @stdout = File.expand_path(File.join(@tmp_root, "executable.out")) 45 | in_project_folder do 46 | system "ruby -rubygems #{executable.inspect} #{arguments} > #{@stdout.inspect} 2> #{@stdout.inspect}" 47 | end 48 | end 49 | 50 | When /^I run local executable "(.*)" with arguments "(.*)"/ do |executable, arguments| 51 | if executable == "ey-migrate" 52 | require 'engineyard-migrate' 53 | require 'engineyard-migrate/cli' 54 | in_project_folder do 55 | stdout, stderr = capture_stdios do 56 | begin 57 | Engineyard::Migrate::CLI.start(arguments.split(/ /)) 58 | rescue SystemExit 59 | end 60 | end 61 | @stdout = File.expand_path(File.join(@tmp_root, "executable.out")) 62 | File.open(@stdout, "w") {|f| f << stdout; f << stderr} 63 | end 64 | else 65 | @stdout = File.expand_path(File.join(@tmp_root, "executable.out")) 66 | executable = File.expand_path(File.join(File.dirname(__FILE__), "/../../bin", executable)) 67 | in_project_folder do 68 | system "ruby -rubygems #{executable.inspect} #{arguments} > #{@stdout.inspect} 2> #{@stdout.inspect}" 69 | end 70 | end 71 | end 72 | 73 | When /^I invoke task "rake (.*)"/ do |task| 74 | @stdout = File.expand_path(File.join(@tmp_root, "tests.out")) 75 | in_project_folder do 76 | system "bundle exec rake #{task} --trace > #{@stdout.inspect} 2> #{@stdout.inspect}" 77 | end 78 | end 79 | 80 | Then /^folder "(.*)" (is|is not) created/ do |folder, is| 81 | in_project_folder do 82 | File.exists?(folder).should(is == 'is' ? be_true : be_false) 83 | end 84 | end 85 | 86 | Then /^file "(.*)" (is|is not) created/ do |file, is| 87 | in_project_folder do 88 | File.exists?(file).should(is == 'is' ? be_true : be_false) 89 | end 90 | end 91 | 92 | Then /^file with name matching "(.*)" is created/ do |pattern| 93 | in_project_folder do 94 | Dir[pattern].should_not be_empty 95 | end 96 | end 97 | 98 | Then /^file "(.*)" contents (does|does not) match \/(.*)\// do |file, does, regex| 99 | in_project_folder do 100 | actual_output = File.read(file) 101 | (does == 'does') ? 102 | actual_output.should(match(/#{regex}/)) : 103 | actual_output.should_not(match(/#{regex}/)) 104 | end 105 | end 106 | 107 | Then /^file "([^"]*)" contains "([^"]*)"$/ do |file, text| 108 | in_project_folder do 109 | actual_output = File.read(file) 110 | actual_output.should contain(text) 111 | end 112 | end 113 | 114 | 115 | Then /gem file "(.*)" and generated file "(.*)" should be the same/ do |gem_file, project_file| 116 | File.exists?(gem_file).should be_true 117 | File.exists?(project_file).should be_true 118 | gem_file_contents = File.read(File.dirname(__FILE__) + "/../../#{gem_file}") 119 | project_file_contents = File.read(File.join(@active_project_folder, project_file)) 120 | project_file_contents.should == gem_file_contents 121 | end 122 | 123 | Then /^(does|does not) invoke generator "(.*)"$/ do |does_invoke, generator| 124 | actual_output = get_command_output 125 | does_invoke == "does" ? 126 | actual_output.should(match(/dependency\s+#{generator}/)) : 127 | actual_output.should_not(match(/dependency\s+#{generator}/)) 128 | end 129 | 130 | Then /help options "(.*)" and "(.*)" are displayed/ do |opt1, opt2| 131 | actual_output = get_command_output 132 | actual_output.should match(/#{opt1}/) 133 | actual_output.should match(/#{opt2}/) 134 | end 135 | 136 | Then /^I should see "([^\"]*)"$/ do |text| 137 | actual_output = get_command_output 138 | actual_output.should contain(text) 139 | end 140 | 141 | Then /^I should not see "([^\"]*)"$/ do |text| 142 | actual_output = 143 | actual_output.should_not contain(text) 144 | end 145 | 146 | Then /^I should see$/ do |text| 147 | actual_output = get_command_output 148 | actual_output.should contain(text) 149 | end 150 | 151 | Then /^I should not see$/ do |text| 152 | actual_output = get_command_output 153 | actual_output.should_not contain(text) 154 | end 155 | 156 | Then /^I should see exactly$/ do |text| 157 | actual_output = get_command_output 158 | actual_output.should == text 159 | end 160 | 161 | Then /^I should see all (\d+) tests pass/ do |expected_test_count| 162 | expected = %r{^#{expected_test_count} tests, \d+ assertions, 0 failures, 0 errors} 163 | actual_output = get_command_output 164 | actual_output.should match(expected) 165 | end 166 | 167 | Then /^I should see all (\d+) examples pass/ do |expected_test_count| 168 | expected = %r{^#{expected_test_count} examples?, 0 failures} 169 | actual_output = get_command_output 170 | actual_output.should match(expected) 171 | end 172 | 173 | Then /^yaml file "(.*)" contains (\{.*\})/ do |file, yaml| 174 | in_project_folder do 175 | yaml = eval yaml 176 | YAML.load(File.read(file)).should == yaml 177 | end 178 | end 179 | 180 | Then /^Rakefile can display tasks successfully/ do 181 | @stdout = File.expand_path(File.join(@tmp_root, "rakefile.out")) 182 | in_project_folder do 183 | system "rake -T > #{@stdout.inspect} 2> #{@stdout.inspect}" 184 | end 185 | actual_output = get_command_output 186 | actual_output.should match(/^rake\s+\w+\s+#\s.*/) 187 | end 188 | 189 | Then /^task "rake (.*)" is executed successfully/ do |task| 190 | @stdout.should_not be_nil 191 | actual_output = get_command_output 192 | actual_output.should_not match(/^Don't know how to build task '#{task}'/) 193 | actual_output.should_not match(/Error/i) 194 | end 195 | 196 | Then /^gem spec key "(.*)" contains \/(.*)\// do |key, regex| 197 | in_project_folder do 198 | gem_file = Dir["pkg/*.gem"].first 199 | gem_spec = Gem::Specification.from_yaml(`gem spec #{gem_file}`) 200 | spec_value = gem_spec.send(key.to_sym) 201 | spec_value.to_s.should match(/#{regex}/) 202 | end 203 | end 204 | 205 | Then /^the file "([^\"]*)" is a valid gemspec$/ do |filename| 206 | spec = eval(File.read(filename)) 207 | spec.validate 208 | end 209 | -------------------------------------------------------------------------------- /features/step_definitions/web_steps.rb: -------------------------------------------------------------------------------- 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 16 | -------------------------------------------------------------------------------- /features/support/appcloud_restore_folders.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /features/support/common.rb: -------------------------------------------------------------------------------- 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) -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 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 25 | -------------------------------------------------------------------------------- /features/support/matchers.rb: -------------------------------------------------------------------------------- 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) 11 | -------------------------------------------------------------------------------- /features/support/web.rb: -------------------------------------------------------------------------------- 1 | module Web 2 | def response_body 3 | @response_body 4 | end 5 | end 6 | World(Web) -------------------------------------------------------------------------------- /fixtures/data/simple-app.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineyard/engineyard-migrate/77f7a6fb695f88eb8325cee51248f865474b2a0c/fixtures/data/simple-app.sqlite3 -------------------------------------------------------------------------------- /heroku-todo.md: -------------------------------------------------------------------------------- 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 | 101 | -------------------------------------------------------------------------------- /lib/engineyard-migrate.rb: -------------------------------------------------------------------------------- 1 | module Engineyard 2 | module Migrate 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/engineyard-migrate/cli.rb: -------------------------------------------------------------------------------- 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 | say `heroku info` 36 | say "" 37 | 38 | repo = `git config remote.origin.url`.strip 39 | if repo.empty? 40 | error "Please host your Git repo externally and add as remote 'origin'.", <<-SUGGESTION.gsub(/^\s{12}/, '') 41 | You can create a GitHub repository using 'github' gem: 42 | $ gem install github 43 | $ gh create-from-local --private 44 | SUGGESTION 45 | end 46 | unless EY::API.new.token 47 | error "Please create, boot and deploy an AppCloud application for #{repo}." 48 | end 49 | 50 | say "Requesting AppCloud account information..."; $stdout.flush 51 | @app, @environment = fetch_app_and_environment(options[:app], options[:environment], options[:account]) 52 | 53 | unless @app.repository_uri == repo 54 | error "Please create, boot and deploy an AppCloud application for #{repo}." 55 | end 56 | unless @environment.app_master 57 | error "Please boot your AppCloud environment and then deploy your application." 58 | end 59 | 60 | @app.name = @app.name 61 | app_master_host = @environment.app_master.public_hostname 62 | app_master_user = @environment.username 63 | 64 | say "Application: "; say "#{@app.name}", :green 65 | say "Account: "; say "#{@environment.account.name}", :green 66 | say "Environment: "; say "#{@environment.name}", :green 67 | say "Cluster size: "; say "#{@environment.instances_count}" 68 | say "Hostname: "; say "#{app_master_host}" 69 | debug "$RACK_ENV: "; debug "#{@environment.framework_env}" 70 | say "" 71 | 72 | # TODO - what if no application deployed yet? 73 | # bash: line 0: cd: /data/heroku2eysimpleapp/current: No such file or directory 74 | 75 | # TODO - to test for cron setup: 76 | # dna_env["cron"] - list of: 77 | # [0] { 78 | # "minute" => "0", 79 | # "name" => "rake cron", 80 | # "command" => "cd /data/heroku2eysimpleapp/current && RAILS_ENV=production rake cron", 81 | # "month" => "*", 82 | # "hour" => "1", 83 | # "day" => "*/1", 84 | # "user" => "deploy", 85 | # "weekday" => "*" 86 | # } 87 | 88 | say "Testing AppCloud application status..." 89 | 90 | deploy_path_found = ssh_appcloud "test -d #{@app.name}/current && echo 'found'", 91 | :path => '/data', :return_output => true 92 | error "Please deploy your AppCloud application before running migration." unless deploy_path_found =~ /found/ 93 | 94 | say "Setting up Heroku on AppCloud..." 95 | 96 | ssh_appcloud "sudo gem install heroku taps sqlite3 mysql2 pg --no-ri --no-rdoc -q" 97 | ssh_appcloud "git remote rm heroku 2> /dev/null; git remote add heroku #{heroku_repo} 2> /dev/null" 98 | 99 | say "Uploading Heroku credential file..." 100 | home_path = ssh_appcloud("pwd", :path => "~", :return_output => true) 101 | debug "AppCloud $HOME: "; debug home_path, :yellow 102 | 103 | require 'heroku/auth' 104 | netrc_file = File.basename(Heroku::Auth.netrc_path) 105 | remote_netrc = File.join(home_path, ".netrc") 106 | Net::SFTP.start(app_master_host, app_master_user) do |sftp| 107 | sftp.upload!(Heroku::Auth.netrc_path, remote_netrc) 108 | end 109 | ssh_appcloud("chmod 0600 #{remote_netrc}") 110 | say "" 111 | 112 | say "Migrating data from Heroku '#{heroku_app_name}' to AppCloud '#{@app.name}'..." 113 | env_vars = %w[RAILS_ENV RACK_ENV MERB_ENV].map {|var| "#{var}=#{@environment.framework_env}" }.join(" ") 114 | ssh_appcloud "#{env_vars} heroku db:pull --confirm #{heroku_app_name} 2>&1" 115 | say "" 116 | 117 | say "Migration complete!", :green 118 | rescue SystemExit 119 | rescue EY::MultipleMatchesError => e 120 | envs = [] 121 | e.message.split(/\n/).map do |line| 122 | env = {} 123 | line.scan(/--([^=]+)='([^']+)'/) do 124 | env[$1] = $2 125 | end 126 | envs << env unless env.empty? 127 | end 128 | too_many_environments_discovered 'heroku', envs, path 129 | rescue Net::SSH::AuthenticationFailed => e 130 | error "Please setup your SSH credentials for AppCloud." 131 | rescue Net::SFTP::StatusException => e 132 | error e.description + ": " + e.text 133 | rescue Exception => e 134 | say "Migration failed", :red 135 | puts e.inspect 136 | puts e.backtrace 137 | end 138 | end 139 | end 140 | 141 | map "-v" => :version, "--version" => :version, "-h" => :help, "--help" => :help 142 | 143 | private 144 | def ssh_appcloud(cmd, options = {}) 145 | path = options[:path] || "/data/#{@app.name}/current/" 146 | flags = " #{options[:flags]}" || "" if options[:flags] # app master by default 147 | full_cmd = "cd #{path}; #{cmd}" 148 | ssh_cmd = "ey ssh #{Escape.shell_command([full_cmd])}#{flags} -e #{@environment.name} -c #{@environment.account.name}" 149 | debug options[:return_output] ? "Capturing: " : "Running: " 150 | debug ssh_cmd, :yellow; $stdout.flush 151 | out = "" 152 | status = 153 | POpen4::popen4(ssh_cmd) do |stdout, stderr, stdin, pid| 154 | if options[:return_output] 155 | out += stdout.read.strip 156 | err = stderr.read.strip; say err unless err.empty? 157 | else 158 | while line = stdout.gets("\n") || stderr.gets("\n") 159 | say line 160 | end 161 | end 162 | end 163 | 164 | puts "exitstatus : #{ status.exitstatus }" unless status.exitstatus == 0 165 | out if options[:return_output] 166 | end 167 | 168 | def say(msg, color = nil) 169 | color ? shell.say(msg, color) : shell.say(msg) 170 | end 171 | 172 | def debug(msg, color = nil) 173 | say(msg, color) if verbose 174 | end 175 | 176 | def display(text) 177 | shell.say text 178 | exit 179 | end 180 | 181 | def error(text, suggestion = nil) 182 | shell.say "ERROR: #{text}", :red 183 | if suggestion 184 | shell.say "" 185 | shell.say suggestion 186 | end 187 | exit 188 | end 189 | 190 | # TODO - not being used yet 191 | def no_environments_discovered 192 | say "No AppCloud environments found for this application.", :red 193 | say "Either:" 194 | say " * Create an AppCloud environment for this application/git URL" 195 | say " * Use --environment/--account flags to select an AppCloud environment" 196 | end 197 | 198 | def too_many_environments_discovered(task, environments, *args) 199 | return no_environments_discovered if environments.empty? 200 | say "Multiple environments possible, please be more specific:", :red 201 | say "" 202 | environments.each do |env| 203 | flags = env.map { |key, value| "--#{key}='#{value}'"}.join(" ") 204 | say " ey-migrate #{task} #{args.join(' ')} #{flags}" 205 | end 206 | exit 1 207 | end 208 | 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /lib/engineyard-migrate/version.rb: -------------------------------------------------------------------------------- 1 | module Engineyard 2 | module Migrate 3 | VERSION = "1.1.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib')) 2 | require 'bundler/setup' 3 | require 'engineyard-migrate' 4 | require 'rspec' --------------------------------------------------------------------------------