Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Commit

Permalink
Revert "use toolbelt v4 for fork"
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeff Dickey committed Mar 21, 2015
1 parent 9f8dc3d commit 13d59bc
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 16 deletions.
34 changes: 34 additions & 0 deletions lib/heroku/api/releases_v3.rb
@@ -0,0 +1,34 @@
module Heroku
class API
def get_releases_v3(app, range=nil)
headers = { 'Accept' => 'application/vnd.heroku+json; version=3' }
headers.merge!('Range' => range) if range
request(
:expects => [ 200, 206 ],
:headers => headers,
:method => :get,
:path => "/apps/#{app}/releases"
)
end

def post_release_v3(app, slug_id, opts={})
headers = {
'Accept' => 'application/vnd.heroku+json; version=3',
'Content-Type' => 'application/json'
}
headers.merge!('Heroku-Deploy-Type' => opts[:deploy_type]) if opts[:deploy_type]
headers.merge!('Heroku-Deploy-Source' => opts[:deploy_source]) if opts[:deploy_source]

body = { 'slug' => slug_id }
body.merge!('description' => opts[:description]) if opts[:description]

request(
:expects => 201,
:headers => headers,
:method => :post,
:path => "/apps/#{app}/releases",
:body => Heroku::Helpers.json_encode(body)
)
end
end
end
167 changes: 163 additions & 4 deletions lib/heroku/command/fork.rb
@@ -1,3 +1,4 @@
require "heroku/api/releases_v3"
require "heroku/command/base"

module Heroku::Command
Expand All @@ -13,12 +14,170 @@ class Fork < Base
#
# -s, --stack STACK # specify a stack for the new app
# --region REGION # specify a region
# --copy-pg-data # copy postgres database data instead of just creating empty databases
#
def index
Heroku::JSPlugin.setup
Heroku::JSPlugin.install('heroku-fork') unless Heroku::JSPlugin.is_plugin_installed?('heroku-fork')
Heroku::JSPlugin.run('fork', nil, ARGV[1..-1])
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

begin
api.get_app(to).body
error "#{to} app exists.\nUSAGE: heroku fork -a COPY_FROM COPY_TO"
rescue
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"]
}

if org
org_api.post_app(params, org).body
else
api.post_app(params).body
end
end

action("Copying slug") do
copy_slug(from_info, to_info)
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

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 => 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

def copy_slug(from_info, to_info)
from = from_info["name"]
to = to_info["name"]
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"],
:description => "Forked from #{from}",
:deploy_type => "fork",
:deploy_source => from_info["id"])
end

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]

resolver = Heroku::Helpers::HerokuPostgresql::Resolver.new(to, api)
attachment = resolver.resolve("#{to_attachment}_URL", nil)
pgb = Heroku::Client::HerokuPostgresql.new(attachment)
transfer = pgb.pg_copy(
from_attachment.gsub('HEROKU_POSTGRESQL_',''),
from_config["#{from_attachment}_URL"],
to_attachment.gsub('HEROKU_POSTGRESQL_',''),
to_config["#{to_attachment}_URL"])

hpg_app_client = Heroku::Client::HerokuPostgresqlApp.new(to)
begin
transfer = hpg_app_client.transfers_get(transfer[:uuid])
sleep 1
end until transfer[:finished_at]
if !transfer[:succeeded]
error "An error occurred and your transfer did not finish."
end
print " "
end
end

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

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

end
end
14 changes: 2 additions & 12 deletions lib/heroku/jsplugin.rb
Expand Up @@ -95,8 +95,7 @@ def self.setup
return if File.exist? bin
$stderr.print "Installing Heroku Toolbelt v4..."
FileUtils.mkdir_p File.dirname(bin)
opts = excon_opts.merge(:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress])
resp = Excon.get(url, opts)
resp = Excon.get(url, :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress])
open(bin, "wb") do |file|
file.write(resp.body)
end
Expand Down Expand Up @@ -138,16 +137,7 @@ def self.os
end

def self.manifest
@manifest ||= JSON.parse(Excon.get("https://d1gvo455cekpjp.cloudfront.net/master/manifest.json", excon_opts).body)
end

