class Heroku::Command::Fork
clone an existing app
Public Instance Methods
index()
click to toggle source
fork [NEWNAME]
Fork an existing app – copy config vars and Heroku Postgres data, and re-provision add-ons to a new app. New app name should not be an existing app. The new app will be created as part of the forking process.
-s, –stack STACK # specify a stack for the new app –region REGION # specify a region
# File lib/heroku/command/fork.rb, line 18 def index options[:ignore_no_org] = true from = app to = shift_argument || "#{from}-#{(rand*1000).to_i}" if from == to raise Heroku::Command::CommandFailed.new("Cannot fork to the same app.") end from_info = api.get_app(from).body to_info = action("Creating fork #{to}", :org => !!org) do params = { "name" => to, "region" => options[:region] || from_info["region"], "stack" => options[:stack] || from_info["stack"], "tier" => from_info["tier"] == "legacy" ? "production" : from_info["tier"] } info = if org org_api.post_app(params, org).body else api.post_app(params).body end end action("Copying slug") do copy_slug(from, to) end from_config = api.get_config_vars(from).body from_addons = api.get_addons(from).body from_addons.each do |addon| print "Adding #{addon["name"]}... " begin to_addon = api.post_addon(to, addon["name"]).body puts "done" rescue Heroku::API::Errors::RequestFailed => ex puts "skipped (%s)" % json_decode(ex.response.body)["error"] rescue Heroku::API::Errors::NotFound puts "skipped (not found)" end if addon["name"] =~ /^heroku-postgresql:/ from_var_name = "#{addon["attachment_name"]}_URL" from_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] if from_config[from_var_name] == from_config["DATABASE_URL"] from_config["DATABASE_URL"] = api.get_config_vars(to).body["#{from_attachment}_URL"] end from_config.delete(from_var_name) plan = addon["name"].split(":").last unless %w(dev basic hobby-dev hobby-basic).include? plan wait_for_db to, to_addon end check_for_pgbackups! from check_for_pgbackups! to migrate_db addon, from, to_addon, to end end to_config = api.get_config_vars(to).body action("Copying config vars") do diff = from_config.inject({}) do |ax, (key, val)| ax[key] = val unless to_config[key] ax end api.put_config_vars to, diff end puts "Fork complete, view it at #{to_info['web_url']}" rescue Exception => e raise if e.is_a?(Heroku::Command::CommandFailed) puts "Failed to fork app #{from} to #{to}." message = "WARNING: Potentially Destructive Action\nThis command will destroy #{to} (including all add-ons)." if confirm_command(to, message) action("Deleting #{to}") do begin api.delete_app(to) rescue Heroku::API::Errors::NotFound end end end puts "Original exception below:" raise e end
Private Instance Methods
check_for_pgbackups!(app)
click to toggle source
# File lib/heroku/command/fork.rb, line 119 def check_for_pgbackups!(app) unless api.get_addons(app).body.detect { |addon| addon["name"] =~ /^pgbackups:/ } action("Adding pgbackups:plus to #{app}") do api.post_addon app, "pgbackups:plus" end end end
copy_slug(from, to)
click to toggle source
# File lib/heroku/command/fork.rb, line 111 def copy_slug(from, to) from_releases = api.get_releases_v3(from, 'version ..; order=desc,max=1;').body raise Heroku::Command::CommandFailed.new("No releases on #{from}") if from_releases.empty? from_slug = from_releases.first.fetch('slug', {}) raise Heroku::Command::CommandFailed.new("No slug on #{from}") unless from_slug api.post_release_v3(to, from_slug["id"], "Forked from #{from}") end
migrate_db(from_addon, from, to_addon, to)
click to toggle source
# File lib/heroku/command/fork.rb, line 127 def migrate_db(from_addon, from, to_addon, to) transfer = nil action("Transferring database (this can take some time)") do from_config = api.get_config_vars(from).body from_attachment = from_addon["attachment_name"] to_config = api.get_config_vars(to).body to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] pgb = Heroku::Client::Pgbackups.new(from_config["PGBACKUPS_URL"]) transfer = pgb.create_transfer( from_config["#{from_attachment}_URL"], from_attachment, to_config["#{to_attachment}_URL"], to_attachment, :expire => "true") error transfer["errors"].values.flatten.join("\n") if transfer["errors"] loop do transfer = pgb.get_transfer(transfer["id"]) error transfer["errors"].values.flatten.join("\n") if transfer["errors"] break if transfer["finished_at"] sleep 1 end print " " end end
pg_api()
click to toggle source
# File lib/heroku/command/fork.rb, line 155 def pg_api require "rest_client" host = "postgres-api.heroku.com" RestClient::Resource.new "https://#{host}/client/v11/databases", Heroku::Auth.user, Heroku::Auth.password end
wait_for_db(app, attachment)
click to toggle source
# File lib/heroku/command/fork.rb, line 161 def wait_for_db(app, attachment) attachments = api.get_attachments(app).body.inject({}) { |ax,att| ax.update(att["name"] => att["resource"]["name"]) } attachment_name = attachment["message"].match(/Attached as (\w+)_URL\n/)[1] action("Waiting for database to be ready (this can take some time)") do loop do begin waiting = json_decode(pg_api["#{attachments[attachment_name]}/wait_status"].get.to_s)["waiting?"] break unless waiting sleep 5 rescue RestClient::ResourceNotFound rescue Interrupt exit 0 end end print " " end end