Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Rename command to clone:create, Add clone:sharing, clone:config, clon…

…e:features
  • Loading branch information...
commit 74e84c57a6529b4ed20ba6f2c303a903b25a1df1 1 parent 86fa209
@eckardt authored
View
2  README.md
@@ -26,7 +26,7 @@ You'll start with an existing app which has some custom config variables and col
Now, creating an exact copy of this app is easy
- $ heroku apps:clone
+ $ heroku clone:create
Creating dry-oasis-7199-clone-19b3... done, stack is cedar
http://dry-oasis-7199-clone-19b3.herokuapp.com/ | git@heroku.com:dry-oasis-7199-clone-19b3.git
Copying someone@company.com to dry-oasis-7199-clone-19b3 collaborators... done
View
2  init.rb
@@ -1 +1 @@
-require 'clone/heroku/command/apps'
+require 'clone/heroku/command/clone'
View
82 lib/clone/heroku/command/apps.rb
@@ -1,82 +0,0 @@
-require 'heroku/command/base'
-require 'securerandom'
-
-class Heroku::Command::Apps < Heroku::Command::Base
-
- # apps:clone [NAME]
- #
- # Create a new app which is a clone of an existing app
- #
- # --addons ADDONS # a comma-delimited list of addons to install
- # -a, --app APP # the app which should be cloned
- # -b, --buildpack BUILDPACK # a buildpack url to use for this app
- # -n, --no-remote # don't create a git remote
- # -r, --remote REMOTE # the git remote to create, default "heroku"
- # -s, --stack STACK # the stack on which to create the app
- # -c, --no-collabs # don't copy over the collaborators
- # -v, --no-vars # don't copy over the config vars
- #
- #Examples:
- #
- # $ heroku apps:clone
- # Creating floating-dragon-42-clone-e153d5... done, stack is cedar
- # http://floating-dragon-42.heroku.com/ | git@heroku.com:floating-dragon-42.git
- #
- # $ heroku apps:clone -s bamboo
- # Creating floating-dragon-42-clone-e153d5... done, stack is bamboo-mri-1.9.2
- # http://floating-dragon-42.herokuapp.com/ | git@heroku.com:floating-dragon-42.git
- #
- # # specify a name
- # $ heroku apps:clone example
- # Creating example... done, stack is cedar
- # http://example.heroku.com/ | git@heroku.com:example.git
- #
- # # create a staging app
- # $ heroku apps:clone example-staging --remote staging
- #
- def clone
- target_app = shift_argument || "#{app}-clone-#{SecureRandom.hex(2)}"
- source_app = app
- options[:app] = target_app
-
- create
-
- unless options[:no_collabs].is_a? FalseClass
- copy_collaborators(source_app, target_app)
- end
-
- unless options[:no_vars].is_a? FalseClass
- copy_config_vars(source_app, target_app)
- end
- end
-
- private
-
- def copy_collaborators(app, target_name)
- collaborators = api.get_collaborators(app).body.map{|collab| collab['email']}
- target_collaborators = api.get_collaborators(target_name).body.map{|collab| collab['email']}
-
- (collaborators - target_collaborators).each do |email|
- action("Copying #{email} to #{target_name} collaborators") do
- api.post_collaborator(target_name, email)
- end
- end
- end
-
-
- def copy_config_vars(app, target_name)
- vars = api.get_config_vars(app).body
-
- action("Copying config vars from #{app} and restarting #{target_name}") do
- api.put_config_vars(target_name, vars)
-
- @status = begin
- if release = api.get_release(target_name, 'current').body
- release['name']
- end
- rescue Heroku::API::Errors::RequestFailed => e
- end
- end
- end
-
-end
View
144 lib/clone/heroku/command/clone.rb
@@ -0,0 +1,144 @@
+require 'heroku/command/base'
+require 'securerandom'
+
+class Heroku::Command::Clone < Heroku::Command::Base
+
+ # clone:create [NAME]
+ #
+ # Create a new app which is a clone of an existing app
+ #
+ # --addons ADDONS # a comma-delimited list of addons to install
+ # -a, --app APP # the app which should be cloned
+ # -b, --buildpack BUILDPACK # a buildpack url to use for this app
+ # -n, --no-remote # don't create a git remote
+ # -r, --remote REMOTE # the git remote to create, default "heroku"
+ # -s, --stack STACK # the stack on which to create the app
+ # -c, --no-collabs # don't copy over the collaborators
+ # -v, --no-vars # don't copy over the config vars
+ # -f, --no-features # don't copy over the labs features
+ #
+ #Examples:
+ #
+ # $ heroku clone:create
+ # Creating floating-dragon-42-clone-e153d5... done, stack is cedar
+ # http://floating-dragon-42.heroku.com/ | git@heroku.com:floating-dragon-42.git
+ #
+ # $ heroku clone:create -s bamboo
+ # Creating floating-dragon-42-clone-e153d5... done, stack is bamboo-mri-1.9.2
+ # http://floating-dragon-42.herokuapp.com/ | git@heroku.com:floating-dragon-42.git
+ #
+ # # specify a name
+ # $ heroku clone:create example
+ # Creating example... done, stack is cedar
+ # http://example.heroku.com/ | git@heroku.com:example.git
+ #
+ # # create a staging app
+ # $ heroku clone:create example-staging --remote staging
+ #
+ def create
+ apps_command.create
+
+ unless options[:no_collabs].is_a? FalseClass
+ sharing
+ end
+
+ unless options[:no_vars].is_a? FalseClass
+ config
+ end
+
+ unless options[:no_features].is_a? FalseClass
+ features
+ end
+ end
+
+ # clone:sharing NAME
+ #
+ # Copy the list of collaborators of one app to another app
+ #
+ # -a, --app APP # app whose collaborators should be copied
+ #
+ #Examples:
+ #
+ # $ heroku clone:sharing example-clone -a example
+ # Copying collaborator@example.com to example-clone collaborators
+ #
+ def sharing
+ collaborators = api.get_collaborators(app).body.map{|collab| collab['email']}
+ target_collaborators = api.get_collaborators(target_app).body.map{|collab| collab['email']}
+
+ (collaborators - target_collaborators).each do |email|
+ action("Copying #{email} to #{target_app} collaborators") do
+ api.post_collaborator(target_app, email)
+ end
+ end
+ end
+
+ # clone:config NAME
+ #
+ # Copy the config of one app to another app
+ #
+ # -a, --app APP # app whose config should be copied
+ #
+ #Examples:
+ #
+ # $ heroku clone:config example-clone -a example
+ # Copying config vars from example and restarting example-clone
+ #
+ def config
+ vars = api.get_config_vars(app).body
+
+ action("Copying config vars from #{app} and restarting #{target_app}") do
+ api.put_config_vars(target_app, vars)
+
+ @status = begin
+ if release = api.get_release(target_app, 'current').body
+ release['name']
+ end
+ rescue Heroku::API::Errors::RequestFailed => e
+ end
+ end
+ end
+
+ # clone:features NAME
+ #
+ # Copy the features of one app to another app
+ #
+ # -a, --app APP # app whose features should be copied
+ #
+ #Examples:
+ #
+ # $ heroku clone:features example-clone -a example
+ # Adding user_env_compile to example-clone
+ # Deleting sigterm-all from example-clone
+ #
+ def features
+ features = Hash[api.get_features(app).body.map{|feature| [feature['name'], feature['enabled']]}]
+ actual_features = Hash[api.get_features(target_app).body.map{|feature| [feature['name'], feature['enabled']]}]
+
+ features_to_enable = features.select{|feature, enabled| enabled && !actual_features[feature]}
+ features_to_disable = actual_features.select{|feature, enabled| enabled && !features[feature]}
+
+ action("Copying labs features from #{app} and restarting #{target_app}") do
+ features_to_enable.each do |feature|
+ puts "Adding #{feature} to #{target_app}"
+ api.post_feature(feature, target_app)
+ end
+
+ features_to_disable.each do |feature|
+ puts "Deleting #{feature} from #{target_app}"
+ api.delete_feature(feature, target_app)
+ end
+ end
+ end
+
+ private
+
+ def apps_command
+ @apps_command ||= Heroku::Command::Apps.new(@args, @options.merge(app: target_app))
+ end
+
+ def target_app
+ @target_app ||= shift_argument || "#{app}-clone-#{SecureRandom.hex(2)}"
+ end
+
+end
View
208 spec/clone_spec.rb
@@ -1,92 +1,198 @@
require 'spec_helper'
-require 'clone/heroku/command/apps'
+require 'clone/heroku/command/clone'
-describe Heroku::Command::Apps do
+describe Heroku::Command::Clone do
before do
- Heroku::Command::Apps.any_instance.stub(:api => heroku)
+ Heroku::Command::Base.any_instance.stub(:api => heroku)
end
let(:new_app_name) { SecureRandom.hex(8) }
- it 'cannot clone a non-existing app' do
- with_app do |app_data|
- proc{ execute("apps:clone -a non-existing") }.should raise_error
+ context "clone:create command" do
+
+ it 'cannot clone a non-existing app' do
+ with_app do |app_data|
+ proc{ execute("clone:create -a non-existing") }.should raise_error
+ end
end
- end
- it 'can clone an existing app' do
- with_app do |app_data|
- proc{ execute("apps:clone -a #{app_data['name']}") }.should_not raise_error
+ it 'can clone an existing app' do
+ with_app do |app_data|
+ proc{ execute("clone:create -a #{app_data['name']}") }.should_not raise_error
+ end
end
- end
- it 'creates app with specified name' do
- with_app do |app_data|
- execute("apps:clone #{new_app_name} -a #{app_data['name']}")
- heroku.get_app(new_app_name).body['name'].should == new_app_name
+ it 'creates app with specified name' do
+ with_app do |app_data|
+ execute("clone:create #{new_app_name} -a #{app_data['name']}")
+ heroku.get_app(new_app_name).body['name'].should == new_app_name
+ end
end
- end
- it 'uses the same stack' do
- with_app do |app_data|
- execute("apps:clone #{new_app_name} -a #{app_data['name']}")
- new_app = heroku.get_app(new_app_name)
+ it 'uses the same stack' do
+ with_app do |app_data|
+ execute("clone:create #{new_app_name} -a #{app_data['name']}")
+ new_app = heroku.get_app(new_app_name)
- new_app.body['stack'].should eql( app_data['stack'] )
+ new_app.body['stack'].should eql( app_data['stack'] )
+ end
end
- end
- it 'uses the stack option if it is provided' do
- with_app do |app_data|
- execute("apps:clone #{new_app_name} -s cedar -a #{app_data['name']}")
- new_app = heroku.get_app(new_app_name)
+ it 'uses the stack option if it is provided' do
+ with_app do |app_data|
+ execute("clone:create #{new_app_name} -s cedar -a #{app_data['name']}")
+ new_app = heroku.get_app(new_app_name)
+
+ new_app.body['stack'].should eql( "cedar" )
+ end
+ end
+
+ it 'uses the same collaborators' do
+ with_app do |app_data|
+ execute("clone:create #{new_app_name} -a #{app_data['name']}")
+
+ new_app_name.should have_same_collaborators(app_data['name'])
+ end
+ end
+
+ it 'skips copying the collaborators when option is set' do
+ with_app do |app_data|
+ execute("clone:create #{new_app_name} -a #{app_data['name']} -c")
+
+ new_app_name.should_not have_same_collaborators(app_data['name'])
+ end
+ end
+
+ it 'clones the config variables' do
+ with_app do |app_data|
+ execute("clone:create #{new_app_name} -a #{app_data['name']}")
+
+ new_app_name.should have_same_config(app_data['name'])
+ end
+ end
+
+ it 'skips cloning the config variables when option is set' do
+ with_app do |app_data|
+ execute("clone:create #{new_app_name} -a #{app_data['name']} -v")
+
+ new_app_name.should_not have_same_config(app_data['name'])
+ end
+ end
+
+ it 'clones the labs features' do
+ with_app do |app_data|
+ err, out = execute("clone:create #{new_app_name} -a #{app_data['name']}")
+ out.should include('Copying labs features')
+ end
+ end
- new_app.body['stack'].should eql( "cedar" )
+ it 'skips cloning the labs features when option is set' do
+ with_app do |app_data|
+ err, out = execute("clone:create #{new_app_name} -a #{app_data['name']} -f")
+ out.should_not include('Copying labs features')
+ end
end
+
end
- it 'uses the same collaborators' do
- with_app do |app_data|
- execute("apps:clone #{new_app_name} -a #{app_data['name']}")
+ context "clone:config command" do
- app_collaborators = heroku.get_collaborators( app_data['name'] ).body.map{|collab| collab['email']}
- new_app_collaborators = heroku.get_collaborators( new_app_name ).body.map{|collab| collab['email']}
+ it 'cannot clone a non-existing app' do
+ with_app do |app_data|
+ proc{ execute("clone:config -a non-existing") }.should raise_error
+ end
+ end
- (app_collaborators - new_app_collaborators).should be_empty
+ it 'requires an existing target app' do
+ with_app do |app_data|
+ proc{ execute("clone:config non-existing -a #{app_data['name']}") }.should raise_error
+ end
end
+
+ it 'clones the config' do
+ with_app do |app_data|
+ with_app do |target_data|
+ execute("clone:config #{target_data['name']} -a #{app_data['name']}")
+ target_data['name'].should have_same_config(app_data['name'])
+ end
+ end
+ end
+
end
- it 'skips copying the collaborators when option is set' do
- with_app do |app_data|
- execute("apps:clone #{new_app_name} -a #{app_data['name']} -c")
+ context "clone:sharing command" do
+
+ it 'cannot clone a non-existing app' do
+ with_app do |app_data|
+ proc{ execute("clone:sharing -a non-existing") }.should raise_error
+ end
+ end
+
+ it 'requires an existing target app' do
+ with_app do |app_data|
+ proc{ execute("clone:sharing non-existing -a #{app_data['name']}") }.should raise_error
+ end
+ end
- app_collaborators = heroku.get_collaborators( app_data['name'] ).body.map{|collab| collab['email']}
- new_app_collaborators = heroku.get_collaborators( new_app_name ).body.map{|collab| collab['email']}
+ it 'clones the collaborator list' do
+ with_app do |app_data|
+ with_app do |target_data|
+ execute("clone:sharing #{target_data['name']} -a #{app_data['name']}")
- (app_collaborators - new_app_collaborators).should_not be_empty
+ target_data['name'].should have_same_collaborators(app_data['name'])
+ end
+ end
end
+
end
- it 'clones the config variables' do
- with_app do |app_data|
- execute("apps:clone #{new_app_name} -a #{app_data['name']}")
+ context "clone:features command" do
+
+ it 'clones all labs features' do
+ with_app do |app_data|
+ with_app do |target_data|
+ features = heroku.get_features(app_data['name']).body
+ target_features = features.map(&:dup)
+ features.detect{|feature| feature['name'] == 'sigterm-all'}.merge!('enabled' => false)
+ features.detect{|feature| feature['name'] == 'user_env_compile'}.merge!('enabled' => true)
+
+ heroku = stub('heroku api')
+ heroku.stub(:get_features).with(app_data['name']).and_return(stub('response', body: features))
+ heroku.stub(:get_features).with(target_data['name']).and_return(stub('response', body: target_features))
+ Heroku::Command::Base.any_instance.stub(:api => heroku)
+
+ heroku.should_receive(:post_feature).with(['user_env_compile', true], target_data['name'])
+ heroku.should_receive(:delete_feature).with(['sigterm-all', true], target_data['name'])
+
+ execute("clone:features #{target_data['name']} -a #{app_data['name']}")
+ end
+ end
+ end
- config_vars = heroku.get_config_vars(app_data['name']).body
- new_config_vars = heroku.get_config_vars(new_app_name).body
+ end
+
+ RSpec::Matchers.define :have_same_config do |expected|
+ match do |actual|
+ expected_vars = heroku.get_config_vars(expected).body
+ actual_vars = heroku.get_config_vars(actual).body
- (config_vars.to_a - new_config_vars.to_a).should be_empty
+ (expected_vars.to_a - actual_vars.to_a).empty?
end
end
- it 'skips cloning the config variables when option is set' do
- with_app do |app_data|
- execute("apps:clone #{new_app_name} -a #{app_data['name']} -v")
+ RSpec::Matchers.define :have_same_collaborators do |expected|
+ match do |actual|
+ expected_collaborators = heroku.get_collaborators( expected ).body.map{|collab| collab['email']}
+ actual_collaborators = heroku.get_collaborators( actual ).body.map{|collab| collab['email']}
- config_vars = heroku.get_config_vars(app_data['name']).body
- new_config_vars = heroku.get_config_vars(new_app_name).body
+ (expected_collaborators - actual_collaborators).empty?
+ end
+ end
- (config_vars.to_a - new_config_vars.to_a).should_not be_empty
+ RSpec::Matchers.define :have_labs_feature do |expected|
+ match do |actual|
+ heroku.get_feature( expected, actual ).body['enabled']
end
end
View
50 spec/spec_helper.rb
@@ -8,25 +8,13 @@ module HerokuHelpers
MOCK = ENV['MOCK'] != 'false'
- def data_site_crt
- @data_site_crt ||= File.read(File.join(DATA_PATH, 'site.crt'))
- end
-
- def data_site_key
- @data_site_key ||= File.read(File.join(DATA_PATH, 'site.key'))
- end
-
def heroku
# ENV['HEROKU_API_KEY'] used for :api_key
Heroku::API.new(:mock => MOCK)
end
- def random_domain
- "#{random_name}.com"
- end
-
def random_name
- "akira-#{SecureRandom.hex(10)}"
+ "myapp-#{SecureRandom.hex(10)}"
end
def random_email_address
@@ -43,35 +31,43 @@ def with_app(params={}, &block)
with_blank_git_repository do
begin
data = heroku.post_app(random_params.merge(params)).body
- @name = data['name']
+ name = data['name']
- heroku.post_collaborator(@name, random_email_address)
- heroku.put_config_vars(@name, random_name.upcase => random_name)
+ heroku.post_collaborator(name, random_email_address)
+ heroku.put_config_vars(name, random_name.upcase => random_name)
ready = false
until ready
- ready = heroku.request(:method => :put, :path => "/apps/#{@name}/status").status == 201
+ ready = heroku.request(:method => :put, :path => "/apps/#{name}/status").status == 201
end
yield(data)
ensure
- heroku.delete_app(@name) rescue nil
+ heroku.delete_app(name) rescue nil
end
end
end
def with_blank_git_repository(&block)
- sandbox = File.join(Dir.tmpdir, "heroku", Process.pid.to_s)
- FileUtils.mkdir_p(sandbox)
+ if @has_blank_git_repo
+ block.call
+ else
+ begin
+ sandbox = File.join(Dir.tmpdir, "heroku", Process.pid.to_s)
+ FileUtils.mkdir_p(sandbox)
- old_dir = Dir.pwd
- Dir.chdir(sandbox)
+ old_dir = Dir.pwd
+ Dir.chdir(sandbox)
- `git init`
- block.call
+ `git init`
+ @has_blank_git_repo = true
+ block.call
- FileUtils.rm_rf(sandbox)
- ensure
- Dir.chdir(old_dir)
+ FileUtils.rm_rf(sandbox)
+ @has_blank_git_repo = false
+ ensure
+ Dir.chdir(old_dir)
+ end
+ end
end
def execute(command_line)
Please sign in to comment.
Something went wrong with that request. Please try again.