def self.excon_opts
if os == 'windows'
# S3 SSL downloads do not work from ruby in Windows
{:ssl_verify_peer => false}
else
{}
end
@manifest ||= JSON.parse(Excon.get("http://d1gvo455cekpjp.cloudfront.net/master/manifest.json").body)
end

def self.url
Expand Down
136 changes: 136 additions & 0 deletions spec/heroku/command/fork_spec.rb
@@ -0,0 +1,136 @@
require "heroku/api/releases_v3"
require "spec_helper"
require "heroku/command/fork"

module Heroku::Command

describe Fork do

before(:each) do
stub_core
api.post_app("name" => "example", "stack" => "cedar")
end

after(:each) do
api.delete_app("example")
begin
api.delete_app("example-fork")
rescue Heroku::API::Errors::NotFound
end
end

context "successfully" do

before(:each) do
Excon.stub({ :method => :get,
:path => "/apps/example/releases" },
{ :body => [{"slug" => {"id" => "SLUG_ID"}}],
:status => 206})

Excon.stub({ :method => :post,
:path => "/apps/example-fork/releases"},
{ :status => 201})
end

after(:each) do
Excon.stubs.shift
Excon.stubs.shift
end

it "forks an app" do
stderr, stdout = execute("fork example-fork")
expect(stderr).to eq("")
expect(stdout).to eq <<-STDOUT
Creating fork example-fork... done
Copying slug... done
Copying config vars... done
Fork complete, view it at http://example-fork.herokuapp.com/
STDOUT
end

it "copies slug" do
from_info = api.get_app("example").body
expect_any_instance_of(Heroku::API).to receive(:get_releases_v3).with("example", "version ..; order=desc,max=1;").and_call_original
expect_any_instance_of(Heroku::API).to receive(:post_release_v3).with("example-fork",
"SLUG_ID",
:description => "Forked from example",
:deploy_type => "fork",
:deploy_source => from_info["id"]).and_call_original
execute("fork example-fork")
end

it "copies config vars" do
config_vars = {
"SECRET" => "imasecret",
"FOO" => "bar",
"LANG_ENV" => "production"
}
api.put_config_vars("example", config_vars)
execute("fork example-fork")
expect(api.get_config_vars("example-fork").body).to eq(config_vars)
end

it "re-provisions add-ons" do
api.post_addon("example", "heroku-postgresql:hobby-dev")
execute("fork example-fork")
expect(api.get_addons("example-fork").body[0]["name"]).to eq("heroku-postgresql:hobby-dev")
end
end

describe "error handling" do
it "fails if no source release exists" do
begin
Excon.stub({ :method => :get,
:path => "/apps/example/releases" },
{ :body => [],
:status => 206})
execute("fork example-fork")
raise
rescue Heroku::Command::CommandFailed => e
expect(e.message).to eq("No releases on example")
ensure
Excon.stubs.shift
end
end

it "fails if source slug does not exist" do
begin
Excon.stub({ :method => :get,
:path => "/apps/example/releases" },
{ :body => [{"slug" => nil}],
:status => 206})
execute("fork example-fork")
raise
rescue Heroku::Command::CommandFailed => e
expect(e.message).to eq("No slug on example")
ensure
Excon.stubs.shift
end
end

it "doesn't attempt to fork to the same app" do
expect do
execute("fork example")
end.to raise_error(Heroku::Command::CommandFailed, /same app/)
end

it "confirms before deleting the app" do
Excon.stub({:path => "/apps/example/releases"}, {:status => 500})
begin
execute("fork example-fork")
rescue Heroku::API::Errors::ErrorWithResponse
ensure
Excon.stubs.shift
end
expect(api.get_apps.body.map { |app| app["name"] }).to eq(
%w( example example-fork )
)
end

it "deletes fork app on error, before re-raising" do
stub(Heroku::Command).confirm_command.returns(true)
expect(api.get_apps.body.map { |app| app["name"] }).to eq(%w( example ))
end
end
end
end

0 comments on commit 13d59bc

Please sign in to comment.