├── .rspec ├── Rakefile ├── lib └── dpl │ ├── version.rb │ ├── error.rb │ ├── provider │ ├── heroku │ │ ├── git_ssh.rb │ │ ├── git.rb │ │ ├── git_deploy_key.rb │ │ ├── api.rb │ │ ├── generic.rb │ │ └── anvil.rb │ ├── appfog.rb │ ├── modulus.rb │ ├── divshot.rb │ ├── ninefold.rb │ ├── bitballoon.rb │ ├── npm.rb │ ├── dot_cloud.rb │ ├── hackage.rb │ ├── heroku.rb │ ├── cloud66.rb │ ├── biicode.rb │ ├── cloud_foundry.rb │ ├── cloud_files.rb │ ├── puppet_forge.rb │ ├── nodejitsu.rb │ ├── rubygems.rb │ ├── openshift.rb │ ├── gcs.rb │ ├── pypi.rb │ ├── gae.rb │ ├── engine_yard.rb │ ├── deis.rb │ ├── code_deploy.rb │ ├── chef_supermarket.rb │ ├── cloudcontrol.rb │ ├── elastic_beanstalk.rb │ ├── s3.rb │ ├── ops_works.rb │ ├── releases.rb │ └── packagecloud.rb │ ├── cli.rb │ └── provider.rb ├── notes ├── engine_yard.md ├── dotcloud.md └── heroku.md ├── .gitignore ├── bin └── dpl ├── spec ├── spec_helper.rb ├── provider │ ├── heroku_git_deploy_key_spec.rb │ ├── divshot_spec.rb │ ├── modulus_spec.rb │ ├── bitballoon_spec.rb │ ├── npm.rb │ ├── gae_spec.rb │ ├── ninefold_spec.rb │ ├── appfog_spec.rb │ ├── dotcloud_spec.rb │ ├── heroku_api_spec.rb │ ├── packagecloud_spec.rb │ ├── hackage_spec.rb │ ├── cloudfoundry_spec.rb │ ├── chef_supermarket_spec.rb │ ├── cloud66_spec.rb │ ├── puppet_forge_spec.rb │ ├── elastic_beanstalk_spec.rb │ ├── cloud_files_spec.rb │ ├── openshift_spec.rb │ ├── heroku_anvil_spec.rb │ ├── pypi_spec.rb │ ├── deis_spec.rb │ ├── gcs_spec.rb │ ├── heroku_git_spec.rb │ ├── rubygems_spec.rb │ ├── ops_works_spec.rb │ ├── s3_spec.rb │ ├── cloudcontrol_spec.rb │ ├── releases_spec.rb │ └── code_deploy_spec.rb ├── cli_spec.rb └── provider_spec.rb ├── .travis.yml ├── LICENSE ├── Gemfile ├── dpl.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --tty 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task(:default) { ruby '-S rspec' } 2 | -------------------------------------------------------------------------------- /lib/dpl/version.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | VERSION = '1.7.9' 3 | end 4 | -------------------------------------------------------------------------------- /lib/dpl/error.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | Error = Class.new(StandardError) 3 | end 4 | -------------------------------------------------------------------------------- /notes/engine_yard.md: -------------------------------------------------------------------------------- 1 | EY has a a special deploy app. Get in touch with Kevin Holler if we don't hear back from them. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | .coverage 3 | .dpl 4 | setuptools*.zip 5 | google_appengine_*.zip 6 | google_appengine/* 7 | -------------------------------------------------------------------------------- /bin/dpl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 3 | 4 | require 'dpl/cli' 5 | DPL::CLI.run(ARGV) 6 | -------------------------------------------------------------------------------- /notes/dotcloud.md: -------------------------------------------------------------------------------- 1 | Destroy and redeploy a service: 2 | 3 | ``` 4 | dotcloud destroy -A 5 | dotcloud deploy latest 6 | ``` 7 | 8 | https://dotcloud.zendesk.com/requests/23637 9 | -------------------------------------------------------------------------------- /notes/heroku.md: -------------------------------------------------------------------------------- 1 | Heroku might send out emails for new deploy keys (doesn't do it for me, but for some others). 2 | 3 | Alternative is Anvil, but it's not perfect, as it duplicates a lot of Heroku logic internally (and failed for me in one case). -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'dpl/error' 3 | require 'dpl/provider' 4 | require 'rspec/its' 5 | 6 | SimpleCov.start do 7 | coverage_dir '.coverage' 8 | 9 | add_filter "/spec/" 10 | add_group 'Library', 'lib' 11 | end 12 | 13 | class DummyContext 14 | def shell(command) 15 | end 16 | 17 | def fold(message) 18 | yield 19 | end 20 | 21 | def env 22 | @env ||= {} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/provider/heroku_git_deploy_key_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'heroku-api' 3 | require 'dpl/provider/heroku' 4 | 5 | describe DPL::Provider::Heroku do 6 | subject(:provider) do 7 | described_class.new(DummyContext.new, :app => 'example', :key_name => 'key', :api_key => "foo", :strategy => "gitdeploykey") 8 | end 9 | 10 | describe "#ssh" do 11 | it "doesn't require an ssh key" do 12 | expect(provider.needs_key?).to eq(false) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/dpl/provider/heroku/git_ssh.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | module Heroku 4 | class GitSSH < Git 5 | def git_url 6 | info['git_url'] 7 | end 8 | 9 | def needs_key? 10 | true 11 | end 12 | 13 | def setup_key(file) 14 | api.post_key File.read(file) 15 | end 16 | 17 | def remove_key 18 | api.delete_key(option(:key_name)) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/dpl/provider/appfog.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class Appfog < Provider 4 | requires 'af', :load => 'vmc' 5 | 6 | def check_auth 7 | context.shell "af login --email=#{option(:email)} --password=#{option(:password)}" 8 | end 9 | 10 | def needs_key? 11 | false 12 | end 13 | 14 | def push_app 15 | context.shell "af update #{options[:app] || File.basename(Dir.getwd)}" 16 | context.shell "af logout" 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.0 6 | - "2.2" 7 | 8 | matrix: 9 | allow_failures: 10 | - rvm: "2.2" 11 | 12 | sudo: false 13 | 14 | cache: bundler 15 | 16 | deploy: 17 | provider: rubygems 18 | api_key: 19 | secure: ZmZoDL1tilWvQrqRWDm2K4xQ5Grt9eRJzYVZPLANR0P1TM5BJBLk+UhWCWCPkkDVIBWMa5ANsiFYBxtH65Lw+uqMztSpVusk0l0LQXZGv9jMpT9445A8008U3vvfS0ke7IG8Q4bMAC7Sd6VGaiHDyZC7zmNvnqMpmVX7ShcgBME= 20 | gem: dpl 21 | on: 22 | repo: travis-ci/dpl 23 | ruby: 1.9.3 24 | 25 | notifications: 26 | webhooks: "http://requestb.in/1guaq7t1" 27 | -------------------------------------------------------------------------------- /lib/dpl/provider/modulus.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class Modulus < Provider 4 | npm_g 'modulus' 5 | 6 | def check_auth 7 | raise Error, "must supply an api key" unless option(:api_key) 8 | end 9 | 10 | def check_app 11 | raise Error, "must supply a project name" unless option(:project_name) 12 | end 13 | 14 | def needs_key? 15 | false 16 | end 17 | 18 | def push_app 19 | context.shell "MODULUS_TOKEN=#{option(:api_key)} modulus deploy -p #{option(:project_name)}" 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/dpl/provider/divshot.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class Divshot < Provider 4 | npm_g 'divshot-cli', 'divshot' 5 | 6 | def check_auth 7 | raise Error, "must supply an api key" unless option(:api_key) 8 | end 9 | 10 | def check_app 11 | error "missing divshot.json" unless File.exist? "divshot.json" 12 | end 13 | 14 | def needs_key? 15 | false 16 | end 17 | 18 | def push_app 19 | context.shell "divshot push #{options[:environment] || "production"} --token=#{option(:api_key)}" 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/dpl/provider/ninefold.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class Ninefold < Provider 4 | requires 'ninefold' 5 | 6 | def check_auth 7 | raise Error, "must supply an auth token" unless option(:auth_token) 8 | end 9 | 10 | def check_app 11 | raise Error, "must supply an app ID" unless option(:app_id) 12 | end 13 | 14 | def needs_key? 15 | false 16 | end 17 | 18 | def push_app 19 | context.shell "AUTH_TOKEN=#{option(:auth_token)} APP_ID=#{option(:app_id)} ninefold app redeploy --robot --sure" 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/dpl/provider/bitballoon.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class BitBalloon < Provider 4 | requires 'bitballoon' 5 | 6 | def check_auth 7 | end 8 | 9 | def needs_key? 10 | false 11 | end 12 | 13 | def push_app 14 | command = 'bitballoon deploy' 15 | command << " ./#{option(:local_dir)}" if options.fetch(:local_dir,false) 16 | command << " --site-id=#{option(:site_id)}" if options[:site_id] 17 | command << " --access-token=#{option(:access_token)}" if options[:access_token] 18 | context.shell command 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/dpl/provider/heroku/git.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | module Heroku 4 | class Git < Generic 5 | requires 'netrc' 6 | 7 | def git_url 8 | "https://git.heroku.com/#{option(:app)}.git" 9 | end 10 | 11 | def push_app 12 | git_remote = options[:git] || git_url 13 | write_netrc if git_remote.start_with?("https://") 14 | context.shell "git push #{git_remote} HEAD:refs/heads/master -f" 15 | end 16 | 17 | def write_netrc 18 | n = Netrc.read 19 | n['git.heroku.com'] = [user, option(:api_key)] 20 | n.save 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/dpl/provider/npm.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class NPM < Provider 4 | NPMRC_FILE = '~/.npmrc' 5 | 6 | def needs_key? 7 | false 8 | end 9 | 10 | def check_app 11 | end 12 | 13 | def setup_auth 14 | File.open(File.expand_path(NPMRC_FILE), 'w') do |f| 15 | f.puts("_auth = #{option(:api_key)}") 16 | f.puts("email = #{option(:email)}") 17 | end 18 | end 19 | 20 | def check_auth 21 | setup_auth 22 | log "Authenticated with email #{option(:email)}" 23 | end 24 | 25 | def push_app 26 | context.shell "npm publish" 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/dpl/provider/heroku/git_deploy_key.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | module Heroku 4 | class GitDeployKey < GitSSH 5 | def needs_key? 6 | false 7 | end 8 | 9 | def check_auth 10 | super 11 | setup_git_ssh 12 | end 13 | 14 | def setup_git_ssh 15 | path = File.expand_path(".dpl/git-ssh") 16 | 17 | File.open(path, 'w') do |file| 18 | file.write "#!/bin/sh\n" 19 | file.write "exec ssh -o StrictHostKeychecking=no -o CheckHostIP=no -o UserKnownHostsFile=/dev/null -- \"$@\"\n" 20 | end 21 | 22 | chmod(0740, path) 23 | context.env['GIT_SSH'] = path 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/dpl/provider/dot_cloud.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class DotCloud < Provider 4 | experimental "dotCloud" 5 | pip 'dotcloud' 6 | 7 | def check_auth 8 | context.shell "echo #{option(:api_key)} | dotcloud setup --api-key" 9 | end 10 | 11 | def check_app 12 | context.shell "dotcloud connect #{option(:app)}" 13 | end 14 | 15 | def needs_key? 16 | false 17 | end 18 | 19 | def push_app 20 | context.shell "dotcloud push #{option(:app)}" 21 | end 22 | 23 | def run(command) 24 | service = options[:instance] || options[:service] || 'www' 25 | context.shell "dotcloud -A #{option(:app)} #{service} #{command}" 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/dpl/provider/hackage.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class Hackage < Provider 4 | apt_get 'cabal', 'cabal-install' 5 | 6 | def check_auth 7 | unless option(:username) and option(:password) 8 | raise Error, "must supply username and password" 9 | end 10 | end 11 | 12 | def check_app 13 | context.shell "cabal check" or raise Error, "cabal check failed" 14 | end 15 | 16 | def needs_key? 17 | false 18 | end 19 | 20 | def push_app 21 | context.shell "cabal sdist" or raise Error, "cabal sdist failed" 22 | Dir.glob("dist/*.tar.gz") do |tar| 23 | context.shell "cabal upload --username=#{option(:username)} --password=#{option(:password)} #{tar}" 24 | end 25 | end 26 | end 27 | end 28 | end 29 | 30 | -------------------------------------------------------------------------------- /lib/dpl/provider/heroku.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | module Heroku 4 | autoload :Anvil, 'dpl/provider/heroku/anvil' 5 | autoload :API, 'dpl/provider/heroku/api' 6 | autoload :Generic, 'dpl/provider/heroku/generic' 7 | autoload :Git, 'dpl/provider/heroku/git' 8 | autoload :GitSSH, 'dpl/provider/heroku/git_ssh' 9 | autoload :GitDeployKey, 'dpl/provider/heroku/git_deploy_key' 10 | 11 | extend self 12 | 13 | def new(context, options) 14 | strategy = options[:strategy] || 'api' 15 | constant = constants.detect { |c| c.to_s.downcase == strategy.downcase.gsub(/\W/, '') } 16 | raise Error, 'unknown strategy %p' % strategy unless constant and constant != Generic 17 | const_get(constant).new(context, options) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/dpl/provider/cloud66.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'net/https' 3 | 4 | module DPL 5 | class Provider 6 | class Cloud66 < Provider 7 | def needs_key? 8 | false 9 | end 10 | 11 | def push_app 12 | uri = URI.parse(redeployment_hook) 13 | 14 | response = webhook_call(uri.scheme, uri.host, uri.port, uri.path) 15 | 16 | error("Redeployment failed [#{response.code}]") if response.code != '200' 17 | end 18 | 19 | def check_auth 20 | end 21 | 22 | private 23 | 24 | def webhook_call(scheme, host, port, path) 25 | http = Net::HTTP.new(host, port) 26 | http.use_ssl = (scheme.downcase == 'https') 27 | 28 | request = Net::HTTP::Post.new(path) 29 | 30 | return http.request(request) 31 | end 32 | 33 | def redeployment_hook 34 | option(:redeployment_hook) 35 | end 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /lib/dpl/provider/biicode.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | module DPL 4 | class Provider 5 | class Biicode < Provider 6 | 7 | experimental 'Biicode' 8 | 9 | def self.install_biicode 10 | unless find_executable 'bii' 11 | context.shell "wget http://apt.biicode.com/install.sh -O install_biicode.sh && chmod +x install_biicode.sh && ./install_biicode.sh" 12 | end 13 | end 14 | 15 | install_biicode 16 | 17 | def needs_key? 18 | false 19 | end 20 | 21 | def check_app 22 | raise Error, "must supply a username" unless option(:user) 23 | raise Error, "must supply a password" unless option(:password) 24 | end 25 | 26 | def check_auth 27 | context.shell "bii user #{option(:user)} -p #{option(:password)}" 28 | end 29 | 30 | def push_app 31 | context.shell "bii publish --msg \"Travis publication\"" 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/provider/divshot_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/divshot' 3 | 4 | describe DPL::Provider::Divshot do 5 | subject :provider do 6 | described_class.new DummyContext.new, :api_key => 'abc123' 7 | end 8 | 9 | describe "#check_auth" do 10 | it 'should require an api key' do 11 | provider.options.update(:api_key => nil) 12 | expect{ provider.check_auth }.to raise_error("must supply an api key") 13 | end 14 | end 15 | 16 | describe "#push_app" do 17 | it 'should include the environment specified' do 18 | provider.options.update(:environment => 'development') 19 | expect(provider.context).to receive(:shell).with("divshot push development --token=abc123") 20 | provider.push_app 21 | end 22 | 23 | it 'should default to production' do 24 | expect(provider.context).to receive(:shell).with("divshot push production --token=abc123") 25 | provider.push_app 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/provider/modulus_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/modulus' 3 | 4 | describe DPL::Provider::Modulus do 5 | subject :provider do 6 | described_class.new(DummyContext.new, :api_key => 'test-token', :project_name => 'test-project') 7 | end 8 | 9 | describe "#check_auth" do 10 | it 'should require an api key' do 11 | provider.options.update(:api_key => nil) 12 | expect{ provider.check_auth }.to raise_error("must supply an api key") 13 | end 14 | end 15 | 16 | describe "#check_app" do 17 | it 'should require a project name' do 18 | provider.options.update(:project_name => nil) 19 | expect{ provider.check_app }.to raise_error("must supply a project name") 20 | end 21 | end 22 | 23 | describe "#push_app" do 24 | it 'should include the api key and project name specified' do 25 | expect(provider.context).to receive(:shell).with("MODULUS_TOKEN=test-token modulus deploy -p test-project") 26 | provider.push_app 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/provider/bitballoon_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/bitballoon' 3 | 4 | describe DPL::Provider::BitBalloon do 5 | subject :provider do 6 | described_class.new(DummyContext.new,{}) 7 | end 8 | 9 | describe "#needs_key?" do 10 | example do 11 | expect(provider.needs_key?).to eq(false) 12 | end 13 | end 14 | 15 | describe "#push_app" do 16 | example "Without optional parameters" do 17 | expect(provider.context).to receive(:shell).with("bitballoon deploy") 18 | provider.push_app 19 | end 20 | 21 | example "With optional parameters" do 22 | provider.options.update(local_dir: 'build') 23 | provider.options.update(access_token:'fake-access-token') 24 | provider.options.update(site_id:'fake-site') 25 | 26 | expected_command = "bitballoon deploy ./build --site-id=fake-site --access-token=fake-access-token" 27 | 28 | expect(provider.context).to receive(:shell).with(expected_command) 29 | provider.push_app 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/provider/npm.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/npm' 3 | 4 | describe DPL::Provider::NPM do 5 | subject :provider do 6 | described_class.new(DummyContext.new, :email => 'foo@blah.com', :api_key => 'test') 7 | end 8 | 9 | describe "#check_auth" do 10 | example do 11 | expect(provider).to receive(:setup_auth) 12 | expect(provider).to receive(:log).with("Authenticated with email foo@blah.com") 13 | provider.check_auth 14 | end 15 | end 16 | 17 | describe "#push_app" do 18 | example do 19 | expect(provider.context).to receive(:shell).with("npm publish") 20 | provider.push_app 21 | end 22 | end 23 | 24 | describe "#setup_auth" do 25 | example do 26 | f = double(:npmrc) 27 | expect(File).to receive(:open).with(File.expand_path(DPL::Provider::NPM::NPMRC_FILE)).and_return(f) 28 | expect(f).to receive(:puts).with("_auth = test") 29 | expect(f).to receive(:puts).with("email = foo@blah.com") 30 | provider.setup_auth 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/dpl/provider/cloud_foundry.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class CloudFoundry < Provider 4 | 5 | def initial_go_tools_install 6 | context.shell 'wget http://go-cli.s3-website-us-east-1.amazonaws.com/releases/latest/cf-cli_amd64.deb -qO temp.deb && sudo dpkg -i temp.deb' 7 | context.shell 'rm temp.deb' 8 | end 9 | 10 | def check_auth 11 | initial_go_tools_install 12 | context.shell "cf api #{option(:api)}" 13 | context.shell "cf login --u #{option(:username)} --p #{option(:password)} --o #{option(:organization)} --s #{option(:space)}" 14 | end 15 | 16 | def check_app 17 | error 'Application must have a manifest.yml for unattended deployment' unless File.exists? 'manifest.yml' 18 | end 19 | 20 | def needs_key? 21 | false 22 | end 23 | 24 | def push_app 25 | context.shell "cf push" 26 | context.shell "cf logout" 27 | end 28 | 29 | def cleanup 30 | end 31 | 32 | def uncleanup 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/dpl/provider/cloud_files.rb: -------------------------------------------------------------------------------- 1 | require 'dpl/provider' 2 | 3 | module DPL 4 | class Provider 5 | class CloudFiles < Provider 6 | requires 'fog' 7 | experimental 'Rackspace Cloud Files' 8 | 9 | def needs_key? 10 | false 11 | end 12 | 13 | def api 14 | @api ||= Fog::Storage.new(:provider => 'Rackspace', :rackspace_username => option(:username), :rackspace_api_key => option(:api_key), :rackspace_region => option(:region)) 15 | end 16 | 17 | def check_auth 18 | log "Authenticated as #{option(:username)}" 19 | end 20 | 21 | def push_app 22 | container = api.directories.get(option(:container)) 23 | 24 | raise Error, 'The specified container does not exist.' if container.nil? 25 | 26 | glob_args = ['**/*'] 27 | glob_args << File::FNM_DOTMATCH if options[:dot_match] 28 | 29 | Dir.glob(*glob_args).each do |name| 30 | container.files.create(:key => name, :body => File.open(name)) unless File.directory?(name) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/provider/gae_spec.rb: -------------------------------------------------------------------------------- 1 | # require 'spec_helper' 2 | # require 'dpl/provider/gae' 3 | # 4 | # describe DPL::Provider::GAE do 5 | # subject :provider do 6 | # described_class.new(DummyContext.new, :user => 'foo', :password => 'bar') 7 | # end 8 | # 9 | # let(:token) { 'deadbeef012345' } 10 | # 11 | # describe '#push_app' do 12 | # example 'with default app_dir' do 13 | # provider.context.env['TRAVIS_BUILD_DIR'] = Dir.pwd 14 | # provider.options.update(:oauth_refresh_token => token) 15 | # expect(provider.context).to receive(:shell).with("#{DPL::Provider::GAE::APPCFG_BIN} --oauth2_refresh_token=#{token} update #{Dir.pwd}").and_return(true) 16 | # provider.push_app 17 | # end 18 | # 19 | # example 'with custom app_dir' do 20 | # app_dir='foo' 21 | # provider.options.update(:oauth_refresh_token => token, :app_dir => app_dir) 22 | # expect(provider.context).to receive(:shell).with("#{DPL::Provider::GAE::APPCFG_BIN} --oauth2_refresh_token=#{token} update #{app_dir}").and_return(true) 23 | # provider.push_app 24 | # end 25 | # end 26 | # end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Konstantin Haase 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /spec/provider/ninefold_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/ninefold' 3 | 4 | describe DPL::Provider::Ninefold do 5 | subject :provider do 6 | described_class.new(DummyContext.new, :auth_token => "123456789", :app_id => "1234") 7 | end 8 | 9 | describe "#check_auth" do 10 | it 'requires an auth token' do 11 | provider.options.update(:auth_token => nil) 12 | expect{ provider.check_auth }.to raise_error "must supply an auth token" 13 | end 14 | end 15 | 16 | describe "#check_app" do 17 | it 'requires an app ID' do 18 | provider.options.update(:app_id => nil) 19 | expect{ provider.check_app }.to raise_error "must supply an app ID" 20 | end 21 | end 22 | 23 | describe "#needs_key?" do 24 | it 'returns false' do 25 | expect(provider.needs_key?).to be_falsey 26 | end 27 | end 28 | 29 | describe "#push_app" do 30 | it 'includes the auth token and app ID specified' do 31 | expect(provider.context).to receive(:shell).with("AUTH_TOKEN=123456789 APP_ID=1234 ninefold app redeploy --robot --sure") 32 | provider.push_app 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/dpl/provider/puppet_forge.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class PuppetForge < Provider 4 | require 'pathname' 5 | 6 | requires 'puppet', :load => 'puppet/face' 7 | requires 'puppet-blacksmith', :load => 'puppet_blacksmith' 8 | 9 | def modulefile 10 | @modulefile ||= Blacksmith::Modulefile.new 11 | end 12 | 13 | def forge 14 | @forge ||= Blacksmith::Forge.new(options[:user], options[:password], options[:url]) 15 | end 16 | 17 | def build 18 | pmod = Puppet::Face['module', :current] 19 | pmod.build('./') 20 | end 21 | 22 | def needs_key? 23 | false 24 | end 25 | 26 | def check_app 27 | modulefile.metadata 28 | end 29 | 30 | def check_auth 31 | raise Error, "must supply a user" unless option(:user) 32 | raise Error, "must supply a password" unless option(:password) 33 | end 34 | 35 | def push_app 36 | build 37 | log "Uploading to Puppet Forge #{forge.username}/#{modulefile.name}" 38 | forge.push!(modulefile.name) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/provider/appfog_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/appfog' 3 | 4 | describe DPL::Provider::Appfog do 5 | subject :provider do 6 | described_class.new(DummyContext.new, :email => 'blah@foo.com', :password => 'bar') 7 | end 8 | 9 | describe "#check_auth" do 10 | example do 11 | expect(provider.context).to receive(:shell).with("af login --email=blah@foo.com --password=bar") 12 | provider.check_auth 13 | end 14 | end 15 | 16 | describe "#needs_key?" do 17 | example do 18 | expect(provider.needs_key?).to eq(false) 19 | end 20 | end 21 | 22 | describe "#push_app" do 23 | example "Without :app" do 24 | expect(provider.context).to receive(:shell).with("af update #{File.basename(Dir.getwd)}") 25 | expect(provider.context).to receive(:shell).with("af logout") 26 | provider.push_app 27 | end 28 | example "With :app" do 29 | provider.options.update(:app => 'test') 30 | expect(provider.context).to receive(:shell).with("af update test") 31 | expect(provider.context).to receive(:shell).with("af logout") 32 | provider.push_app 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/provider/dotcloud_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/dot_cloud' 3 | 4 | describe DPL::Provider::DotCloud do 5 | subject :provider do 6 | described_class.new(DummyContext.new, :app => 'example', :api_key => 'foo') 7 | end 8 | 9 | describe "#check_auth" do 10 | example do 11 | expect(provider.context).to receive(:shell).with("echo foo | dotcloud setup --api-key") 12 | provider.check_auth 13 | end 14 | end 15 | 16 | describe "#check_app" do 17 | example do 18 | expect(provider.context).to receive(:shell).with("dotcloud connect example") 19 | provider.check_app 20 | end 21 | end 22 | 23 | describe "#needs_key?" do 24 | example do 25 | expect(provider.needs_key?).to eq(false) 26 | end 27 | end 28 | 29 | describe "#push_app" do 30 | example do 31 | expect(provider.context).to receive(:shell).with("dotcloud push example") 32 | provider.push_app 33 | end 34 | end 35 | 36 | describe "#run" do 37 | example do 38 | expect(provider.context).to receive(:shell).with("dotcloud -A example www test") 39 | provider.run("test") 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/provider/heroku_api_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'heroku-api' 3 | require 'dpl/provider/heroku' 4 | 5 | describe DPL::Provider::Heroku do 6 | subject(:provider) do 7 | described_class.new(DummyContext.new, :app => 'example', :key_name => 'key', :api_key => "foo", :strategy => "api") 8 | end 9 | 10 | let(:expected_headers) do 11 | { "User-Agent" => "dpl/#{DPL::VERSION} heroku-rb/#{Heroku::API::VERSION}" } 12 | end 13 | 14 | describe "#ssh" do 15 | it "doesn't require an ssh key" do 16 | expect(provider.needs_key?).to eq(false) 17 | end 18 | end 19 | 20 | describe "#api" do 21 | it 'accepts an api key' do 22 | api = double(:api) 23 | expect(::Heroku::API).to receive(:new).with(:api_key => "foo", :headers => expected_headers).and_return(api) 24 | expect(provider.api).to eq(api) 25 | end 26 | 27 | it 'accepts a user and a password' do 28 | api = double(:api) 29 | provider.options.update(:user => "foo", :password => "bar") 30 | expect(::Heroku::API).to receive(:new).with(:user => "foo", :password => "bar", :headers => expected_headers).and_return(api) 31 | expect(provider.api).to eq(api) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | platforms :mri_19 do 5 | gem 'slop', '~> 3.6.0' 6 | gem 'ohai', '~> 7.4.0' 7 | end 8 | 9 | group :heroku do 10 | gem 'rendezvous', '~> 0.0.2' 11 | gem 'heroku-api', '= 0.3.16' 12 | gem 'anvil-cli', '~> 0.16.1' 13 | end 14 | 15 | group :openshift do 16 | gem 'rhc' 17 | gem 'httpclient' 18 | end 19 | 20 | group :appfog do 21 | gem 'af' 22 | end 23 | 24 | group :rubygems do 25 | gem 'gems' 26 | end 27 | 28 | group :sss do 29 | gem 'aws-sdk-v1' 30 | gem 'mime-types' 31 | end 32 | 33 | group :code_deploy do 34 | gem 'aws-sdk', '2.0.13.pre' 35 | end 36 | 37 | group :cloud_files do 38 | gem 'fog' 39 | end 40 | 41 | group :releases do 42 | gem 'octokit' 43 | end 44 | 45 | group :ninefold do 46 | gem 'ninefold' 47 | end 48 | 49 | group :gcs do 50 | gem 'gstore' 51 | gem 'mime-types' 52 | end 53 | 54 | group :gae do 55 | gem 'rubyzip' 56 | end 57 | 58 | group :elastic_beanstalk do 59 | gem 'rubyzip' 60 | gem 'aws-sdk-v1' 61 | end 62 | 63 | group :bitballoon do 64 | gem 'bitballoon' 65 | end 66 | 67 | group :puppet_forge do 68 | gem 'puppet' 69 | gem 'puppet-blacksmith' 70 | end 71 | 72 | group :packagecloud do 73 | gem 'packagecloud-ruby', '= 0.2.17' 74 | end 75 | 76 | group :chef_supermarket do 77 | gem 'chef' 78 | end 79 | -------------------------------------------------------------------------------- /spec/provider/packagecloud_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rubygems' 3 | require 'gems' 4 | require 'dpl/provider/packagecloud' 5 | 6 | describe DPL::Provider::Packagecloud do 7 | 8 | subject :provider do 9 | described_class.new(DummyContext.new, :username => 'joedamato', :repository => 'test_repo', :token => 'test_token') 10 | end 11 | 12 | describe "#setup_auth" do 13 | it 'should get username and token' do 14 | expect(provider).to receive(:log).with("Logging into https://packagecloud.io with joedamato:****************oken") 15 | provider.setup_auth 16 | end 17 | 18 | it 'should require username' do 19 | new_provider = described_class.new(DummyContext.new, {:token => 'test_token'}) 20 | expect{ new_provider.setup_auth }.to raise_error("missing username") 21 | end 22 | 23 | it 'should require token' do 24 | new_provider = described_class.new(DummyContext.new, {:username => 'test_token'}) 25 | expect{ new_provider.setup_auth }.to raise_error("missing token") 26 | end 27 | 28 | it 'should require repository' do 29 | new_provider = described_class.new(DummyContext.new, {:username => 'joedamato', :token => 'test_token'}) 30 | expect{ new_provider.setup_auth }.to raise_error("missing repository") 31 | end 32 | 33 | end 34 | 35 | end -------------------------------------------------------------------------------- /lib/dpl/provider/nodejitsu.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class Nodejitsu < Provider 4 | CONFIG_FILE = '.dpl/jitsu.json' 5 | requires 'json' 6 | npm_g 'jitsu' 7 | 8 | def config 9 | { 10 | "username" => option(:username, :user_name, :user), 11 | "apiToken" => option(:api_key), 12 | "apiTokenName" => "travis" 13 | } 14 | end 15 | 16 | def check_auth 17 | File.open(CONFIG_FILE, 'w') { |f| f << config.to_json } 18 | end 19 | 20 | def check_app 21 | error "missing package.json" unless File.exist? 'package.json' 22 | 23 | package = JSON.parse File.read('package.json') 24 | message = "missing %s in package.json, see https://www.nodejitsu.com/documentation/appendix/package-json/" 25 | error message % "subdomain" unless package['subdomain'] 26 | error message % "node version" unless package['engines'] and package['engines']['node'] 27 | error message % "start script" unless package['scripts'] and package['scripts']['start'] 28 | end 29 | 30 | def needs_key? 31 | false 32 | end 33 | 34 | def push_app 35 | context.shell "jitsu deploy --jitsuconf #{File.expand_path(CONFIG_FILE)} --release=yes" 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /dpl.gemspec: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../lib", __FILE__) 2 | require "dpl/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "dpl" 6 | s.version = DPL::VERSION 7 | s.author = "Konstantin Haase" 8 | s.email = "konstantin.mailinglists@googlemail.com" 9 | s.homepage = "https://github.com/travis-ci/dpl" 10 | s.summary = %q{deploy tool} 11 | s.description = %q{deploy tool abstraction for clients} 12 | s.license = 'MIT' 13 | s.files = `git ls-files`.split("\n") 14 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 15 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 16 | s.require_path = 'lib' 17 | s.required_ruby_version = '>= 1.9.3' 18 | 19 | s.add_development_dependency 'rspec', '~> 3.0.0' 20 | s.add_development_dependency 'rspec-its' 21 | s.add_development_dependency 'rake' 22 | s.add_development_dependency 'simplecov' 23 | s.add_development_dependency 'json' 24 | 25 | # prereleases from Travis CI 26 | if ENV['CI'] 27 | digits = s.version.to_s.split '.' 28 | digits[-1] = digits[-1].to_s.succ 29 | s.version = digits.join('.') + ".travis.#{ENV['TRAVIS_JOB_NUMBER']}" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/dpl/provider/rubygems.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class RubyGems < Provider 4 | requires 'gems', version: '>= 0.8.3' 5 | 6 | def setup_auth 7 | ::Gems.key = option(:api_key) if options[:api_key] 8 | ::Gems.username = option(:user) unless options[:api_key] 9 | ::Gems.password = option(:password) unless options[:api_key] 10 | end 11 | 12 | def needs_key? 13 | false 14 | end 15 | 16 | def setup_gem 17 | options[:gem] ||= options[:app] 18 | end 19 | 20 | def gemspec 21 | options[:gemspec].gsub('.gemspec', '') if options[:gemspec] 22 | end 23 | 24 | def check_app 25 | setup_auth 26 | setup_gem 27 | info = ::Gems.info(options[:gem]) 28 | log "Found gem #{info['name']}" 29 | end 30 | 31 | def check_auth 32 | setup_auth 33 | log "Authenticated with username #{::Gems.username}" if ::Gems.username 34 | end 35 | 36 | def push_app 37 | setup_auth 38 | setup_gem 39 | context.shell "gem build #{gemspec || option(:gem)}.gemspec" 40 | Dir.glob("#{gemspec || option(:gem)}-*.gem") do |f| 41 | if options[:host] 42 | log ::Gems.push(File.new(f), options[:host]) 43 | else 44 | log ::Gems.push(File.new f) 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/provider/hackage_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/hackage' 3 | 4 | describe DPL::Provider::Hackage do 5 | subject :provider do 6 | described_class.new(DummyContext.new, :username => 'FooUser', :password => 'bar') 7 | end 8 | 9 | describe "#check_auth" do 10 | it 'should require username' do 11 | provider.options.update(:username => nil) 12 | expect { 13 | provider.check_auth 14 | }.to raise_error(DPL::Error) 15 | end 16 | 17 | it 'should require password' do 18 | provider.options.update(:password => nil) 19 | expect { 20 | provider.check_auth 21 | }.to raise_error(DPL::Error) 22 | end 23 | end 24 | 25 | describe "#check_app" do 26 | it 'calls cabal' do 27 | expect(provider.context).to receive(:shell).with("cabal check").and_return(true) 28 | provider.check_app 29 | end 30 | 31 | it 'fails when cabal complains' do 32 | expect(provider.context).to receive(:shell).with("cabal check").and_return(false) 33 | expect { 34 | provider.check_app 35 | }.to raise_error(DPL::Error) 36 | end 37 | end 38 | 39 | describe "#push_app" do 40 | example do 41 | expect(provider.context).to receive(:shell).with("cabal sdist").and_return(true) 42 | expect(Dir).to receive(:glob).and_yield('dist/package-0.1.2.3.tar.gz') 43 | expect(provider.context).to receive(:shell).with("cabal upload --username=FooUser --password=bar dist/package-0.1.2.3.tar.gz") 44 | provider.push_app 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/cli' 3 | 4 | describe DPL::CLI do 5 | describe "#options" do 6 | example { expect(described_class.new.options[:app]) .to eq(File.basename(Dir.pwd)) } 7 | example { expect(described_class.new(:app => 'foo') .options[:app]).to eq('foo') } 8 | example { expect(described_class.new("--app=foo") .options[:app]).to eq('foo') } 9 | example { expect(described_class.new("--app") .options[:app]).to eq(true) } 10 | example { expect(described_class.new("--app=foo", "--app=bar") .options[:app]).to eq(['foo', 'bar']) } 11 | 12 | example "error handling" do 13 | expect($stderr).to receive(:puts).with('invalid option "app"') 14 | expect { described_class.new("app") }.to raise_error(SystemExit) 15 | end 16 | end 17 | 18 | describe "#run" do 19 | example "triggers deploy" do 20 | provider = double('provider') 21 | expect(DPL::Provider).to receive(:new).and_return(provider) 22 | expect(provider).to receive(:deploy) 23 | 24 | described_class.run("--provider=foo") 25 | end 26 | 27 | example "error handling" do 28 | expect($stderr).to receive(:puts).with('missing provider') 29 | expect { described_class.run }.to raise_error(SystemExit) 30 | end 31 | 32 | example "error handling in debug mode" do 33 | expect { described_class.run("--debug") }.to raise_error(DPL::Error, 'missing provider') 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/provider/cloudfoundry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/cloud_foundry' 3 | 4 | describe DPL::Provider::CloudFoundry do 5 | subject :provider do 6 | described_class.new(DummyContext.new, api: 'api.run.awesome.io', username: 'mallomar', 7 | password: 'myreallyawesomepassword', 8 | organization: 'myorg', 9 | space: 'outer') 10 | end 11 | 12 | describe "#check_auth" do 13 | example do 14 | expect(provider.context).to receive(:shell).with('wget http://go-cli.s3-website-us-east-1.amazonaws.com/releases/latest/cf-cli_amd64.deb -qO temp.deb && sudo dpkg -i temp.deb') 15 | expect(provider.context).to receive(:shell).with('rm temp.deb') 16 | expect(provider.context).to receive(:shell).with('cf api api.run.awesome.io') 17 | expect(provider.context).to receive(:shell).with('cf login --u mallomar --p myreallyawesomepassword --o myorg --s outer') 18 | provider.check_auth 19 | end 20 | end 21 | 22 | describe "#check_app" do 23 | example do 24 | expect{provider.check_app}.to raise_error('Application must have a manifest.yml for unattended deployment') 25 | end 26 | end 27 | 28 | describe "#needs_key?" do 29 | example do 30 | expect(provider.needs_key?).to eq(false) 31 | end 32 | end 33 | 34 | describe "#push_app" do 35 | example do 36 | expect(provider.context).to receive(:shell).with('cf push') 37 | expect(provider.context).to receive(:shell).with('cf logout') 38 | provider.push_app 39 | 40 | end 41 | end 42 | end -------------------------------------------------------------------------------- /lib/dpl/provider/openshift.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class Openshift < Provider 4 | requires 'httpclient', version: '~> 2.4.0' 5 | requires 'rhc' 6 | 7 | def initialize(context, options) 8 | super 9 | @deployment_branch = options[:deployment_branch] 10 | end 11 | 12 | def api 13 | @api ||= ::RHC::Rest::Client.new(:user => option(:user), :password => option(:password), :server => 'openshift.redhat.com') 14 | end 15 | 16 | def user 17 | @user ||= api.user.login 18 | end 19 | 20 | def app 21 | @app ||= api.find_application(option(:domain), option(:app)) 22 | end 23 | 24 | def check_auth 25 | log "authenticated as %s" % user 26 | end 27 | 28 | def check_app 29 | log "found app #{app.name}" 30 | end 31 | 32 | def setup_key(file, type = nil) 33 | specified_type, content, comment = File.read(file).split 34 | api.add_key(option(:key_name), content, type || specified_type) 35 | end 36 | 37 | def remove_key 38 | api.delete_key(option(:key_name)) 39 | end 40 | 41 | def push_app 42 | if @deployment_branch 43 | log "deployment_branch detected: #{@deployment_branch}" 44 | app.deployment_branch = @deployment_branch 45 | context.shell "git push #{app.git_url} -f #{app.deployment_branch}" 46 | else 47 | context.shell "git push #{app.git_url} -f" 48 | end 49 | end 50 | 51 | def restart 52 | app.restart 53 | end 54 | 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/provider/chef_supermarket_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'chef/cookbook_loader' 3 | require 'chef/cookbook_uploader' 4 | require 'dpl/provider/chef_supermarket' 5 | 6 | describe DPL::Provider::ChefSupermarket do 7 | subject :provider do 8 | described_class.new( 9 | DummyContext.new, 10 | app: 'example', 11 | cookbook_category: 'Others', 12 | user_id: 'user', 13 | client_key: '/tmp/example.pem' 14 | ) 15 | end 16 | 17 | let(:cookbook_uploader) do 18 | double('cookbook_uploader', validate_cookbooks: true) 19 | end 20 | 21 | let(:http_resp) do 22 | double('http_resp', body: '{}', code: '201') 23 | end 24 | 25 | describe "#check_auth" do 26 | example do 27 | ::File.stub(:exist?).and_return(true) 28 | expect(File).to receive(:exist?) 29 | provider.check_auth 30 | end 31 | end 32 | 33 | describe "#check_app" do 34 | example do 35 | ::Chef::CookbookLoader.any_instance.stub(:[]).and_return nil 36 | expect(::Chef::CookbookUploader).to receive(:new).and_return(cookbook_uploader) 37 | provider.check_app 38 | end 39 | end 40 | 41 | describe "#push_app" do 42 | example do 43 | expect(::Chef::CookbookSiteStreamingUploader).to receive(:create_build_dir).and_return('/tmp/build_dir') 44 | expect(provider).to receive(:system).and_return(true) 45 | expect(::File).to receive(:open) 46 | expect(::Chef::CookbookSiteStreamingUploader).to receive(:post).and_return(http_resp) 47 | expect(::FileUtils).to receive(:rm_rf).with('/tmp/build_dir') 48 | provider.push_app 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/provider/cloud66_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/cloud66' 3 | 4 | describe DPL::Provider::Cloud66 do 5 | subject :provider do 6 | described_class.new(DummyContext.new, options) 7 | end 8 | 9 | let(:successful_response){ double(code: '200') } 10 | let(:not_found_response){ double(code: '404') } 11 | let(:options){ {} } 12 | 13 | describe "#push_app" do 14 | context 'with a successful response' do 15 | let(:options){ {:redeployment_hook => 'https://hooks.cloud66.com/stacks/redeploy/0101010101010101'} } 16 | 17 | example do 18 | expect(provider).to receive(:webhook_call).with('https', 'hooks.cloud66.com', 443, '/stacks/redeploy/0101010101010101').and_return(successful_response) 19 | provider.push_app 20 | end 21 | end 22 | 23 | context 'with a 404 response' do 24 | let(:options){ {:redeployment_hook => 'https://hooks.cloud66.com/stacks/redeploy/0101010101010101'} } 25 | 26 | it 'should raise an error' do 27 | expect(provider).to receive(:webhook_call).with('https', 'hooks.cloud66.com', 443, '/stacks/redeploy/0101010101010101').and_return(not_found_response) 28 | expect { provider.push_app }.to raise_error(DPL::Error, "Redeployment failed [404]") 29 | end 30 | end 31 | 32 | context 'with missing redeployment_hook option' do 33 | it 'should raise an error' do 34 | expect { provider.push_app }.to raise_error(DPL::Error, "missing redeployment_hook") 35 | end 36 | end 37 | end 38 | 39 | describe "#needs_key?" do 40 | example do 41 | expect(provider.needs_key?).to eq(false) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/dpl/cli.rb: -------------------------------------------------------------------------------- 1 | require 'dpl/error' 2 | require 'dpl/provider' 3 | 4 | module DPL 5 | class CLI 6 | def self.run(*args) 7 | new(args).run 8 | end 9 | 10 | OPTION_PATTERN = /\A--([a-z][a-z_\-]*)(?:=(.+))?\z/ 11 | attr_accessor :options, :fold_count 12 | 13 | def initialize(*args) 14 | options = {} 15 | args.flatten.each do |arg| 16 | next options.update(arg) if arg.is_a? Hash 17 | die("invalid option %p" % arg) unless match = OPTION_PATTERN.match(arg) 18 | key = match[1].tr('-', '_').to_sym 19 | if options.include? key 20 | options[key] = Array(options[key]) << match[2] 21 | else 22 | options[key] = match[2] || true 23 | end 24 | end 25 | 26 | self.fold_count = 0 27 | self.options = default_options.merge(options) 28 | end 29 | 30 | def run 31 | provider = Provider.new(self, options) 32 | provider.deploy 33 | rescue Error => error 34 | options[:debug] ? raise(error) : die(error.message) 35 | end 36 | 37 | def fold(message) 38 | self.fold_count += 1 39 | print "travis_fold:start:dpl.#{fold_count}\r" if options[:fold] 40 | puts "\e[33m#{message}\e[0m" 41 | yield 42 | ensure 43 | print "\ntravis_fold:end:dpl.#{fold_count}\r" if options[:fold] 44 | end 45 | 46 | def default_options 47 | { 48 | :app => File.basename(Dir.pwd), 49 | :key_name => %x[hostname].strip 50 | } 51 | end 52 | 53 | def shell(command) 54 | system(command) 55 | end 56 | 57 | def die(message) 58 | $stderr.puts(message) 59 | exit 1 60 | end 61 | 62 | def env 63 | ENV 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/dpl/provider/gcs.rb: -------------------------------------------------------------------------------- 1 | require 'kconv' 2 | 3 | module DPL 4 | class Provider 5 | class GCS < Provider 6 | requires 'gstore' 7 | requires 'mime-types' 8 | 9 | def needs_key? 10 | false 11 | end 12 | 13 | def client 14 | @client ||= GStore::Client.new( 15 | :access_key => option(:access_key_id), 16 | :secret_key => option(:secret_access_key) 17 | ) 18 | end 19 | 20 | def check_auth 21 | log "Logging in with Access Key: #{option(:access_key_id)[-4..-1].rjust(20, '*')}" 22 | end 23 | 24 | def upload_path(filename) 25 | [options[:upload_dir], filename].compact.join("/") 26 | end 27 | 28 | def push_app 29 | glob_args = ["**/*"] 30 | glob_args << File::FNM_DOTMATCH if options[:dot_match] 31 | Dir.chdir(options.fetch(:local_dir, Dir.pwd)) do 32 | Dir.glob(*glob_args) do |filename| 33 | next if File.directory?(filename) 34 | content_type = MIME::Types.type_for(filename).first.to_s 35 | opts = { :"Content-Type" => content_type }.merge(encoding_option_for(filename)) 36 | opts["Cache-Control"] = options[:cache_control] if options[:cache_control] 37 | opts["x-goog-acl"] = options[:acl] if options[:acl] 38 | 39 | client.put_object( 40 | option(:bucket), 41 | upload_path(filename), 42 | { :data => File.read(filename), :headers => opts } 43 | ) 44 | end 45 | end 46 | end 47 | 48 | private 49 | def encoding_option_for(path) 50 | if detect_encoding? && encoding_for(path) 51 | {"Content-Encoding" => encoding_for(path)} 52 | else 53 | {} 54 | end 55 | end 56 | 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/dpl/provider/heroku/api.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'shellwords' 3 | 4 | module DPL 5 | class Provider 6 | module Heroku 7 | class API < Generic 8 | def push_app 9 | pack_archive 10 | upload_archive 11 | trigger_build 12 | end 13 | 14 | def archive_file 15 | Shellwords.escape("#{context.env['HOME']}/.dpl.#{option(:app)}.tgz") 16 | end 17 | 18 | def pack_archive 19 | log "creating application archive" 20 | context.shell "tar -zcf #{archive_file} ." 21 | end 22 | 23 | def upload_archive 24 | log "uploading application archive" 25 | context.shell "curl #{Shellwords.escape(put_url)} -X PUT -H 'Content-Type:' --data-binary @#{archive_file}" 26 | end 27 | 28 | def trigger_build 29 | log "triggering new deployment" 30 | response = post(:builds, source_blob: { url: get_url, version: version }) 31 | stream_url = response.fetch('stream_url') 32 | context.shell "curl #{Shellwords.escape(stream_url)}" 33 | end 34 | 35 | def get_url 36 | source_blob.fetch("get_url") 37 | end 38 | 39 | def put_url 40 | source_blob.fetch("put_url") 41 | end 42 | 43 | def source_blob 44 | @source_blog ||= post(:sources).fetch("source_blob") 45 | end 46 | 47 | def version 48 | @version ||= options[:version] || context.env['TRAVIS_COMMIT'] || `git rev-parse HEAD`.strip 49 | end 50 | 51 | def post(subpath, body = nil, options = {}) 52 | options = { 53 | method: :post, 54 | path: "/apps/#{option(:app)}/#{subpath}", 55 | headers: { "Accept" => "application/vnd.heroku+json; version=edge" }, 56 | expects: [200, 201] 57 | }.merge(options) 58 | 59 | if body 60 | options[:body] = JSON.dump(body) 61 | options[:headers]['Content-Type'] = 'application/json' 62 | end 63 | 64 | api.request(options).body 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/dpl/provider/heroku/generic.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | module Heroku 4 | class Generic < Provider 5 | requires 'heroku-api' 6 | requires 'rendezvous' 7 | 8 | def needs_key? 9 | false 10 | end 11 | 12 | def api 13 | @api ||= ::Heroku::API.new(api_options) 14 | end 15 | 16 | def api_options 17 | api_options = { headers: { 'User-Agent' => user_agent(::Heroku::API::HEADERS.fetch('User-Agent')) } } 18 | if options[:user] and options[:password] 19 | api_options[:user] = options[:user] 20 | api_options[:password] = options[:password] 21 | else 22 | api_options[:api_key] = option(:api_key) 23 | end 24 | api_options 25 | end 26 | 27 | def user 28 | @user ||= api.get_user.body["email"] 29 | end 30 | 31 | def check_auth 32 | log "authenticated as %s" % user 33 | end 34 | 35 | def info 36 | @info ||= api.get_app(option(:app)).body 37 | end 38 | 39 | def check_app 40 | log "checking for app '#{option(:app)}'" 41 | log "found app '#{info['name']}'" 42 | rescue ::Heroku::API::Errors::Forbidden => error 43 | raise Error, "#{error.message} (does the app '#{option(:app)}' exist and does your account have access to it?)", error.backtrace 44 | end 45 | 46 | def run(command) 47 | data = api.post_ps(option(:app), command, :attach => true).body 48 | rendezvous_url = data['rendezvous_url'] 49 | Rendezvous.start(:url => rendezvous_url) unless rendezvous_url.nil? 50 | end 51 | 52 | def restart 53 | api.post_ps_restart option(:app) 54 | end 55 | 56 | def deploy 57 | super 58 | rescue ::Heroku::API::Errors::NotFound => error 59 | raise Error, "#{error.message} (wrong app #{options[:app].inspect}?)", error.backtrace 60 | rescue ::Heroku::API::Errors::Unauthorized => error 61 | raise Error, "#{error.message} (wrong API key?)", error.backtrace 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/dpl/provider/pypi.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class PyPI < Provider 4 | DEFAULT_SERVER = 'http://pypi.python.org/pypi' 5 | PYPIRC_FILE = '~/.pypirc' 6 | 7 | def self.install_setuptools 8 | shell 'wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | sudo python' 9 | shell 'rm -f setuptools-*.tar.gz' 10 | end 11 | 12 | def initialize(*args) 13 | super(*args) 14 | self.class.pip 'wheel' if options[:distributions].to_s.include? 'bdist_wheel' 15 | end 16 | 17 | install_setuptools 18 | 19 | def config 20 | { 21 | :header => '[distutils]', 22 | :servers_line => 'index-servers = pypi', 23 | :servers => { 24 | 'pypi' => [ 25 | "repository: #{options[:server] || DEFAULT_SERVER}", 26 | "username: #{option(:user)}", 27 | "password: #{option(:password)}", 28 | ] 29 | } 30 | } 31 | end 32 | 33 | def write_servers(f) 34 | config[:servers].each do |key, val| 35 | f.puts " " * 4 + key 36 | end 37 | 38 | config[:servers].each do |key, val| 39 | f.puts "[#{key}]" 40 | f.puts val 41 | end 42 | end 43 | 44 | def write_config 45 | File.open(File.expand_path(PYPIRC_FILE), 'w') do |f| 46 | config.each do |key, val| 47 | f.puts(val) if val.is_a? String or val.is_a? Array 48 | end 49 | write_servers(f) 50 | end 51 | end 52 | 53 | def check_auth 54 | write_config 55 | log "Authenticated as #{option(:user)}" 56 | end 57 | 58 | def check_app 59 | end 60 | 61 | def needs_key? 62 | false 63 | end 64 | 65 | def push_app 66 | context.shell "python setup.py register -r #{options[:server] || 'pypi'}" 67 | context.shell "python setup.py #{options[:distributions] || 'sdist'} upload -r #{options[:server] || 'pypi'}" 68 | context.shell "python setup.py upload_docs --upload-dir #{options[:docs_dir] || 'build/docs'}" 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/provider/puppet_forge_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/puppet_forge' 3 | require 'puppet/face' 4 | require 'puppet_blacksmith' 5 | 6 | describe DPL::Provider::PuppetForge do 7 | subject :provider do 8 | described_class.new(DummyContext.new, :user => 'puppetlabs', :password => 's3cr3t') 9 | end 10 | 11 | describe "forge" do 12 | it 'should include the user and password specified' do 13 | expect(provider.forge.username).to eq(provider.options[:user]) 14 | expect(provider.forge.password).to eq(provider.options[:password]) 15 | end 16 | end 17 | 18 | describe "build" do 19 | it 'should use Puppet module tool to build the module' do 20 | pmt = double('pmt') 21 | expect(::Puppet::Face).to receive(:[]).and_return(pmt) 22 | expect(pmt).to receive(:build).with('./') 23 | provider.build 24 | end 25 | end 26 | 27 | describe "#check_auth" do 28 | it 'should require a user' do 29 | provider.options.update(:user => nil) 30 | expect{ provider.check_auth }.to raise_error("must supply a user") 31 | end 32 | 33 | it 'should require a password' do 34 | provider.options.update(:password => nil) 35 | expect{ provider.check_auth }.to raise_error("must supply a password") 36 | end 37 | end 38 | 39 | describe "#check_app" do 40 | it 'should load module metadata using Blacksmith' do 41 | modulefile = double('modulefile') 42 | expect(::Blacksmith::Modulefile).to receive(:new).and_return(modulefile) 43 | expect(modulefile).to receive(:metadata) { true } 44 | provider.check_app 45 | end 46 | end 47 | 48 | describe "#push_app" do 49 | it 'should use Blacksmith to push to the Forge' do 50 | forge = double('forge') 51 | expect(provider).to receive(:build).and_return(true) 52 | expect(provider).to receive(:modulefile).at_least(:once).and_return(double('modulefile', :name => 'test')) 53 | expect(provider).to receive(:log).and_return(true) 54 | expect(::Blacksmith::Forge).to receive(:new).and_return(forge) 55 | expect(forge).to receive(:push!) { true } 56 | expect(forge).to receive(:username) { provider.options[:user] } 57 | provider.push_app 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/dpl/provider/gae.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha1' 2 | require 'open-uri' 3 | 4 | module DPL 5 | class Provider 6 | class GAE < Provider 7 | experimental 'Google App Engine' 8 | 9 | # https://developers.google.com/appengine/downloads 10 | GAE_VERSION='1.9.13' 11 | GAE_ZIP_FILE="google_appengine_#{GAE_VERSION}.zip" 12 | SHA1SUM='05166691108caddc4d4cfdf683cfc4748df197a2' 13 | BASE_DIR=Dir.pwd 14 | GAE_DIR=File.join(BASE_DIR, 'google_appengine') 15 | APPCFG_BIN=File.join(GAE_DIR, 'appcfg.py') 16 | 17 | def self.install_sdk 18 | requires 'rubyzip', :load => 'zip' 19 | $stderr.puts "Setting up Google App Engine SDK" 20 | 21 | Dir.chdir(BASE_DIR) do 22 | unless File.exists? GAE_ZIP_FILE 23 | $stderr.puts "Downloading Google App Engine SDK" 24 | File.open(GAE_ZIP_FILE, "wb") do |dest| 25 | open("https://storage.googleapis.com/appengine-sdks/featured/#{GAE_ZIP_FILE}", "rb") do |src| 26 | dest.write(src.read) 27 | end 28 | end 29 | end 30 | sha1sum = Digest::SHA1.hexdigest(File.read(GAE_ZIP_FILE)) 31 | unless sha1sum == SHA1SUM 32 | raise "Checksum did not match for #{GAE_ZIP_FILE}" 33 | end 34 | 35 | unless File.directory? 'google_appengine' 36 | $stderr.puts "Extracting Google App Engine SDK archive" 37 | Zip::File.open(GAE_ZIP_FILE) do |file| 38 | file.each do |entry| 39 | entry.extract entry.name 40 | end 41 | end 42 | end 43 | end 44 | end 45 | 46 | install_sdk 47 | 48 | def needs_key? 49 | false 50 | end 51 | 52 | def check_auth 53 | end 54 | 55 | def app_dir 56 | options[:app_dir] || context.env['TRAVIS_BUILD_DIR'] || Dir.pwd 57 | end 58 | 59 | def push_app 60 | puts "About to call push_app in with #{self.class} provider" 61 | puts "APPCFG_BIN: #{APPCFG_BIN}" 62 | unless File.exist?(APPCFG_BIN) 63 | puts "APPCFG_BIN does not exist" 64 | end 65 | context.shell "#{APPCFG_BIN} --oauth2_refresh_token=#{options[:oauth_refresh_token]} update #{app_dir}" 66 | end 67 | end 68 | end 69 | end -------------------------------------------------------------------------------- /spec/provider/elastic_beanstalk_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'aws-sdk-v1' 3 | require 'dpl/provider' 4 | require 'dpl/provider/elastic_beanstalk' 5 | 6 | describe DPL::Provider::ElasticBeanstalk do 7 | 8 | before (:each) do 9 | AWS.stub! 10 | end 11 | 12 | let(:access_key_id) { 'qwertyuiopasdfghjklz' } 13 | let(:secret_access_key) { 'qwertyuiopasdfghjklzqwertyuiopasdfghjklz' } 14 | let(:region) { 'us-west-2' } 15 | let(:app) { 'example-app' } 16 | let(:env) { 'live' } 17 | let(:bucket_name) { "travis-elasticbeanstalk-test-builds-#{region}" } 18 | 19 | subject :provider do 20 | described_class.new( 21 | DummyContext.new, :access_key_id => access_key_id, :secret_access_key => secret_access_key, 22 | :region => region, :app => app, :env => env, :bucket_name => bucket_name 23 | ) 24 | end 25 | 26 | describe "#check_auth" do 27 | example do 28 | expect(AWS).to receive(:config).with(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region) 29 | provider.check_auth 30 | end 31 | end 32 | 33 | describe "#push_app" do 34 | 35 | let(:bucket_name) { "travis-elasticbeanstalk-test-builds-#{region}" } 36 | let(:s3_object) { Object.new } 37 | let(:app_version) { Object.new } 38 | 39 | let(:bucket) { Struct.new(:name) } 40 | let(:s3) { Struct.new(:buckets) } 41 | 42 | example 'bucket exists already' do 43 | s3_mock = s3.new([bucket.new(bucket_name)]) 44 | expect(provider).to receive(:s3).and_return(s3_mock) 45 | expect(provider).not_to receive(:create_bucket) 46 | expect(provider).to receive(:create_zip).and_return('/path/to/file.zip') 47 | expect(provider).to receive(:archive_name).and_return('file.zip') 48 | expect(provider).to receive(:upload).with('file.zip', '/path/to/file.zip').and_return(s3_object) 49 | expect(provider).to receive(:sleep).with(5) 50 | expect(provider).to receive(:create_app_version).with(s3_object).and_return(app_version) 51 | expect(provider).to receive(:update_app).with(app_version) 52 | 53 | provider.push_app 54 | end 55 | 56 | example 'bucket doesnt exist yet' do 57 | s3_mock = s3.new([]) 58 | expect(provider).to receive(:s3).and_return(s3_mock) 59 | expect(provider).to receive(:create_bucket) 60 | expect(provider).to receive(:create_zip).and_return('/path/to/file.zip') 61 | expect(provider).to receive(:archive_name).and_return('file.zip') 62 | expect(provider).to receive(:upload).with('file.zip', '/path/to/file.zip').and_return(s3_object) 63 | expect(provider).to receive(:sleep).with(5) 64 | expect(provider).to receive(:create_app_version).with(s3_object).and_return(app_version) 65 | expect(provider).to receive(:update_app).with(app_version) 66 | 67 | provider.push_app 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/provider/cloud_files_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/cloud_files' 3 | require 'fog' 4 | 5 | describe DPL::Provider::CloudFiles do 6 | before :each do 7 | Fog.mock! 8 | end 9 | 10 | subject :provider do 11 | described_class.new(DummyContext.new, :username => 'username', :api_key => 'api key', :container => 'travis', :region => 'dfw') 12 | end 13 | 14 | describe "#needs_key?" do 15 | example do 16 | expect(provider.needs_key?).to eq(false) 17 | end 18 | end 19 | 20 | describe "#api" do 21 | example do 22 | expect(Fog::Storage).to receive(:new).with(:provider => 'Rackspace', :rackspace_username => 'username', :rackspace_api_key => 'api key', :rackspace_region => 'dfw') 23 | 24 | provider.api 25 | end 26 | end 27 | 28 | describe "#check_auth" do 29 | example do 30 | expect(provider).to receive(:log).with('Authenticated as username') 31 | 32 | provider.check_auth 33 | end 34 | end 35 | 36 | describe "#push_app" do 37 | let :files do 38 | files = double 39 | 40 | directory = double 41 | allow(directory).to receive(:files) { files } 42 | 43 | directories = double 44 | expect(directories).to receive(:get).with('travis').and_return(directory) 45 | 46 | service = double(:service) 47 | allow(service).to receive(:directories) { directories } 48 | allow(provider).to receive(:api) { service } 49 | 50 | files 51 | end 52 | 53 | example do 54 | expect(files).to receive(:create).with(:key => 'a', :body => 'a body') 55 | expect(files).to receive(:create).with(:key => 'b', :body => 'b body') 56 | expect(files).to receive(:create).with(:key => 'c', :body => 'c body') 57 | 58 | expect(Dir).to receive(:glob).with('**/*').and_return(['a', 'b', 'c']) 59 | allow(File).to receive(:open) { |name| "#{name} body" } 60 | 61 | provider.push_app 62 | end 63 | 64 | example "with dot_match option" do 65 | provider.options.update(:dot_match => true) 66 | expect(files).to receive(:create).with(:key => '.a', :body => '.a body') 67 | expect(files).to receive(:create).with(:key => 'a', :body => 'a body') 68 | 69 | expect(Dir).to receive(:glob).with('**/*', File::FNM_DOTMATCH).and_return(['.a', 'a']) 70 | allow(File).to receive(:open) { |name| "#{name} body" } 71 | 72 | provider.push_app 73 | end 74 | end 75 | 76 | describe "#deploy" do 77 | example 'Not Found' do 78 | directories = double 79 | allow(directories).to receive(:get) { nil } 80 | 81 | service = double(:service) 82 | allow(service).to receive(:directories) { directories } 83 | allow(provider).to receive(:api) { service } 84 | 85 | expect { provider.deploy }.to raise_error(DPL::Error, 'The specified container does not exist.') 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/dpl/provider/heroku/anvil.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | module Heroku 4 | class Anvil < Git 5 | HEROKU_BUILDPACKS = ['ruby', 'nodejs', 'clojure', 'python', 'java', 'gradle', 'grails', 'scala', 'play'] 6 | HEROKU_BUILDPACK_PREFIX = "https://github.com/heroku/heroku-buildpack-" 7 | requires 'anvil-cli', :load => 'anvil/engine' 8 | requires 'excon' # comes with heroku 9 | requires 'json' 10 | 11 | def api 12 | raise Error, 'anvil deploy strategy only works with api_key' unless options[:api_key] 13 | super 14 | end 15 | 16 | def deploy 17 | warn '' 18 | if options[:strategy] 19 | warn 'You have explicitly set your deploy strategy to %p.' % options[:strategy] 20 | warn 'Anvil support will be dropped in the near future.' 21 | warn 'Please consider changing it to "api" or "git".' 22 | else 23 | warn 'The default strategy for Heroku deployments is currently "anvil".' 24 | warn 'This will be changed to "api" in the near future.' 25 | warn 'Consider setting it explicitly to "api" or "git".' 26 | end 27 | warn '' 28 | 29 | super 30 | end 31 | 32 | def push_app 33 | sha = context.env['TRAVIS_COMMIT'] || `git rev-parse HEAD`.strip 34 | response = Excon.post release_url, 35 | :body => { "slug_url" => slug_url, "description" => "Deploy #{sha} via Travis CI" }.to_json, 36 | :headers => { 'Content-Type' => 'application/json', 'Accept' => 'application/json' } 37 | 38 | print "\nDeploying slug " 39 | while response.status == 202 40 | location = response.headers['Location'] 41 | response = Excon.get("https://:#{option(:api_key)}@cisaurus.heroku.com#{location}") 42 | sleep(1) 43 | print '.' 44 | end 45 | 46 | if response.status.between? 200, 299 47 | puts " success!" 48 | else 49 | raise Error, "deploy failed, anvil response: #{response.body}" 50 | end 51 | end 52 | 53 | def slug_url 54 | @slug_url ||= begin 55 | ::Anvil.headers["X-Heroku-User"] = user 56 | ::Anvil.headers["X-Heroku-App"] = option(:app) 57 | if HEROKU_BUILDPACKS.include? options[:buildpack] 58 | options[:buildpack] = HEROKU_BUILDPACK_PREFIX + options[:buildpack] + ".git" 59 | end 60 | ::Anvil::Engine.build ".", :buildpack => options[:buildpack] 61 | rescue ::Anvil::Builder::BuildError => e 62 | raise Error, "deploy failed, anvil build error: #{e.message}" 63 | end 64 | end 65 | 66 | def release_url 67 | "https://:#{option(:api_key)}@cisaurus.heroku.com/v1/apps/#{option(:app)}/release" 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/provider/openshift_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rhc' 3 | require 'dpl/provider/openshift' 4 | 5 | describe DPL::Provider::Openshift do 6 | subject :provider do 7 | described_class.new(DummyContext.new, :user => 'foo', :password => 'foo', :domain => 'foo', :app => 'example', :key_name => 'key') 8 | end 9 | 10 | describe "#api" do 11 | it 'accepts a user and a password' do 12 | api = double(:api) 13 | provider.options.update(:user => "foo", :password => "bar") 14 | expect(::RHC::Rest::Client).to receive(:new).with(:user => "foo", :password => "bar", :server => "openshift.redhat.com").and_return(api) 15 | expect(provider.api).to eq(api) 16 | end 17 | end 18 | 19 | context "with api" do 20 | let :api do 21 | double "api", 22 | :user => double(:login => "foo@bar.com"), 23 | :find_application => double(:name => "example", :git_url => "git://something"), 24 | :add_key => double 25 | end 26 | let :app do 27 | double "app", 28 | :restart => double 29 | end 30 | 31 | before do 32 | expect(::RHC::Rest::Client).to receive(:new).at_most(:once).and_return(api) 33 | provider.api 34 | end 35 | 36 | its(:api) {should be == api} 37 | 38 | describe "#check_auth" do 39 | example do 40 | expect(provider).to receive(:log).with("authenticated as foo@bar.com") 41 | provider.check_auth 42 | end 43 | end 44 | 45 | describe "#check_app" do 46 | example do 47 | expect(provider).to receive(:log).with("found app example") 48 | provider.check_app 49 | end 50 | end 51 | 52 | describe "#setup_key" do 53 | example do 54 | expect(File).to receive(:read).with("the file").and_return("ssh-rsa\nfoo") 55 | expect(api).to receive(:add_key).with("key", "foo", "ssh-rsa") 56 | provider.setup_key("the file") 57 | end 58 | end 59 | 60 | describe "#remove_key" do 61 | example do 62 | expect(api).to receive(:delete_key).with("key") 63 | provider.remove_key 64 | end 65 | end 66 | 67 | describe "#push_app" do 68 | example "when app.deployment_branch is not set" do 69 | expect(provider.context).to receive(:shell).with("git push git://something -f") 70 | provider.push_app 71 | end 72 | end 73 | 74 | context "when app.deployment_branch is set" do 75 | subject :provider do 76 | described_class.new(DummyContext.new, :user => 'foo', :password => 'foo', :domain => 'foo', :app => 'example', :key_name => 'key', :deployment_branch => 'test-branch') 77 | 78 | expect(provider.app).to receive(:deployment_branch=).with("test-branch") 79 | expect(provider.context).to receive(:shell).with("git push git://something -f test-branch") 80 | provider.push_app 81 | end 82 | end 83 | 84 | describe "#restart" do 85 | example do 86 | expect(provider.app).to receive(:restart) 87 | provider.restart 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/dpl/provider/engine_yard.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | module DPL 4 | class Provider 5 | class EngineYard < Provider 6 | requires 'engineyard-cloud-client' 7 | 8 | def token 9 | options[:api_key] ||= if options[:email] and options[:password] 10 | EY::CloudClient.authenticate(options[:email], options[:password]) 11 | else 12 | option(:api_key) # will raise 13 | end 14 | end 15 | 16 | def api 17 | @api ||= EY::CloudClient.new(:token => token) 18 | end 19 | 20 | def check_auth 21 | log "authenticated as %s" % api.current_user.email 22 | end 23 | 24 | def check_app 25 | remotes = `git remote -v`.scan(/\t[^\s]+\s/).map { |c| c.strip }.uniq 26 | @current_sha = `git rev-parse HEAD`.chomp 27 | resolver = api.resolve_app_environments( 28 | :app_name => options[:app], 29 | :account_name => options[:account], 30 | :environment_name => options[:environment], 31 | :remotes => remotes) 32 | resolver.one_match { @app_env = resolver.matches.first } 33 | resolver.no_matches { error resolver.errors.join("\n").inspect } 34 | resolver.many_matches do |matches| 35 | message = "Multiple matches possible, please be more specific:\n\n" 36 | matches.each do |appenv| 37 | message << "environment: '#{appenv.environment.name}' account: '#{appenv.environment.account.name}'\n" 38 | end 39 | error message 40 | end 41 | @app_env 42 | end 43 | 44 | def needs_key? 45 | false 46 | end 47 | 48 | def cleanup 49 | end 50 | 51 | def uncleanup 52 | end 53 | 54 | def push_app 55 | deploy_opts = {:ref => @current_sha} 56 | if command = options[:migrate] 57 | if command === true || command === "true" 58 | error("\"true\" doesn't look like a migration command, try --migrate=\"rake db:migrate\"") 59 | end 60 | deploy_opts[:migrate] = true 61 | deploy_opts[:migration_command] = command 62 | end 63 | print "deploying " 64 | deployment = EY::CloudClient::Deployment.deploy(api, @app_env, deploy_opts) 65 | result = poll_for_result(deployment) 66 | unless result.successful 67 | error "Deployment failed (see logs on Engine Yard)" 68 | end 69 | end 70 | 71 | def poll_for_result(deployment) 72 | until deployment.finished? 73 | sleep 5 74 | #TODO: configurable timeout? 75 | print "." 76 | deployment = EY::CloudClient::Deployment.get(api, deployment.app_environment, deployment.id) 77 | end 78 | puts "DONE: https://cloud.engineyard.com/apps/#{deployment.app.id}/environments/#{deployment.environment.id}/deployments/#{deployment.id}/pretty" 79 | deployment 80 | end 81 | 82 | def deploy 83 | super 84 | rescue EY::CloudClient::Error => e 85 | error(e.message) 86 | end 87 | 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/dpl/provider/deis.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class Deis < Provider 4 | experimental 'Deis' 5 | 6 | def install_deploy_dependencies 7 | self.class.pip 'deis', 'deis', options[:cli_version] 8 | end 9 | 10 | def needs_key? 11 | true 12 | end 13 | 14 | def check_auth 15 | unless context.shell "deis login #{controller_url}" \ 16 | " --username=#{option(:username)}" \ 17 | " --password=#{option(:password)}" 18 | error 'Login failed.' 19 | end 20 | end 21 | 22 | def check_app 23 | unless context.shell "deis apps:info --app=#{option(:app)}" 24 | error 'Application could not be verified.' 25 | end 26 | end 27 | 28 | def setup_key(file) 29 | unless context.shell "deis keys:add #{file}" 30 | error 'Adding keys failed.' 31 | end 32 | end 33 | 34 | def setup_git_ssh(path, key_path) 35 | super(path, key_path) 36 | # Deis uses a non-standard port, so we need to create a 37 | # ssh config shortcut 38 | key_path = File.expand_path(key_path) 39 | add_ssh_config_entry(key_path) 40 | # A git remote is required for running commands 41 | # https://github.com/deis/deis/issues/1086 42 | add_git_remote 43 | end 44 | 45 | def remove_key 46 | unless context.shell "deis keys:remove #{option(:key_name)}" 47 | error 'Removing keys failed.' 48 | end 49 | end 50 | 51 | def push_app 52 | wait_until_key_is_set 53 | unless context.shell "git push #{git_push_url} HEAD:refs/heads/master -f" 54 | error 'Deploying application failed.' 55 | end 56 | end 57 | 58 | def run(command) 59 | unless context.shell "deis apps:run #{command}" 60 | error 'Running command failed.' 61 | end 62 | end 63 | 64 | private 65 | 66 | def wait_until_key_is_set 67 | sleep 5 68 | end 69 | 70 | def ssh_config_entry(key_file) 71 | "\nHost deis-repo\n" \ 72 | " Hostname #{option(:controller)}\n" \ 73 | " Port 2222\n" \ 74 | " User git\n" \ 75 | " IdentityFile #{key_file}\n" 76 | end 77 | 78 | def add_ssh_config_entry(key_file) 79 | FileUtils.mkdir_p(ssh_config_dir) 80 | File.open(ssh_config, 'a') { |f| f.write(ssh_config_entry(key_file)) } 81 | end 82 | 83 | def ssh_config 84 | File.join(ssh_config_dir, 'config') 85 | end 86 | 87 | def ssh_config_dir 88 | File.join(Dir.home, '.ssh') 89 | end 90 | 91 | def add_git_remote 92 | context.shell "git remote add deis #{git_remote_url}" 93 | end 94 | 95 | def git_push_url 96 | "deis-repo:#{option(:app)}.git" 97 | end 98 | 99 | def git_remote_url 100 | "ssh://git@#{option(:controller)}:2222/#{option(:app)}.git" 101 | end 102 | 103 | def controller_url 104 | "http://#{option(:controller)}" 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/dpl/provider/code_deploy.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module DPL 4 | class Provider 5 | class CodeDeploy < Provider 6 | requires 'aws-sdk', pre: true 7 | 8 | def code_deploy 9 | @code_deploy ||= begin 10 | Aws.add_service('CodeDeploy', api: File.expand_path("../CodeDeploy.api.json", __FILE__)) unless defined? Aws::CodeDeploy 11 | Aws::CodeDeploy::Client.new(code_deploy_options) 12 | end 13 | end 14 | 15 | def code_deploy_options 16 | code_deploy_options = { 17 | region: options[:region] || 'us-east-1', 18 | credentials: Aws::Credentials.new(option(:access_key_id), option(:secret_access_key)) 19 | } 20 | code_deploy_options[:endpoint] = options[:endpoint] if options[:endpoint] 21 | code_deploy_options 22 | end 23 | 24 | def needs_key? 25 | false 26 | end 27 | 28 | def revision 29 | case options[:revision_type].to_s.downcase 30 | when "s3" then s3_revision 31 | when "github" then github_revision 32 | when "" then options[:bucket] ? s3_revision : github_revision 33 | else error("unknown revision type %p" % options[:revision_type]) 34 | end 35 | end 36 | 37 | def s3_revision 38 | { 39 | revision_type: 'S3', 40 | s3_location: { 41 | bucket: option(:bucket), 42 | bundle_type: bundle_type, 43 | key: s3_key 44 | } 45 | } 46 | end 47 | 48 | def github_revision 49 | { 50 | revision_type: 'GitHub', 51 | git_hub_location: { 52 | commit_id: options[:commit_id] || context.env['TRAVIS_COMMIT'] || `git rev-parse HEAD`.strip, 53 | repository: options[:repository] || context.env['TRAVIS_REPO_SLUG'] || option(:repository) 54 | } 55 | } 56 | end 57 | 58 | def push_app 59 | deployment = code_deploy.create_deployment({ 60 | revision: revision, 61 | application_name: options[:application] || option(:application_name), 62 | deployment_group_name: options[:deployment_group] || option(:deployment_group_name), 63 | description: options[:description] || default_description 64 | }) 65 | log "Triggered deployment #{deployment.deployment_id.inspect}." 66 | rescue Aws::CodeDeploy::Errors::DeploymentLimitExceededException => exception 67 | error(exception.message) 68 | end 69 | 70 | def bundle_type 71 | if s3_key =~ /\.(tar|tgz|zip)$/ 72 | options[:bundle_type] || $1 73 | else 74 | option(:bundle_type) 75 | end 76 | end 77 | 78 | def s3_key 79 | options[:key] || option(:s3_key) 80 | end 81 | 82 | def default_description 83 | "Deploy build #{context.env['TRAVIS_BUILD_NUMBER']} via Travis CI" 84 | end 85 | 86 | def check_auth 87 | log "Logging in with Access Key: #{option(:access_key_id)[-4..-1].rjust(20, '*')}" 88 | end 89 | 90 | def cleanup 91 | end 92 | 93 | def uncleanup 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/provider/heroku_anvil_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'anvil' 3 | require 'heroku-api' 4 | require 'excon' 5 | require 'dpl/provider/heroku' 6 | 7 | describe DPL::Provider::Heroku do 8 | subject :provider do 9 | described_class.new(DummyContext.new, :app => 'example', :api_key => 'foo', :strategy => 'anvil', :buildpack => 'git://some-buildpack.git') 10 | end 11 | 12 | describe "#api" do 13 | it 'accepts an api key' do 14 | api = double(:api) 15 | expect { provider.api }.not_to raise_error 16 | end 17 | 18 | it 'raises an error if an api key is not present' do 19 | provider.options.delete :api_key 20 | expect { provider.api }.to raise_error(DPL::Error) 21 | end 22 | end 23 | 24 | context "with fake api" do 25 | let :api do 26 | double "api", 27 | :get_user => double("get_user", :body => { "email" => "foo@bar.com" }), 28 | :get_app => double("get_app", :body => { "name" => "example", "git_url" => "GIT URL" }) 29 | end 30 | 31 | before do 32 | expect(::Heroku::API).to receive(:new).and_return(api) 33 | provider.api 34 | end 35 | 36 | describe "#push_app" do 37 | example do 38 | response = double :response 39 | allow(response).to receive_messages(:status => 202) 40 | allow(response).to receive_messages(:headers => {'Location' => '/blah'}) 41 | 42 | second_response = double :second_response 43 | allow(second_response).to receive_messages(:status => 200) 44 | 45 | allow(provider).to receive_messages(:slug_url => "http://slug-url") 46 | 47 | expect(provider.context.env).to receive(:[]).with('TRAVIS_COMMIT').and_return('123') 48 | expect(::Excon).to receive(:post).with(provider.release_url, 49 | :body => {"slug_url" => "http://slug-url", "description" => "Deploy 123 via Travis CI" }.to_json, 50 | :headers => {"Content-Type" => 'application/json', 'Accept' => 'application/json'}).and_return(response) 51 | 52 | expect(::Excon).to receive(:get).with("https://:#{provider.options[:api_key]}@cisaurus.heroku.com/blah").and_return(second_response) 53 | provider.push_app 54 | end 55 | end 56 | 57 | describe "#slug_url" do 58 | 59 | before(:each) do 60 | headers = double(:headers) 61 | expect(::Anvil).to receive(:headers).at_least(:twice).and_return(headers) 62 | expect(headers).to receive(:[]=).at_least(:once).with('X-Heroku-User', "foo@bar.com") 63 | expect(headers).to receive(:[]=).at_least(:once).with('X-Heroku-App', "example") 64 | end 65 | 66 | example "with full buildpack url" do 67 | expect(::Anvil::Engine).to receive(:build).with(".", :buildpack=>"git://some-buildpack.git") 68 | provider.slug_url 69 | end 70 | 71 | example "with buildpack name expansion" do 72 | DPL::Provider::Heroku::Anvil::HEROKU_BUILDPACKS.each do |b| 73 | provider.options.update(:buildpack => b) 74 | expect(::Anvil::Engine).to receive(:build).with(".", :buildpack=>described_class::Anvil::HEROKU_BUILDPACK_PREFIX + b + ".git") 75 | provider.slug_url 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/provider/pypi_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/pypi' 3 | 4 | describe DPL::Provider::PyPI do 5 | subject :provider do 6 | described_class.new(DummyContext.new, :user => 'foo', :password => 'bar') 7 | end 8 | 9 | describe "#config" do 10 | it 'accepts a user and a password' do 11 | expect(provider.config[:servers]['pypi']).to include 'username: foo' 12 | expect(provider.config[:servers]['pypi']).to include 'password: bar' 13 | end 14 | end 15 | 16 | describe "#initialize" do 17 | example "with :distributions option containing 'bdist_wheel'" do 18 | expect(described_class).to receive(:pip).with("wheel") 19 | described_class.new(DummyContext.new, :user => 'foo', :password => 'bar', :distributions => 'bdist_wheel sdist') 20 | end 21 | end 22 | 23 | describe "#check_auth" do 24 | example do 25 | expect(provider).to receive(:log).with("Authenticated as foo") 26 | provider.check_auth 27 | end 28 | end 29 | 30 | describe "#push_app" do 31 | example do 32 | expect(provider.context).to receive(:shell).with("python setup.py register -r pypi") 33 | expect(provider.context).to receive(:shell).with("python setup.py sdist upload -r pypi") 34 | expect(provider.context).to receive(:shell).with("python setup.py upload_docs --upload-dir build/docs") 35 | provider.push_app 36 | end 37 | 38 | example "with :distributions option" do 39 | provider.options.update(:distributions => 'sdist bdist') 40 | expect(provider.context).to receive(:shell).with("python setup.py register -r pypi") 41 | expect(provider.context).to receive(:shell).with("python setup.py sdist bdist upload -r pypi") 42 | expect(provider.context).to receive(:shell).with("python setup.py upload_docs --upload-dir build/docs") 43 | provider.push_app 44 | end 45 | 46 | example "with :server option" do 47 | provider.options.update(:server => 'http://blah.com') 48 | expect(provider.context).to receive(:shell).with("python setup.py register -r http://blah.com") 49 | expect(provider.context).to receive(:shell).with("python setup.py sdist upload -r http://blah.com") 50 | expect(provider.context).to receive(:shell).with("python setup.py upload_docs --upload-dir build/docs") 51 | provider.push_app 52 | end 53 | 54 | example "with :docs_dir option" do 55 | provider.options.update(:docs_dir => 'some/dir') 56 | expect(provider.context).to receive(:shell).with("python setup.py register -r pypi") 57 | expect(provider.context).to receive(:shell).with("python setup.py sdist upload -r pypi") 58 | expect(provider.context).to receive(:shell).with("python setup.py upload_docs --upload-dir some/dir") 59 | provider.push_app 60 | end 61 | end 62 | 63 | describe "#write_servers" do 64 | example do 65 | f = double(:f) 66 | expect(f).to receive(:puts).with(" pypi") 67 | expect(f).to receive(:puts).with("[pypi]") 68 | expect(f).to receive(:puts).with(["repository: http://pypi.python.org/pypi", 69 | "username: foo", 70 | "password: bar" 71 | ]) 72 | provider.write_servers(f) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/dpl/provider/chef_supermarket.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class ChefSupermarket < Provider 4 | 5 | # Most of the code is inspired by: 6 | # https://github.com/opscode/chef/blob/11.16.4/lib/chef/knife/cookbook_site_share.rb 7 | 8 | # Compatibility with ruby 1.9 9 | requires 'chef', version: '< 12.0' 10 | requires 'chef', load: 'chef/config' 11 | requires 'chef', load: 'chef/cookbook_loader' 12 | requires 'chef', load: 'chef/cookbook_uploader' 13 | requires 'chef', load: 'chef/cookbook_site_streaming_uploader' 14 | 15 | attr_reader :cookbook_name, :cookbook_category 16 | attr_reader :cookbook 17 | 18 | def needs_key? 19 | false 20 | end 21 | 22 | def check_auth 23 | error "Missing user_id option" unless options[:user_id] 24 | error "Missing client_key option" unless options[:client_key] 25 | ::Chef::Config[:client_key] = options[:client_key] 26 | error "#{options[:client_key]} does not exist" unless ::File.exist?(options[:client_key]) 27 | end 28 | 29 | def check_app 30 | @cookbook_name = options[:cookbook_name] || options[:app] 31 | @cookbook_category = options[:cookbook_category] 32 | unless cookbook_category 33 | error "Missing cookbook_category option\n" + 34 | "see https://docs.getchef.com/knife_cookbook_site.html#id12" 35 | end 36 | 37 | log "Validating cookbook #{cookbook_name}" 38 | # Check that cookbook exist and is valid 39 | # So we assume cookbook path is '..' 40 | cl = ::Chef::CookbookLoader.new '..' 41 | @cookbook = cl[cookbook_name] 42 | ::Chef::CookbookUploader.new(cookbook, '..').validate_cookbooks 43 | end 44 | 45 | def push_app 46 | log "Creating cookbook build directory" 47 | tmp_cookbook_dir = Chef::CookbookSiteStreamingUploader.create_build_dir(cookbook) 48 | log "Making tarball in #{tmp_cookbook_dir}" 49 | system("tar -czf #{cookbook_name}.tgz #{cookbook_name}", :chdir => tmp_cookbook_dir) 50 | 51 | uri = "http://cookbooks.opscode.com/api/v1/cookbooks" 52 | 53 | log "Uploading to #{uri}" 54 | category_string = { 'category'=>cookbook_category }.to_json 55 | http_resp = ::Chef::CookbookSiteStreamingUploader.post( 56 | uri, 57 | options[:user_id], 58 | options[:client_key], 59 | { 60 | :tarball => File.open("#{tmp_cookbook_dir}/#{cookbook_name}.tgz"), 61 | :cookbook => category_string 62 | } 63 | ) 64 | res = ::Chef::JSONCompat.from_json(http_resp.body) 65 | if http_resp.code.to_i != 201 66 | if res['error_messages'] 67 | if res['error_messages'][0] =~ /Version already exists/ 68 | error "The same version of this cookbook already exists on the Opscode Cookbook Site." 69 | else 70 | error "#{res['error_messages'][0]}" 71 | end 72 | else 73 | error "Unknown error while sharing cookbook\n" + 74 | "Server response: #{http_resp.body}" 75 | end 76 | end 77 | 78 | log "Upload complete." 79 | ::FileUtils.rm_rf tmp_cookbook_dir 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/dpl/provider/cloudcontrol.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'net/http' 3 | require 'net/https' 4 | 5 | module DPL 6 | class Provider 7 | class CloudControl < Provider 8 | attr_accessor :app_name 9 | attr_accessor :dep_name 10 | 11 | def initialize(context, options) 12 | super 13 | option(:email) && option(:password) && option(:deployment) 14 | @app_name, @dep_name = options[:deployment].split('/') 15 | 16 | @http = Net::HTTP.new('api.cloudcontrol.com', 443) 17 | @http.use_ssl = true 18 | end 19 | 20 | def check_auth 21 | headers_with_token 22 | end 23 | 24 | def check_app 25 | response = api_call('GET', "/app/#{ app_name }/deployment/#{ dep_name }") 26 | error('application check failed') if response.code != '200' 27 | @repository = JSON.parse(response.body)["branch"] 28 | end 29 | 30 | def setup_key(file) 31 | data = { 'key' => File.read(file).chomp } 32 | response = api_call('POST', "/user/#{ user['username'] }/key", JSON.dump(data)) 33 | error('adding key failed') if response.code != '200' 34 | key = JSON.parse response.body 35 | @ssh_key_id = key['key_id'] 36 | end 37 | 38 | def remove_key 39 | response = api_call('DELETE', "/user/#{ user['username']}/key/#{ @ssh_key_id }") 40 | error('key removal failed') if response.code != '204' 41 | end 42 | 43 | def push_app 44 | branch = (dep_name == 'default') ? 'master' : dep_name 45 | context.shell "git push #{ @repository } HEAD:#{ branch } -f" 46 | deploy_app 47 | end 48 | 49 | private 50 | 51 | def get_token 52 | request = Net::HTTP::Post.new '/token/' 53 | request.basic_auth options[:email], options[:password] 54 | response = @http.request(request) 55 | error('authorization failed') if response.code != '200' 56 | return JSON.parse response.body 57 | end 58 | 59 | def headers_with_token(options = {}) 60 | @token = get_token if options[:new_token] || @token.nil? 61 | return { 62 | 'Authorization' => %Q|cc_auth_token="#{ @token['token'] }"|, 63 | 'Content-Type' => 'application/json' 64 | } 65 | end 66 | 67 | def get_headers 68 | headers = headers_with_token 69 | response = api_call('GET', '/user/', nil, headers) 70 | return headers if response.code == '200' 71 | 72 | return headers_with_token :new_token => true 73 | end 74 | 75 | def api_call(method, path, data = nil, headers = nil) 76 | return @http.send_request(method, path, data, headers || get_headers) 77 | end 78 | 79 | def deploy_app 80 | data = {'version' => -1} 81 | response = api_call('PUT', "/app/#{ app_name }/deployment/#{ dep_name }", JSON.dump(data)) 82 | error('deployment failed - the deployment is already deploying') if response.code != '200' 83 | end 84 | 85 | def user 86 | if @user.nil? 87 | response = api_call('GET', '/user/') 88 | error('can not find the user') if response.code != '200' 89 | users = JSON.parse response.body 90 | @user = users[0] 91 | end 92 | return @user 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/dpl/provider/elastic_beanstalk.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class ElasticBeanstalk < Provider 4 | experimental 'AWS Elastic Beanstalk' 5 | 6 | requires 'aws-sdk-v1' 7 | requires 'rubyzip', :load => 'zip' 8 | 9 | DEFAULT_REGION = 'us-east-1' 10 | 11 | def needs_key? 12 | false 13 | end 14 | 15 | def check_auth 16 | AWS.config(access_key_id: option(:access_key_id), secret_access_key: option(:secret_access_key), region: region) 17 | end 18 | 19 | def check_app 20 | end 21 | 22 | def push_app 23 | create_bucket unless bucket_exists? 24 | zip_file = create_zip 25 | s3_object = upload(archive_name, zip_file) 26 | sleep 5 #s3 eventual consistency 27 | version = create_app_version(s3_object) 28 | update_app(version) 29 | end 30 | 31 | private 32 | 33 | def app_name 34 | option(:app) 35 | end 36 | 37 | def env_name 38 | option(:env) 39 | end 40 | 41 | def version_label 42 | "travis-#{sha}-#{Time.now.to_i}" 43 | end 44 | 45 | def archive_name 46 | "#{version_label}.zip" 47 | end 48 | 49 | def region 50 | option(:region) || DEFAULT_REGION 51 | end 52 | 53 | def bucket_name 54 | option(:bucket_name) 55 | end 56 | 57 | def s3 58 | @s3 ||= AWS::S3.new 59 | end 60 | 61 | def eb 62 | @eb ||= AWS::ElasticBeanstalk.new.client 63 | end 64 | 65 | def bucket_exists? 66 | s3.buckets.map(&:name).include? bucket_name 67 | end 68 | 69 | def create_bucket 70 | s3.buckets.create(bucket_name) 71 | end 72 | 73 | def files_to_pack 74 | `git ls-files -z`.split("\x0") 75 | end 76 | 77 | def create_zip 78 | directory = Dir.pwd 79 | zipfile_name = File.join(directory, archive_name) 80 | 81 | Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile| 82 | files_to_pack.each do |file| 83 | relative_archive_path = File.join(directory, '/') 84 | zipfile.add(file.sub(relative_archive_path, ''), file) 85 | end 86 | end 87 | zipfile_name 88 | end 89 | 90 | def upload(key, file) 91 | obj = s3.buckets[bucket_name].objects[key] 92 | obj.write(Pathname.new(file)) 93 | obj 94 | end 95 | 96 | def create_app_version(s3_object) 97 | # Elastic Beanstalk doesn't support descriptions longer than 200 characters 98 | description = commit_msg[0, 200] 99 | options = { 100 | :application_name => app_name, 101 | :version_label => version_label, 102 | :description => description, 103 | :source_bundle => { 104 | :s3_bucket => bucket_name, 105 | :s3_key => s3_object.key 106 | }, 107 | :auto_create_application => false 108 | } 109 | eb.create_application_version(options) 110 | end 111 | 112 | def update_app(version) 113 | options = { 114 | :environment_name => env_name, 115 | :version_label => version[:application_version][:version_label] 116 | } 117 | eb.update_environment(options) 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/dpl/provider/s3.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module DPL 4 | class Provider 5 | class S3 < Provider 6 | requires 'aws-sdk-v1' 7 | requires 'mime-types' 8 | 9 | def api 10 | @api ||= AWS::S3.new(endpoint: options[:endpoint] || 's3.amazonaws.com') 11 | end 12 | 13 | def needs_key? 14 | false 15 | end 16 | 17 | def check_app 18 | 19 | end 20 | 21 | def setup_auth 22 | AWS.config(:access_key_id => option(:access_key_id), :secret_access_key => option(:secret_access_key), :region => options[:region]||'us-east-1') 23 | end 24 | 25 | def check_auth 26 | setup_auth 27 | log "Logging in with Access Key: #{option(:access_key_id)[-4..-1].rjust(20, '*')}" 28 | end 29 | 30 | def upload_path(filename) 31 | [options[:upload_dir], filename].compact.join("/") 32 | end 33 | 34 | def push_app 35 | glob_args = ["**/*"] 36 | glob_args << File::FNM_DOTMATCH if options[:dot_match] 37 | Dir.chdir(options.fetch(:local_dir, Dir.pwd)) do 38 | Dir.glob(*glob_args) do |filename| 39 | content_type = MIME::Types.type_for(filename).first.to_s 40 | opts = { :content_type => content_type }.merge(encoding_option_for(filename)) 41 | opts[:cache_control] = get_option_value_by_filename(options[:cache_control], filename) if options[:cache_control] 42 | opts[:acl] = options[:acl] if options[:acl] 43 | opts[:expires] = get_option_value_by_filename(options[:expires], filename) if options[:expires] 44 | unless File.directory?(filename) 45 | log "uploading %p" % filename 46 | api.buckets[option(:bucket)].objects.create(upload_path(filename), File.read(filename), opts) 47 | end 48 | end 49 | end 50 | 51 | if suffix = options[:index_document_suffix] 52 | api.buckets[option(:bucket)].configure_website do |cfg| 53 | cfg.index_document_suffix = suffix 54 | end 55 | end 56 | end 57 | 58 | def deploy 59 | super 60 | rescue AWS::S3::Errors::InvalidAccessKeyId 61 | raise Error, "Invalid S3 Access Key Id, Stopping Deploy" 62 | rescue AWS::S3::Errors::SignatureDoesNotMatch 63 | raise Error, "Aws Secret Key does not match Access Key Id, Stopping Deploy" 64 | rescue AWS::S3::Errors::AccessDenied 65 | raise Error, "Oops, It looks like you tried to write to a bucket that isn't yours or doesn't exist yet. Please create the bucket before trying to write to it." 66 | end 67 | 68 | private 69 | def encoding_option_for(path) 70 | if detect_encoding? && encoding_for(path) 71 | {:content_encoding => encoding_for(path)} 72 | else 73 | {} 74 | end 75 | end 76 | 77 | def get_option_value_by_filename(option_values, filename) 78 | return option_values if !option_values.kind_of?(Array) 79 | preferred_value = nil 80 | hashes = option_values.select {|value| value.kind_of?(Hash) } 81 | hashes.each do |hash| 82 | hash.each do |value, patterns| 83 | unless patterns.kind_of?(Array) 84 | patterns = [patterns] 85 | end 86 | patterns.each do |pattern| 87 | if File.fnmatch?(pattern, filename) 88 | preferred_value = value 89 | end 90 | end 91 | end 92 | end 93 | preferred_value = option_values.select {|value| value.kind_of?(String) }.last if preferred_value.nil? 94 | return preferred_value 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/dpl/provider/ops_works.rb: -------------------------------------------------------------------------------- 1 | require 'timeout' 2 | 3 | module DPL 4 | class Provider 5 | class OpsWorks < Provider 6 | requires 'aws-sdk-v1' 7 | experimental 'AWS OpsWorks' 8 | 9 | def api 10 | @api ||= AWS::OpsWorks.new 11 | end 12 | 13 | def client 14 | @client ||= api.client 15 | end 16 | 17 | def needs_key? 18 | false 19 | end 20 | 21 | def check_app 22 | 23 | end 24 | 25 | def setup_auth 26 | AWS.config(access_key_id: option(:access_key_id), secret_access_key: option(:secret_access_key)) 27 | end 28 | 29 | def check_auth 30 | setup_auth 31 | log "Logging in with Access Key: #{option(:access_key_id)[-4..-1].rjust(20, '*')}" 32 | end 33 | 34 | def custom_json 35 | { 36 | deploy: { 37 | ops_works_app[:shortname] => { 38 | migrate: !!options[:migrate], 39 | scm: { 40 | revision: current_sha 41 | } 42 | } 43 | } 44 | } 45 | end 46 | 47 | def current_sha 48 | @current_sha ||= `git rev-parse HEAD`.chomp 49 | end 50 | 51 | def ops_works_app 52 | @ops_works_app ||= fetch_ops_works_app 53 | end 54 | 55 | def fetch_ops_works_app 56 | data = client.describe_apps(app_ids: [option(:app_id)]) 57 | unless data[:apps] && data[:apps].count == 1 58 | raise Error, "App #{option(:app_id)} not found.", error.backtrace 59 | end 60 | data[:apps].first 61 | end 62 | 63 | def push_app 64 | Timeout::timeout(600) do 65 | create_deployment 66 | end 67 | rescue Timeout::Error 68 | error 'Timeout: Could not finish deployment in 10 minutes.' 69 | end 70 | 71 | def create_deployment 72 | deployment_config = { 73 | stack_id: ops_works_app[:stack_id], 74 | app_id: option(:app_id), 75 | command: {name: 'deploy'}, 76 | comment: travis_deploy_comment, 77 | custom_json: custom_json.to_json 78 | } 79 | if !options[:instance_ids].nil? 80 | deployment_config[:instance_ids] = option(:instance_ids) 81 | end 82 | data = client.create_deployment(deployment_config) 83 | log "Deployment created: #{data[:deployment_id]}" 84 | return unless options[:wait_until_deployed] 85 | print "Deploying " 86 | deployment = wait_until_deployed(data[:deployment_id]) 87 | print "\n" 88 | if deployment[:status] == 'successful' 89 | log "Deployment successful." 90 | else 91 | error "Deployment failed." 92 | end 93 | end 94 | 95 | def wait_until_deployed(deployment_id) 96 | deployment = nil 97 | loop do 98 | result = client.describe_deployments(deployment_ids: [deployment_id]) 99 | deployment = result[:deployments].first 100 | break unless deployment[:status] == "running" 101 | print "." 102 | sleep 5 103 | end 104 | deployment 105 | end 106 | 107 | def travis_deploy_comment 108 | "Deploy build #{context.env['TRAVIS_BUILD_NUMBER'] || current_sha} via Travis CI" 109 | end 110 | 111 | def deploy 112 | super 113 | rescue AWS::Errors::ClientError => error 114 | raise Error, "Stopping Deploy, OpsWorks error: #{error.message}", error.backtrace 115 | rescue AWS::Errors::ServerError => error 116 | raise Error, "Stopping Deploy, OpsWorks server error: #{error.message}", error.backtrace 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/provider/deis_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/deis' 3 | 4 | describe DPL::Provider::Deis do 5 | let(:options) do 6 | { 7 | :app => 'example', 8 | :key_name => 'key', 9 | :controller => 'deis.deisapps.com', 10 | :username => 'travis', 11 | :password => 'secret' 12 | } 13 | end 14 | 15 | subject :provider do 16 | described_class.new(DummyContext.new, options) 17 | end 18 | 19 | describe "#install_deploy_dependencies" do 20 | example 'without version specified' do 21 | expect(provider.class).to receive(:pip).with('deis', 'deis', nil) 22 | provider.install_deploy_dependencies 23 | end 24 | 25 | example 'with version specified' do 26 | options[:cli_version] = '1.0' 27 | expect(provider.class).to receive(:pip).with('deis', 'deis', '1.0') 28 | provider.install_deploy_dependencies 29 | end 30 | end 31 | 32 | describe "#needs_key?" do 33 | example do 34 | expect(provider.needs_key?).to eq(true) 35 | end 36 | end 37 | 38 | describe "#check_auth" do 39 | example do 40 | expect(provider.context).to receive(:shell).with( 41 | 'deis login http://deis.deisapps.com --username=travis --password=secret' 42 | ).and_return(true) 43 | provider.check_auth 44 | end 45 | end 46 | 47 | describe "#check_app" do 48 | example do 49 | expect(provider.context).to receive(:shell).with( 50 | 'deis apps:info --app=example' 51 | ).and_return(true) 52 | provider.check_app 53 | end 54 | end 55 | 56 | describe "#setup_key" do 57 | let(:ssh_config_handle) { double 'ssh_config_handle' } 58 | let(:ssh_config) { File.join(Dir.home, '.ssh', 'config') } 59 | let(:identity_file) { File.join(Dir.pwd, 'key_file') } 60 | example do 61 | expect(provider.context).to receive(:shell).with( 62 | 'deis keys:add key_file' 63 | ).and_return(true) 64 | provider.setup_key('key_file') 65 | end 66 | end 67 | 68 | describe "#setup_git_ssh" do 69 | let(:ssh_config_handle) { double 'ssh_config_handle' } 70 | let(:ssh_config) { File.join(Dir.home, '.ssh', 'config') } 71 | let(:identity_file) { File.join(Dir.pwd, 'key_file') } 72 | let(:git_ssh) { File.join(Dir.pwd, 'foo') } 73 | after { FileUtils.rm provider.context.env.delete('GIT_SSH') } 74 | 75 | example do 76 | expect(File).to receive(:open).with(git_ssh, 'w').and_call_original 77 | expect(File).to receive(:open).with(ssh_config, 'a') 78 | .and_yield(ssh_config_handle) 79 | 80 | expect(ssh_config_handle).to receive(:write).with( 81 | "\nHost deis-repo\n Hostname deis.deisapps.com\n Port 2222\n" \ 82 | " User git\n IdentityFile #{identity_file}\n" 83 | ) 84 | expect(provider.context).to receive(:shell).with( 85 | 'git remote add deis ssh://git@deis.deisapps.com:2222/example.git' 86 | ) 87 | provider.setup_git_ssh('foo', 'key_file') 88 | end 89 | end 90 | 91 | describe "#remove_key" do 92 | example do 93 | expect(provider.context).to receive(:shell).with( 94 | 'deis keys:remove key' 95 | ).and_return(true) 96 | provider.remove_key 97 | end 98 | end 99 | 100 | describe "#push_app" do 101 | example do 102 | expect(provider.context).to receive(:shell).with( 103 | 'git push deis-repo:example.git HEAD:refs/heads/master -f' 104 | ).and_return(true) 105 | provider.push_app 106 | end 107 | end 108 | 109 | describe "#run" do 110 | example do 111 | expect(provider.context).to receive(:shell).with( 112 | 'deis apps:run shell command' 113 | ).and_return(true) 114 | provider.run('shell command') 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/dpl/provider/releases.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class Releases < Provider 4 | require 'pathname' 5 | 6 | requires 'octokit' 7 | requires 'mime-types' 8 | 9 | def travis_tag 10 | # Check if $TRAVIS_TAG is unset or set but empty 11 | if context.env.fetch('TRAVIS_TAG','') == '' 12 | nil 13 | else 14 | context.env['TRAVIS_TAG'] 15 | end 16 | end 17 | 18 | def get_tag 19 | if travis_tag.nil? 20 | @tag ||= `git describe --tags --exact-match 2>/dev/null`.chomp 21 | else 22 | @tag ||= travis_tag 23 | end 24 | end 25 | 26 | def api 27 | if options[:user] and options[:password] 28 | @api ||= Octokit::Client.new(:login => options[:user], :password => options[:password]) 29 | else 30 | @api ||= Octokit::Client.new(:access_token => option(:api_key)) 31 | end 32 | end 33 | 34 | def slug 35 | options.fetch(:repo) { context.env['TRAVIS_REPO_SLUG'] } 36 | end 37 | 38 | def releases 39 | @releases ||= api.releases(slug) 40 | end 41 | 42 | def user 43 | @user ||= api.user 44 | end 45 | 46 | def files 47 | if options[:file_glob] 48 | Array(options[:file]).map do |glob| 49 | Dir.glob(glob) 50 | end.flatten 51 | else 52 | Array(options[:file]) 53 | end 54 | end 55 | 56 | def needs_key? 57 | false 58 | end 59 | 60 | def check_app 61 | log "Deploying to repo: #{slug}" 62 | 63 | context.shell 'git fetch --tags' if travis_tag.nil? 64 | log "Current tag is: #{get_tag}" 65 | end 66 | 67 | def setup_auth 68 | user.login 69 | end 70 | 71 | def check_auth 72 | setup_auth 73 | 74 | unless api.scopes.include? 'public_repo' or api.scopes.include? 'repo' 75 | raise Error, "Dpl does not have permission to upload assets. Make sure your token contains the repo or public_repo scope." 76 | end 77 | 78 | log "Logged in as #{user.name}" 79 | end 80 | 81 | def push_app 82 | tag_matched = false 83 | release_url = nil 84 | 85 | if options[:release_number] 86 | tag_matched = true 87 | release_url = "https://api.github.com/repos/" + slug + "/releases/" + options[:release_number] 88 | else 89 | releases.each do |release| 90 | if release.tag_name == get_tag 91 | release_url = release.rels[:self].href 92 | tag_matched = true 93 | end 94 | end 95 | end 96 | 97 | #If for some reason GitHub hasn't already created a release for the tag, create one 98 | if tag_matched == false 99 | release_url = api.create_release(slug, get_tag, options).rels[:self].href 100 | end 101 | 102 | files.each do |file| 103 | already_exists = false 104 | filename = Pathname.new(file).basename.to_s 105 | api.release(release_url).rels[:assets].get.data.each do |existing_file| 106 | if existing_file.name == filename 107 | already_exists = true 108 | end 109 | end 110 | if already_exists 111 | log "#{filename} already exists, skipping." 112 | else 113 | content_type = MIME::Types.type_for(file).first.to_s 114 | if content_type.empty? 115 | # Specify the default content type, as it is required by GitHub 116 | content_type = "application/octet-stream" 117 | end 118 | api.upload_asset(release_url, file, {:name => filename, :content_type => content_type}) 119 | end 120 | end 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/provider/gcs_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/gcs' 3 | 4 | describe DPL::Provider::GCS do 5 | 6 | subject :provider do 7 | described_class.new(DummyContext.new, :access_key_id => 'qwertyuiopasdfghjklz', :secret_access_key => 'qwertyuiopasdfghjklzqwertyuiopasdfghjklz', :bucket => 'my-bucket') 8 | end 9 | 10 | describe "#check_auth" do 11 | example do 12 | expect(provider).to receive(:log).with("Logging in with Access Key: ****************jklz") 13 | provider.check_auth 14 | end 15 | end 16 | 17 | describe "#needs_key?" do 18 | example do 19 | expect(provider.needs_key?).to eq(false) 20 | end 21 | end 22 | 23 | describe "#upload_path" do 24 | example "Without: :upload_dir" do 25 | filename = "testfile.file" 26 | 27 | expect(provider.upload_path(filename)).to eq("testfile.file") 28 | end 29 | 30 | example "With :upload_dir" do 31 | provider.options.update(:upload_dir => 'BUILD3') 32 | filename = "testfile.file" 33 | 34 | expect(provider.upload_path(filename)).to eq("BUILD3/testfile.file") 35 | end 36 | end 37 | 38 | describe "#push_app" do 39 | example "Without local_dir" do 40 | expect(Dir).to receive(:chdir).with(Dir.pwd) 41 | provider.push_app 42 | end 43 | 44 | example "With local_dir" do 45 | provider.options.update(:local_dir => 'BUILD') 46 | 47 | expect(Dir).to receive(:chdir).with('BUILD') 48 | provider.push_app 49 | end 50 | 51 | example "Sends MIME type" do 52 | expect(Dir).to receive(:glob).and_yield(__FILE__) 53 | expect_any_instance_of(GStore::Client).to receive(:put_object).with( 54 | anything(), 55 | anything(), 56 | hash_including(:headers => {:"Content-Type" => 'application/x-ruby'}) 57 | ) 58 | provider.push_app 59 | end 60 | 61 | example "Sets Cache" do 62 | provider.options.update(:cache_control => "max-age=99999999") 63 | expect(Dir).to receive(:glob).and_yield(__FILE__) 64 | expect_any_instance_of(GStore::Client).to receive(:put_object).with( 65 | anything(), 66 | anything(), 67 | hash_including(:headers => hash_including("Cache-Control" => 'max-age=99999999')) 68 | ) 69 | provider.push_app 70 | end 71 | 72 | example "Sets ACL" do 73 | provider.options.update(:acl => "public-read") 74 | expect(Dir).to receive(:glob).and_yield(__FILE__) 75 | expect_any_instance_of(GStore::Client).to receive(:put_object).with( 76 | anything(), 77 | anything(), 78 | hash_including(:headers => hash_including("x-goog-acl" => 'public-read')) 79 | ) 80 | provider.push_app 81 | end 82 | 83 | example "when detect_encoding is set" do 84 | path = 'foo.js' 85 | provider.options.update(:detect_encoding => true) 86 | expect(Dir).to receive(:glob).and_yield(path) 87 | expect(provider).to receive(:`).at_least(1).times.with("file #{path}").and_return('gzip compressed') 88 | expect(File).to receive(:read).with(path).and_return("") 89 | expect_any_instance_of(GStore::Client).to receive(:put_object).with( 90 | anything(), 91 | anything(), 92 | hash_including(:headers => hash_including("Content-Encoding" => 'gzip')) 93 | ) 94 | provider.push_app 95 | end 96 | 97 | example "With dot_match" do 98 | provider.options.update(:dot_match => true) 99 | 100 | expect(Dir).to receive(:glob).with('**/*', File::FNM_DOTMATCH) 101 | provider.push_app 102 | end 103 | 104 | end 105 | 106 | describe '#client' do 107 | example do 108 | expect(GStore::Client).to receive(:new).with( 109 | :access_key => 'qwertyuiopasdfghjklz', 110 | :secret_key => 'qwertyuiopasdfghjklzqwertyuiopasdfghjklz' 111 | ) 112 | provider.client 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /spec/provider/heroku_git_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'heroku-api' 3 | require 'dpl/provider/heroku' 4 | 5 | describe DPL::Provider::Heroku do 6 | subject :provider do 7 | described_class.new(DummyContext.new, :app => 'example', :key_name => 'key', :api_key => "foo", :strategy => "git-ssh") 8 | end 9 | 10 | let(:expected_headers) do 11 | { "User-Agent" => "dpl/#{DPL::VERSION} heroku-rb/#{Heroku::API::VERSION}" } 12 | end 13 | 14 | describe "#api" do 15 | it 'accepts an api key' do 16 | api = double(:api) 17 | expect(::Heroku::API).to receive(:new).with(:api_key => "foo", :headers => expected_headers).and_return(api) 18 | expect(provider.api).to eq(api) 19 | end 20 | 21 | it 'accepts a user and a password' do 22 | api = double(:api) 23 | provider.options.update(:user => "foo", :password => "bar") 24 | expect(::Heroku::API).to receive(:new).with(:user => "foo", :password => "bar", :headers => expected_headers).and_return(api) 25 | expect(provider.api).to eq(api) 26 | end 27 | end 28 | 29 | context "with fake api" do 30 | let :api do 31 | double "api", 32 | :get_user => double("get_user", :body => { "email" => "foo@bar.com" }), 33 | :get_app => double("get_app", :body => { "name" => "example", "git_url" => "GIT URL" }) 34 | end 35 | 36 | before do 37 | expect(::Heroku::API).to receive(:new).and_return(api) 38 | provider.api 39 | end 40 | 41 | its(:api) { should be == api } 42 | 43 | describe "#check_auth" do 44 | example do 45 | expect(provider).to receive(:log).with("authenticated as foo@bar.com") 46 | provider.check_auth 47 | end 48 | end 49 | 50 | describe "#check_app" do 51 | example do 52 | expect(provider).to receive(:log).at_least(1).times.with(/example/) 53 | provider.check_app 54 | end 55 | end 56 | 57 | describe "#setup_key" do 58 | example do 59 | expect(File).to receive(:read).with("the file").and_return("foo") 60 | expect(api).to receive(:post_key).with("foo") 61 | provider.setup_key("the file") 62 | end 63 | end 64 | 65 | describe "#remove_key" do 66 | example do 67 | expect(api).to receive(:delete_key).with("key") 68 | provider.remove_key 69 | end 70 | end 71 | 72 | describe "#push_app" do 73 | example do 74 | provider.options[:git] = "git://something" 75 | expect(provider.context).to receive(:shell).with("git push git://something HEAD:refs/heads/master -f") 76 | provider.push_app 77 | expect(provider.context.env['GIT_HTTP_USER_AGENT']).to include("dpl/#{DPL::VERSION}") 78 | end 79 | end 80 | 81 | describe "#run" do 82 | example do 83 | data = double("data", :body => { "rendezvous_url" => "rendezvous url" }) 84 | expect(api).to receive(:post_ps).with("example", "that command", :attach => true).and_return(data) 85 | expect(Rendezvous).to receive(:start).with(:url => "rendezvous url") 86 | provider.run("that command") 87 | end 88 | end 89 | 90 | describe "#restart" do 91 | example do 92 | expect(api).to receive(:post_ps_restart).with("example") 93 | provider.restart 94 | end 95 | end 96 | 97 | describe "#deploy" do 98 | example "not found error" do 99 | expect(provider).to receive(:api) { raise ::Heroku::API::Errors::NotFound.new("the message", nil) }.at_least(:once) 100 | expect { provider.deploy }.to raise_error(DPL::Error, 'the message (wrong app "example"?)') 101 | end 102 | 103 | example "unauthorized error" do 104 | expect(provider).to receive(:api) { raise ::Heroku::API::Errors::Unauthorized.new("the message", nil) }.at_least(:once) 105 | expect { provider.deploy }.to raise_error(DPL::Error, 'the message (wrong API key?)') 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/provider/rubygems_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rubygems' 3 | require 'gems' 4 | require 'dpl/provider/rubygems' 5 | 6 | describe DPL::Provider::RubyGems do 7 | subject :provider do 8 | described_class.new(DummyContext.new, :app => 'example', :api_key => 'foo') 9 | end 10 | 11 | describe "#api" do 12 | example "with an api key" do 13 | expect(::Gems).to receive(:key=).with('foo') 14 | provider.setup_auth 15 | end 16 | 17 | example "with a username and password" do 18 | provider.options.update(:user => 'test', :password => 'blah') 19 | provider.options.delete(:api_key) 20 | expect(::Gems).to receive(:username=).with('test') 21 | expect(::Gems).to receive(:password=).with('blah') 22 | provider.setup_auth 23 | end 24 | end 25 | 26 | describe "#check_auth" do 27 | example do 28 | provider.options.update(:user => 'test', :password => 'blah') 29 | provider.options.delete(:api_key) 30 | expect(provider).to receive(:log).with("Authenticated with username test") 31 | provider.check_auth 32 | end 33 | end 34 | 35 | describe "#check_app" do 36 | example do 37 | expect(::Gems).to receive(:info).with('example').and_return({'name' => 'example'}) 38 | expect(provider).to receive(:log).with("Found gem example") 39 | provider.check_app 40 | end 41 | end 42 | 43 | describe "#push_app" do 44 | after(:each) do 45 | expect(File).to receive(:new).with('File').and_return('Test file') 46 | expect(provider).to receive(:log).with('Yes!') 47 | provider.push_app 48 | end 49 | 50 | example "with options[:app]" do 51 | provider.options.update(:app => 'example') 52 | expect(provider.context).to receive(:shell).with("gem build example.gemspec") 53 | expect(Dir).to receive(:glob).with('example-*.gem').and_yield('File') 54 | expect(::Gems).to receive(:push).with('Test file').and_return('Yes!') 55 | end 56 | 57 | example "with options[:gem]" do 58 | provider.options.update(:gem => 'example-gem') 59 | expect(provider.context).to receive(:shell).with("gem build example-gem.gemspec") 60 | expect(Dir).to receive(:glob).with('example-gem-*.gem').and_yield('File') 61 | expect(::Gems).to receive(:push).with('Test file').and_return('Yes!') 62 | end 63 | 64 | example "with options[:gemspec]" do 65 | provider.options.update(:gemspec => 'blah.gemspec') 66 | expect(provider.context).to receive(:shell).with("gem build blah.gemspec") 67 | expect(Dir).to receive(:glob).with('blah-*.gem').and_yield('File') 68 | expect(::Gems).to receive(:push).with('Test file').and_return('Yes!') 69 | end 70 | 71 | example "with options[:host]" do 72 | provider.options.update(:host => 'http://example.com') 73 | expect(provider.context).to receive(:shell).with("gem build example.gemspec") 74 | expect(Dir).to receive(:glob).with('example-*.gem').and_yield('File') 75 | expect(::Gems).to receive(:push).with('Test file', host='http://example.com').and_return('Yes!') 76 | end 77 | end 78 | 79 | describe "#setup_gem" do 80 | example "with options[:gem] and options[:app] set" do 81 | provider.options.update(:gem => 'test', :app => 'blah') 82 | provider.setup_gem 83 | expect(provider.options[:gem]).to eq('test') 84 | end 85 | 86 | example "with options[:app] set" do 87 | provider.options.update(:app => 'foo') 88 | provider.setup_gem 89 | expect(provider.options[:gem]).to eq('foo') 90 | end 91 | 92 | example "with options[:gem] set" do 93 | provider.options.update(:gem => 'bar') 94 | provider.setup_gem 95 | expect(provider.options[:gem]).to eq('bar') 96 | end 97 | end 98 | 99 | describe "#gemspec" do 100 | example do 101 | provider.options.update(:gemspec => 'test.gemspec') 102 | expect(provider.gemspec).to eq('test') 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/provider/ops_works_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'aws-sdk-v1' 3 | require 'dpl/provider' 4 | require 'dpl/provider/ops_works' 5 | 6 | describe DPL::Provider::OpsWorks do 7 | 8 | before (:each) do 9 | AWS.stub! 10 | end 11 | 12 | subject :provider do 13 | described_class.new(DummyContext.new, :access_key_id => 'qwertyuiopasdfghjklz', :secret_access_key => 'qwertyuiopasdfghjklzqwertyuiopasdfghjklz', :bucket => 'my-bucket') 14 | end 15 | 16 | describe "#check_auth" do 17 | example do 18 | expect(provider).to receive(:setup_auth) 19 | expect(provider).to receive(:log).with('Logging in with Access Key: ****************jklz') 20 | provider.check_auth 21 | end 22 | end 23 | 24 | describe "#setup_auth" do 25 | example do 26 | expect(AWS).to receive(:config).with(:access_key_id => 'qwertyuiopasdfghjklz', :secret_access_key => 'qwertyuiopasdfghjklzqwertyuiopasdfghjklz').once.and_call_original 27 | provider.setup_auth 28 | end 29 | end 30 | 31 | describe "#needs_key?" do 32 | example do 33 | expect(provider.needs_key?).to eq(false) 34 | end 35 | end 36 | 37 | describe "#push_app" do 38 | let(:client) { double(:ops_works_client) } 39 | let(:ops_works_app) { {shortname: 'app', stack_id: 'stack-id'} } 40 | before do 41 | expect(provider).to receive(:current_sha).and_return('sha') 42 | expect(provider.api).to receive(:client).and_return(client) 43 | expect(provider.context.env).to receive(:[]).with('TRAVIS_BUILD_NUMBER').and_return('123') 44 | end 45 | 46 | let(:custom_json) { "{\"deploy\":{\"app\":{\"migrate\":false,\"scm\":{\"revision\":\"sha\"}}}}" } 47 | example 'without :migrate option' do 48 | provider.options.update(app_id: 'app-id') 49 | expect(client).to receive(:describe_apps).with(app_ids: ['app-id']).and_return({apps: [ops_works_app]} 50 | ) 51 | expect(client).to receive(:create_deployment).with( 52 | stack_id: 'stack-id', app_id: 'app-id', command: {name: 'deploy'}, comment: 'Deploy build 123 via Travis CI', custom_json: custom_json 53 | ).and_return({}) 54 | provider.push_app 55 | end 56 | 57 | let(:custom_json_with_migrate) { "{\"deploy\":{\"app\":{\"migrate\":true,\"scm\":{\"revision\":\"sha\"}}}}" } 58 | example 'with :migrate option' do 59 | provider.options.update(app_id: 'app-id', migrate: true) 60 | expect(client).to receive(:describe_apps).with(app_ids: ['app-id']).and_return({apps: [ops_works_app]}) 61 | expect(client).to receive(:create_deployment).with( 62 | stack_id: 'stack-id', app_id: 'app-id', command: {name: 'deploy'}, comment: 'Deploy build 123 via Travis CI', custom_json: custom_json_with_migrate 63 | ).and_return({}) 64 | provider.push_app 65 | end 66 | 67 | example 'with :wait_until_deployed' do 68 | provider.options.update(app_id: 'app-id', wait_until_deployed: true) 69 | expect(client).to receive(:describe_apps).with(app_ids: ['app-id']).and_return({apps: [ops_works_app]}) 70 | expect(client).to receive(:create_deployment).and_return({deployment_id: 'deployment_id'}) 71 | expect(client).to receive(:describe_deployments).with({deployment_ids: ['deployment_id']}).and_return({deployments: [status: 'running']}, {deployments: [status: 'successful']}) 72 | provider.push_app 73 | end 74 | 75 | example 'with :instance-ids' do 76 | provider.options.update(app_id: 'app-id', instance_ids: ['instance-id']) 77 | expect(client).to receive(:describe_apps).with(app_ids: ['app-id']).and_return({apps: [ops_works_app]}) 78 | expect(client).to receive(:create_deployment).with( 79 | stack_id: 'stack-id', app_id: 'app-id', instance_ids:['instance-id'], command: {name: 'deploy'}, comment: 'Deploy build 123 via Travis CI', custom_json: custom_json 80 | ).and_return({}) 81 | provider.push_app 82 | end 83 | end 84 | 85 | describe "#api" do 86 | example do 87 | expect(AWS::OpsWorks).to receive(:new) 88 | provider.api 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/dpl/provider/packagecloud.rb: -------------------------------------------------------------------------------- 1 | module DPL 2 | class Provider 3 | class Packagecloud < Provider 4 | requires 'packagecloud-ruby', :version => "0.2.17", :load => 'packagecloud' 5 | 6 | def check_auth 7 | setup_auth 8 | begin 9 | @client = ::Packagecloud::Client.new(@creds, "travis-ci") 10 | rescue ::Packagecloud::UnauthenticatedException 11 | error "Could not authenticate to https://packagecloud.io, please check credentials" 12 | end 13 | end 14 | 15 | def needs_key? 16 | false 17 | end 18 | 19 | def setup_auth 20 | @username = option(:username) 21 | @token = option(:token) 22 | @repo = option(:repository) 23 | @dist = option(:dist) if options[:dist] 24 | @creds = ::Packagecloud::Credentials.new(@username, @token) 25 | log "Logging into https://packagecloud.io with #{@username}:#{@token[-4..-1].rjust(20, '*')}" 26 | end 27 | 28 | def get_distro(query) 29 | distro = nil 30 | begin 31 | distro = @client.find_distribution_id(query) 32 | rescue ArgumentError => exception 33 | error "Error: #{exception.message}" 34 | end 35 | if distro.nil? 36 | error "Could not find distribution named #{query}" 37 | end 38 | distro 39 | end 40 | 41 | def is_supported_package?(filename) 42 | ext = File.extname(filename).gsub!('.','') 43 | ::Packagecloud::SUPPORTED_EXTENSIONS.include?(ext) 44 | end 45 | 46 | def dist_required?(filename) 47 | ext = File.extname(filename).gsub!('.','') 48 | ["rpm", "deb", "dsc"].include?(ext) 49 | end 50 | 51 | def error_if_dist_required(filename) 52 | ext = File.extname(filename).gsub!('.','') 53 | if dist_required?(ext) && @dist.nil? 54 | error "Distribution needed for rpm, deb, and dsc packages, example --dist='ubuntu/breezy'" 55 | end 56 | end 57 | 58 | def is_source_package?(filename) 59 | ext = File.extname(filename).gsub!('.','') 60 | ext == 'dsc' 61 | end 62 | 63 | def get_source_files_for(orig_filename) 64 | source_files = {} 65 | glob_args = ["**/*"] 66 | package = ::Packagecloud::Package.new(open(orig_filename)) 67 | result = @client.package_contents(@repo, package) 68 | if result.succeeded 69 | package_contents_files = result.response["files"].map { |x| x["filename"] } 70 | Dir.chdir(options.fetch(:local_dir, Dir.pwd)) do 71 | Dir.glob(*glob_args) do |filename| 72 | unless File.directory?(filename) 73 | basename = File.basename(filename) 74 | if package_contents_files.include?(basename) 75 | log "Found source fragment: #{basename} for #{orig_filename}" 76 | source_files = source_files.merge({basename => open(filename)}) 77 | end 78 | end 79 | end 80 | end 81 | else 82 | error "Error: #{result.response}" 83 | end 84 | source_files 85 | end 86 | 87 | def push_app 88 | packages = [] 89 | glob_args = ["**/*"] 90 | Dir.chdir(options.fetch(:local_dir, Dir.pwd)) do 91 | Dir.glob(*glob_args) do |filename| 92 | unless File.directory?(filename) 93 | if is_supported_package?(filename) 94 | error_if_dist_required(filename) 95 | log "Detected supported package: #{filename}" 96 | if dist_required?(filename) 97 | if is_source_package?(filename) 98 | log "Processing source package: #{filename}" 99 | source_files = get_source_files_for(filename) 100 | packages << ::Packagecloud::Package.new(open(filename), get_distro(@dist), source_files, filename) 101 | else 102 | packages << ::Packagecloud::Package.new(open(filename), get_distro(@dist), {}, filename) 103 | end 104 | else 105 | packages << ::Packagecloud::Package.new(open(filename), nil, {}, filename) 106 | end 107 | end 108 | end 109 | end 110 | end 111 | 112 | packages.each do |package| 113 | result = @client.put_package(@repo, package) 114 | if result.succeeded 115 | log "Successfully pushed #{package.filename} to #{@username}/#{@repo}" 116 | else 117 | error "Error #{result.response}" 118 | end 119 | end 120 | if packages.empty? 121 | error "Error: No supported packages found! Perhaps try skip_cleanup: true" 122 | end 123 | end 124 | 125 | end 126 | 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /spec/provider/s3_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'aws-sdk-v1' 3 | require 'dpl/provider/s3' 4 | 5 | describe DPL::Provider::S3 do 6 | 7 | before (:each) do 8 | AWS.stub! 9 | end 10 | 11 | subject :provider do 12 | described_class.new(DummyContext.new, :access_key_id => 'qwertyuiopasdfghjklz', :secret_access_key => 'qwertyuiopasdfghjklzqwertyuiopasdfghjklz', :bucket => 'my-bucket') 13 | end 14 | 15 | describe "#check_auth" do 16 | example do 17 | expect(provider).to receive(:setup_auth) 18 | expect(provider).to receive(:log).with("Logging in with Access Key: ****************jklz") 19 | provider.check_auth 20 | end 21 | end 22 | 23 | describe "#upload_path" do 24 | example "Without :upload_dir"do 25 | filename = "testfile.file" 26 | 27 | expect(provider.upload_path(filename)).to eq("testfile.file") 28 | end 29 | 30 | example "With :upload_dir" do 31 | provider.options.update(:upload_dir => 'BUILD3') 32 | filename = "testfile.file" 33 | 34 | expect(provider.upload_path(filename)).to eq("BUILD3/testfile.file") 35 | end 36 | end 37 | 38 | describe "#setup_auth" do 39 | example "Without :region" do 40 | expect(AWS).to receive(:config).with(:access_key_id => 'qwertyuiopasdfghjklz', :secret_access_key => 'qwertyuiopasdfghjklzqwertyuiopasdfghjklz', :region => 'us-east-1').once.and_call_original 41 | provider.setup_auth 42 | end 43 | example "With :region" do 44 | provider.options.update(:region => 'us-west-2') 45 | 46 | expect(AWS).to receive(:config).with(:access_key_id => 'qwertyuiopasdfghjklz', :secret_access_key => 'qwertyuiopasdfghjklzqwertyuiopasdfghjklz', :region => 'us-west-2').once 47 | provider.setup_auth 48 | end 49 | end 50 | 51 | describe "#needs_key?" do 52 | example do 53 | expect(provider.needs_key?).to eq(false) 54 | end 55 | end 56 | 57 | describe "#push_app" do 58 | example "Without local_dir" do 59 | expect(Dir).to receive(:chdir).with(Dir.pwd) 60 | provider.push_app 61 | end 62 | 63 | example "With local_dir" do 64 | provider.options.update(:local_dir => 'BUILD') 65 | 66 | expect(Dir).to receive(:chdir).with('BUILD') 67 | provider.push_app 68 | end 69 | 70 | example "Sends MIME type" do 71 | expect(Dir).to receive(:glob).and_yield(__FILE__) 72 | expect_any_instance_of(AWS::S3::ObjectCollection).to receive(:create).with(anything(), anything(), hash_including(:content_type => 'application/x-ruby')) 73 | provider.push_app 74 | end 75 | 76 | example "Sets Cache and Expiration" do 77 | provider.options.update(:cache_control => "max-age=99999999", :expires => "2012-12-21 00:00:00 -0000") 78 | expect(Dir).to receive(:glob).and_yield(__FILE__) 79 | expect_any_instance_of(AWS::S3::ObjectCollection).to receive(:create).with(anything(), anything(), hash_including(:cache_control => 'max-age=99999999', :expires => '2012-12-21 00:00:00 -0000')) 80 | provider.push_app 81 | end 82 | 83 | example "Sets different Cache and Expiration" do 84 | option_list = [] 85 | provider.options.update(:cache_control => ["max-age=99999999", "no-cache" => ["foo.html", "bar.txt"], "max-age=9999" => "*.txt"], :expires => ["2012-12-21 00:00:00 -0000", "1970-01-01 00:00:00 -0000" => "*.html"]) 86 | expect(Dir).to receive(:glob).and_yield("foo.html").and_yield("bar.txt").and_yield("baz.js") 87 | expect(File).to receive(:read).exactly(3).times.and_return("") 88 | allow_any_instance_of(AWS::S3::ObjectCollection).to receive(:create) do |_instance, key, _data, options| 89 | option_list << { key: key, options: options } 90 | end 91 | provider.push_app 92 | expect(option_list).to match_array([ 93 | { key: "foo.html", options: hash_including(:cache_control => "no-cache", :expires => "1970-01-01 00:00:00 -0000") }, 94 | { key: "bar.txt", options: hash_including(:cache_control => "max-age=9999", :expires => "2012-12-21 00:00:00 -0000") }, 95 | { key: "baz.js", options: hash_including(:cache_control => "max-age=99999999", :expires => "2012-12-21 00:00:00 -0000") }, 96 | ]) 97 | end 98 | 99 | example "Sets ACL" do 100 | provider.options.update(:acl => "public_read") 101 | expect(Dir).to receive(:glob).and_yield(__FILE__) 102 | expect_any_instance_of(AWS::S3::ObjectCollection).to receive(:create).with(anything(), anything(), hash_including(:acl => "public_read")) 103 | provider.push_app 104 | end 105 | 106 | example "Sets Website Index Document" do 107 | provider.options.update(:index_document_suffix => "test/index.html") 108 | expect(Dir).to receive(:glob).and_yield(__FILE__) 109 | provider.push_app 110 | end 111 | 112 | example "when detect_encoding is set" do 113 | path = 'foo.js' 114 | provider.options.update(:detect_encoding => true) 115 | expect(Dir).to receive(:glob).and_yield(path) 116 | expect(provider).to receive(:`).at_least(1).times.with("file #{path}").and_return('gzip compressed') 117 | expect(File).to receive(:read).with(path).and_return("") 118 | expect_any_instance_of(AWS::S3::ObjectCollection).to receive(:create).with(anything(), anything(), hash_including(:content_encoding => 'gzip')) 119 | provider.push_app 120 | end 121 | 122 | example "when dot_match is set" do 123 | provider.options.update(:dot_match => true) 124 | expect(Dir).to receive(:glob).with("**/*", File::FNM_DOTMATCH) 125 | provider.push_app 126 | end 127 | end 128 | 129 | describe "#api" do 130 | example "Without Endpoint" do 131 | expect(AWS::S3).to receive(:new).with(:endpoint => 's3.amazonaws.com') 132 | provider.api 133 | end 134 | example "With Endpoint" do 135 | provider.options.update(:endpoint => 's3test.com.s3-website-us-west-2.amazonaws.com') 136 | expect(AWS::S3).to receive(:new).with(:endpoint => 's3test.com.s3-website-us-west-2.amazonaws.com') 137 | provider.api 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /spec/provider_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider' 3 | 4 | describe DPL::Provider do 5 | let(:example_provider) { Class.new(described_class)} 6 | subject(:provider) { example_provider.new(DummyContext.new, :app => 'example', :key_name => 'foo', :run => ["foo", "bar"]) } 7 | 8 | before { stub_const "DPL::Provider::Example", example_provider } 9 | 10 | describe "#new" do 11 | example { expect(described_class.new(DummyContext.new, :provider => "example")) .to be_an(example_provider) } 12 | example { expect(described_class.new(DummyContext.new, :provider => "Example")) .to be_an(example_provider) } 13 | example { expect(described_class.new(DummyContext.new, :provider => "exa_mple")).to be_an(example_provider) } 14 | example { expect(described_class.new(DummyContext.new, :provider => "exa-mple")).to be_an(example_provider) } 15 | example "install deployment dependencies" do 16 | expect_any_instance_of(described_class).to receive(:respond_to?).with(:install_deploy_dependencies).and_return(true) 17 | expect_any_instance_of(described_class).to receive(:install_deploy_dependencies) 18 | described_class.new(DummyContext.new, :provider => "example") 19 | end 20 | end 21 | 22 | describe "#requires" do 23 | before do 24 | expect(example_provider).to receive(:require).with("foo") 25 | end 26 | 27 | example "installed" do 28 | expect(example_provider).to receive(:gem).with("foo", "~> 1.4") 29 | example_provider.requires("foo", :version => "~> 1.4") 30 | end 31 | 32 | example "missing" do 33 | expect(example_provider).to receive(:gem).with("foo", "~> 1.4").and_raise(LoadError) 34 | expect(example_provider.context).to receive(:shell).with('gem install foo -v "~> 1.4" --no-ri --no-rdoc ', retry: true) 35 | example_provider.requires("foo", :version => "~> 1.4") 36 | end 37 | end 38 | 39 | describe "#apt_get" do 40 | example "installed" do 41 | expect(example_provider).to receive(:`).with("which foo").and_return("/bin/foo\n") 42 | expect(example_provider).not_to receive(:system) 43 | example_provider.apt_get("foo") 44 | end 45 | 46 | example "missing" do 47 | expect(example_provider).to receive(:`).with("which foo").and_return("") 48 | expect(example_provider.context).to receive(:shell).with("sudo apt-get -qq install foo", retry: true) 49 | example_provider.apt_get("foo") 50 | end 51 | end 52 | 53 | describe "#pip" do 54 | example "installed" do 55 | expect(example_provider).to receive(:`).with("which foo").and_return("/bin/foo\n") 56 | expect(example_provider).not_to receive(:system) 57 | example_provider.pip("foo") 58 | end 59 | 60 | example "missing" do 61 | expect(example_provider).to receive(:`).with("which foo").and_return("") 62 | expect(example_provider.context).to receive(:shell).with("sudo pip install foo", retry: true) 63 | example_provider.pip("foo") 64 | end 65 | 66 | example "specific version" do 67 | expect(example_provider).to receive(:`).with("which foo").and_return("") 68 | expect(example_provider.context).to receive(:shell).with("sudo pip install foo==1.0", retry: true) 69 | example_provider.pip("foo", "foo", "1.0") 70 | end 71 | end 72 | 73 | describe "#deploy" do 74 | before do 75 | expect(provider).to receive(:check_auth) 76 | expect(provider).to receive(:check_app) 77 | expect(provider).to receive(:push_app) 78 | expect(provider).to receive(:run).with("foo") 79 | expect(provider).to receive(:run).with("bar") 80 | end 81 | 82 | example "needs key" do 83 | expect(provider).to receive(:remove_key) 84 | expect(provider).to receive(:create_key) 85 | expect(provider).to receive(:setup_key) 86 | expect(provider).to receive(:setup_git_ssh) 87 | provider.deploy 88 | end 89 | 90 | example "does not need key" do 91 | allow(provider).to receive_messages(:needs_key? => false) 92 | provider.deploy 93 | end 94 | end 95 | 96 | describe "#cleanup" do 97 | example do 98 | expect(provider.context).to receive(:shell).with('mv .dpl ~/dpl') 99 | expect(provider.context).to receive(:shell).with('git stash --all') 100 | expect(provider.context).to receive(:shell).with('mv ~/dpl .dpl') 101 | provider.cleanup 102 | end 103 | 104 | example "skip cleanup" do 105 | expect(provider.options).to receive(:[]).with(:skip_cleanup).and_return("true") 106 | expect(provider.context).not_to receive(:shell) 107 | provider.cleanup 108 | end 109 | end 110 | 111 | describe "#uncleanup" do 112 | example do 113 | expect(provider.context).to receive(:shell).with('git stash pop') 114 | provider.uncleanup 115 | end 116 | 117 | example "skip cleanup" do 118 | expect(provider.options).to receive(:[]).with(:skip_cleanup).and_return("true") 119 | expect(provider.context).not_to receive(:shell) 120 | provider.uncleanup 121 | end 122 | end 123 | 124 | describe "#create_key" do 125 | example do 126 | expect(provider.context).to receive(:shell).with('ssh-keygen -t rsa -N "" -C foo -f thekey') 127 | provider.create_key('thekey') 128 | end 129 | end 130 | 131 | describe "#setup_git_ssh" do 132 | after { FileUtils.rm provider.context.env.delete('GIT_SSH') } 133 | 134 | example do 135 | provider.setup_git_ssh('foo', 'bar') 136 | expect(provider.context.env['GIT_SSH']).to eq(File.expand_path('foo')) 137 | end 138 | end 139 | 140 | describe "#detect_encoding?" do 141 | example do 142 | provider.options.update(:detect_encoding => true) 143 | expect(provider.detect_encoding?).to eq(true) 144 | end 145 | end 146 | 147 | describe "#encoding_for" do 148 | example do 149 | path = 'foo.js' 150 | expect(provider).to receive(:`).at_least(1).times.with("file #{path}").and_return('gzip compressed') 151 | expect(provider.encoding_for(path)).to eq('gzip') 152 | end 153 | end 154 | 155 | describe "#log" do 156 | example do 157 | expect($stderr).to receive(:puts).with("foo") 158 | provider.log("foo") 159 | end 160 | end 161 | 162 | describe "#shell" do 163 | example do 164 | expect(example_provider).to receive(:system).with("command") 165 | example_provider.shell("command") 166 | end 167 | end 168 | 169 | describe "#npm_g" do 170 | example do 171 | expect(example_provider.context).to receive(:shell).with("npm install -g foo", retry: true) 172 | example_provider.npm_g("foo") 173 | end 174 | end 175 | 176 | describe "#run" do 177 | example do 178 | expect(provider).to receive(:error).with("running commands not supported") 179 | provider.run "blah" 180 | end 181 | end 182 | 183 | describe "#error" do 184 | example do 185 | expect { provider.error("Foo") }.to raise_error("Foo") 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /spec/provider/cloudcontrol_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/cloudcontrol' 3 | 4 | describe DPL::Provider::CloudControl do 5 | subject :provider do 6 | described_class.new(DummyContext.new, :deployment => 'foo_app/default', :email => 'foo@test.com', :password => 'password') 7 | end 8 | 9 | its(:app_name) { should == 'foo_app' } 10 | its(:dep_name) { should == 'default' } 11 | 12 | its(:needs_key?) { should be true } 13 | 14 | describe 'constructor' do 15 | it 'with wrong arguments' do 16 | expect { 17 | described_class.new(DummyContext.new, :foo_dep => 'foo_app/default', :email => 'foo@test.com', :password => 'password') 18 | }.to raise_error(DPL::Error) 19 | end 20 | end 21 | 22 | it '#check_auth should call #headers_with_token' do 23 | expect(provider).to receive(:headers_with_token) 24 | provider.check_auth 25 | end 26 | 27 | describe '#check_app' do 28 | it 'on deployment found' do 29 | expect(provider).to receive(:api_call).and_return double( 30 | :code => '200', 31 | :body => '{"branch":"foo_repo.git"}' 32 | ) 33 | expect(provider.instance_variable_get(:@repository)).to be_nil 34 | provider.check_app 35 | expect(provider.instance_variable_get(:@repository)).to eq('foo_repo.git') 36 | end 37 | 38 | it 'on deployment not found' do 39 | expect(provider).to receive(:api_call).and_return double(:code => '410') 40 | expect { provider.check_app }.to raise_error(DPL::Error) 41 | end 42 | end 43 | 44 | describe '#setup_key' do 45 | before do 46 | expect(File).to receive(:read).with('file').and_return('foo_key') 47 | expect(provider).to receive(:user).and_return({ 'username' => 'foo_user' }) 48 | end 49 | 50 | it 'on api success' do 51 | expect(provider).to receive(:api_call).with('POST', '/user/foo_user/key', '{"key":"foo_key"}').and_return double( 52 | :code => '200', 53 | :body => '{ "key": "foo_key", "key_id": "foo_key_id"}' 54 | ) 55 | 56 | expect(provider.instance_variable_get(:@ssh_key_id)).to be_nil 57 | provider.setup_key 'file' 58 | expect(provider.instance_variable_get(:@ssh_key_id)).to eq('foo_key_id') 59 | end 60 | 61 | it 'on api failure' do 62 | expect(provider).to receive(:api_call).with('POST', '/user/foo_user/key', '{"key":"foo_key"}').and_return double(:code => '401') 63 | 64 | expect { provider.setup_key 'file' }.to raise_error(DPL::Error) 65 | end 66 | end 67 | 68 | describe '#remove_key' do 69 | before do 70 | provider.instance_variable_set(:@ssh_key_id, 'foo_key_id') 71 | expect(provider).to receive(:user).and_return({ 'username' => 'foo_user' }) 72 | end 73 | 74 | it 'on api success' do 75 | expect(provider).to receive(:api_call).with('DELETE', '/user/foo_user/key/foo_key_id').and_return double(:code => '204') 76 | provider.remove_key 77 | end 78 | 79 | it 'on api failure' do 80 | expect(provider).to receive(:api_call).with('DELETE', '/user/foo_user/key/foo_key_id').and_return double(:code => '410') 81 | expect { provider.remove_key }.to raise_error(DPL::Error) 82 | end 83 | end 84 | 85 | it '#push_app shuld deploy the app' do 86 | provider.instance_variable_set(:@repository, 'foo_repo.git') 87 | context = double(:shell) 88 | expect(context).to receive(:shell).with("git push foo_repo.git HEAD:master -f") 89 | expect(provider).to receive(:context).and_return context 90 | expect(provider).to receive(:deploy_app) 91 | 92 | provider.push_app 93 | end 94 | 95 | describe 'private method' do 96 | describe '#get_token' do 97 | it 'on api success' do 98 | request = double() 99 | expect(request).to receive(:basic_auth).with('foo@test.com', 'password') 100 | expect(Net::HTTP::Post).to receive(:new).with('/token/').and_return request 101 | 102 | expect(provider.instance_variable_get(:@http)).to receive(:request).and_return double( 103 | :code => '200', 104 | :body => '{ "token": "foo_token"}' 105 | ) 106 | 107 | expect(provider.instance_eval { get_token }).to eq({ 'token' => 'foo_token' }) 108 | end 109 | 110 | it 'on api failure' do 111 | expect(provider.instance_variable_get(:@http)).to receive(:request).and_return double(:code => '401') 112 | 113 | expect do 114 | provider.instance_eval { get_token } 115 | end.to raise_error(DPL::Error) 116 | end 117 | end 118 | 119 | it '#headers_with_token should return headers' do 120 | expect(provider).to receive(:get_token).and_return({ 'token' => 'foo_token' }) 121 | expected_return = { 122 | 'Authorization' => 'cc_auth_token="foo_token"', 123 | 'Content-Type' => 'application/json' 124 | } 125 | 126 | expect(provider.instance_eval { headers_with_token }).to eq(expected_return) 127 | end 128 | 129 | describe '#get_headers' do 130 | let(:expected_args) { [ 'GET', '/user/', nil, {'foo' => 'headers'} ] } 131 | 132 | before do 133 | expect(provider).to receive(:headers_with_token).and_return({ 'foo' => 'headers' }) 134 | end 135 | 136 | it 'on token valid' do 137 | expect(provider).to receive(:api_call).with(*expected_args).and_return double(:code => '200') 138 | expect(provider.instance_eval { get_headers }).to eq({ 'foo' => 'headers' }) 139 | end 140 | 141 | it 'on token expired' do 142 | expect(provider).to receive(:api_call).with(*expected_args).and_return double(:code => '401') 143 | expect(provider).to receive(:headers_with_token).with({ :new_token => true}) 144 | 145 | provider.instance_eval { get_headers } 146 | end 147 | end 148 | 149 | it '#api_call should send request' do 150 | expected_args = [ "foo_method", "foo_path", "\"foo\":\"data\"", {"foo"=>"headers"} ] 151 | expect(provider.instance_variable_get(:@http)).to receive(:send_request).with(*expected_args) 152 | 153 | provider.instance_eval do 154 | api_call('foo_method', 'foo_path', '"foo":"data"', { 'foo' => 'headers'}) 155 | end 156 | end 157 | 158 | describe '#deploy_app' do 159 | it 'on api success' do 160 | expect(provider).to receive(:api_call).with('PUT', '/app/foo_app/deployment/default', '{"version":-1}').and_return double(:code => '200') 161 | provider.instance_eval { deploy_app } 162 | end 163 | 164 | it 'on api failure' do 165 | expect(provider).to receive(:api_call).with('PUT', '/app/foo_app/deployment/default', '{"version":-1}').and_return double(:code => '410') 166 | expect do 167 | provider.instance_eval { deploy_app } 168 | end.to raise_error(DPL::Error) 169 | end 170 | end 171 | 172 | describe '#user' do 173 | it 'on api success' do 174 | expect(provider).to receive(:api_call).with('GET', '/user/').and_return double( 175 | :code => '200', 176 | :body => '["foo_user"]' 177 | ) 178 | 179 | expect(provider.instance_eval { user }).to eq('foo_user') 180 | end 181 | 182 | it 'on api failure' do 183 | expect(provider).to receive(:api_call).with('GET', '/user/').and_return double(:code => '410') 184 | 185 | expect do 186 | provider.instance_eval { user } 187 | end.to raise_error(DPL::Error) 188 | end 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /lib/dpl/provider.rb: -------------------------------------------------------------------------------- 1 | require 'dpl/error' 2 | require 'dpl/version' 3 | require 'fileutils' 4 | 5 | module DPL 6 | class Provider 7 | include FileUtils 8 | 9 | autoload :Heroku, 'dpl/provider/heroku' 10 | autoload :Appfog, 'dpl/provider/appfog' 11 | autoload :EngineYard, 'dpl/provider/engine_yard' 12 | autoload :DotCloud, 'dpl/provider/dot_cloud' 13 | autoload :Nodejitsu, 'dpl/provider/nodejitsu' 14 | autoload :Openshift, 'dpl/provider/openshift' 15 | autoload :RubyGems, 'dpl/provider/rubygems' 16 | autoload :NPM, 'dpl/provider/npm' 17 | autoload :S3, 'dpl/provider/s3' 18 | autoload :CloudControl, 'dpl/provider/cloudcontrol' 19 | autoload :CloudFoundry, 'dpl/provider/cloud_foundry' 20 | autoload :CodeDeploy, 'dpl/provider/code_deploy' 21 | autoload :PyPI, 'dpl/provider/pypi' 22 | autoload :Divshot, 'dpl/provider/divshot' 23 | autoload :CloudFiles, 'dpl/provider/cloud_files' 24 | autoload :OpsWorks, 'dpl/provider/ops_works' 25 | autoload :Modulus, 'dpl/provider/modulus' 26 | autoload :Releases, 'dpl/provider/releases' 27 | autoload :Cloud66, 'dpl/provider/cloud66' 28 | autoload :Ninefold, 'dpl/provider/ninefold' 29 | autoload :Hackage, 'dpl/provider/hackage' 30 | autoload :Deis, 'dpl/provider/deis' 31 | autoload :GCS, 'dpl/provider/gcs' 32 | autoload :GAE, 'dpl/provider/gae' 33 | autoload :BitBalloon, 'dpl/provider/bitballoon' 34 | autoload :Biicode, 'dpl/provider/biicode' 35 | autoload :ElasticBeanstalk, 'dpl/provider/elastic_beanstalk' 36 | autoload :PuppetForge, 'dpl/provider/puppet_forge' 37 | autoload :Packagecloud, 'dpl/provider/packagecloud' 38 | autoload :ChefSupermarket, 'dpl/provider/chef_supermarket' 39 | 40 | 41 | def self.new(context, options) 42 | return super if self < Provider 43 | 44 | context.fold("Installing deploy dependencies") do 45 | name = super.option(:provider).to_s.downcase.gsub(/[^a-z0-9]/, '') 46 | raise Error, 'could not find provider %p' % options[:provider] unless name = constants.detect { |c| c.to_s.downcase == name } 47 | provider = const_get(name).new(context, options) 48 | provider.install_deploy_dependencies if provider.respond_to?(:install_deploy_dependencies) 49 | provider 50 | end 51 | end 52 | 53 | def self.experimental(name) 54 | puts "", "!!! #{name} support is experimental !!!", "" 55 | end 56 | 57 | def self.requires(name, options = {}) 58 | version = options[:version] || '> 0' 59 | load = options[:load] || name 60 | gem(name, version) 61 | rescue LoadError 62 | context.shell("gem install %s -v %p --no-ri --no-rdoc #{'--pre' if options[:pre]}" % [name, version], retry: true) 63 | Gem.clear_paths 64 | ensure 65 | require load 66 | end 67 | 68 | def self.context 69 | self 70 | end 71 | 72 | def self.shell(command, options = {}) 73 | system(command) 74 | end 75 | 76 | def self.apt_get(name, command = name) 77 | context.shell("sudo apt-get -qq install #{name}", retry: true) if `which #{command}`.chop.empty? 78 | end 79 | 80 | def self.pip(name, command = name, version = nil) 81 | if version 82 | puts "sudo pip install #{name}==#{version}" 83 | context.shell("sudo pip uninstall -y #{name}") unless `which #{command}`.chop.empty? 84 | context.shell("sudo pip install #{name}==#{version}", retry: true) 85 | else 86 | puts "sudo pip install #{name}" 87 | context.shell("sudo pip install #{name}", retry: true) if `which #{command}`.chop.empty? 88 | end 89 | end 90 | 91 | def self.npm_g(name, command = name) 92 | context.shell("npm install -g #{name}", retry: true) if `which #{command}`.chop.empty? 93 | end 94 | 95 | attr_reader :context, :options 96 | 97 | def initialize(context, options) 98 | @context, @options = context, options 99 | context.env['GIT_HTTP_USER_AGENT'] = user_agent(git: `git --version`[/[\d\.]+/]) 100 | end 101 | 102 | def user_agent(*strings) 103 | strings.unshift "dpl/#{DPL::VERSION}" 104 | strings.unshift "travis/0.1.0" if context.env['TRAVIS'] 105 | strings = strings.flat_map { |e| Hash === e ? e.map { |k,v| "#{k}/#{v}" } : e } 106 | strings.join(" ").gsub(/\s+/, " ").strip 107 | end 108 | 109 | def option(name, *alternatives) 110 | options.fetch(name) do 111 | alternatives.any? ? option(*alternatives) : raise(Error, "missing #{name}") 112 | end 113 | end 114 | 115 | def deploy 116 | setup_git_credentials 117 | rm_rf ".dpl" 118 | mkdir_p ".dpl" 119 | 120 | context.fold("Preparing deploy") do 121 | check_auth 122 | check_app 123 | 124 | if needs_key? 125 | create_key(".dpl/id_rsa") 126 | setup_key(".dpl/id_rsa.pub") 127 | setup_git_ssh(".dpl/git-ssh", ".dpl/id_rsa") 128 | end 129 | 130 | cleanup 131 | end 132 | 133 | context.fold("Deploying application") { push_app } 134 | 135 | Array(options[:run]).each do |command| 136 | if command == 'restart' 137 | context.fold("Restarting application") { restart } 138 | else 139 | context.fold("Running %p" % command) { run(command) } 140 | end 141 | end 142 | ensure 143 | if needs_key? 144 | remove_key rescue nil 145 | end 146 | uncleanup 147 | end 148 | 149 | def sha 150 | @sha ||= context.env['TRAVIS_COMMIT'] || `git rev-parse HEAD`.strip 151 | end 152 | 153 | def commit_msg 154 | @commit_msg ||= %x{git log #{sha} -n 1 --pretty=%B}.strip 155 | end 156 | 157 | def cleanup 158 | return if options[:skip_cleanup] 159 | context.shell "mv .dpl ~/dpl" 160 | context.shell "git stash --all" 161 | context.shell "mv ~/dpl .dpl" 162 | end 163 | 164 | def uncleanup 165 | return if options[:skip_cleanup] 166 | context.shell "git stash pop" 167 | end 168 | 169 | def needs_key? 170 | true 171 | end 172 | 173 | def check_app 174 | end 175 | 176 | def create_key(file) 177 | context.shell "ssh-keygen -t rsa -N \"\" -C #{option(:key_name)} -f #{file}" 178 | end 179 | 180 | def setup_git_credentials 181 | context.shell "git config user.email >/dev/null 2>/dev/null || git config user.email `whoami`@localhost" 182 | context.shell "git config user.name >/dev/null 2>/dev/null || git config user.name `whoami`@localhost" 183 | end 184 | 185 | def setup_git_ssh(path, key_path) 186 | key_path = File.expand_path(key_path) 187 | path = File.expand_path(path) 188 | 189 | File.open(path, 'w') do |file| 190 | file.write "#!/bin/sh\n" 191 | file.write "exec ssh -o StrictHostKeychecking=no -o CheckHostIP=no -o UserKnownHostsFile=/dev/null -i #{key_path} -- \"$@\"\n" 192 | end 193 | 194 | chmod(0740, path) 195 | context.env['GIT_SSH'] = path 196 | end 197 | 198 | def detect_encoding? 199 | options[:detect_encoding] 200 | end 201 | 202 | def encoding_for(path) 203 | file_cmd_output = `file #{path}` 204 | case file_cmd_output 205 | when /gzip compressed/ 206 | 'gzip' 207 | when /compress'd/ 208 | 'compress' 209 | end 210 | end 211 | 212 | def log(message) 213 | $stderr.puts(message) 214 | end 215 | 216 | def warn(message) 217 | log "\e[31;1m#{message}\e[0m" 218 | end 219 | 220 | def run(command) 221 | error "running commands not supported" 222 | end 223 | 224 | def error(message) 225 | raise Error, message 226 | end 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /spec/provider/releases_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dpl/provider/releases' 3 | require 'octokit' 4 | 5 | describe DPL::Provider::Releases do 6 | subject :provider do 7 | described_class.new(DummyContext.new, :api_key => '0123445789qwertyuiop0123445789qwertyuiop', :file => 'blah.txt') 8 | end 9 | 10 | describe "#travis_tag" do 11 | example "When $TRAVIS_TAG is nil" do 12 | provider.context.env['TRAVIS_TAG'] = nil 13 | 14 | expect(provider.travis_tag).to eq(nil) 15 | end 16 | 17 | example "When $TRAVIS_TAG if set but empty" do 18 | provider.context.env['TRAVIS_TAG'] = nil 19 | 20 | expect(provider.travis_tag).to eq(nil) 21 | end 22 | 23 | example "When $TRAVIS_TAG if set" do 24 | provider.context.env['TRAVIS_TAG'] = "foo" 25 | 26 | expect(provider.travis_tag).to eq("foo") 27 | end 28 | end 29 | 30 | describe "#api" do 31 | example "With API key" do 32 | api = double(:api) 33 | expect(::Octokit::Client).to receive(:new).with(:access_token => '0123445789qwertyuiop0123445789qwertyuiop').and_return(api) 34 | expect(provider.api).to eq(api) 35 | end 36 | 37 | example "With username and password" do 38 | api = double(:api) 39 | provider.options.update(:user => 'foo') 40 | provider.options.update(:password => 'bar') 41 | 42 | expect(::Octokit::Client).to receive(:new).with(:login => 'foo', :password => 'bar').and_return(api) 43 | expect(provider.api).to eq(api) 44 | end 45 | end 46 | 47 | describe "#releases" do 48 | example "With ENV Slug" do 49 | allow(provider).to receive(:slug).and_return("foo/bar") 50 | 51 | expect(provider.api).to receive(:releases).with("foo/bar") 52 | provider.releases 53 | end 54 | 55 | example "With repo option" do 56 | provider.options.update(:repo => 'bar/foo') 57 | 58 | expect(provider.api).to receive(:releases).with('bar/foo') 59 | provider.releases 60 | end 61 | end 62 | 63 | describe "#files" do 64 | example "without file globbing and a single file" do 65 | expect(provider.files).to eq(['blah.txt']) 66 | end 67 | 68 | example "without file globbing and multiple files" do 69 | provider.options.update(:file => ['foo.txt', 'bar.txt']) 70 | expect(provider.files).to eq(['foo.txt', 'bar.txt']) 71 | end 72 | 73 | example "with file globbing and a single glob" do 74 | provider.options.update(:file_glob => true) 75 | provider.options.update(:file => 'bl*.txt') 76 | expect(::Dir).to receive(:glob).with('bl*.txt').and_return(['blah.txt']) 77 | expect(provider.files).to eq(['blah.txt']) 78 | end 79 | 80 | example "with file globbing and multiple globs" do 81 | provider.options.update(:file_glob => true) 82 | provider.options.update(:file => ['f*.txt', 'b*.txt']) 83 | expect(::Dir).to receive(:glob).with('f*.txt').and_return(['foo.txt']) 84 | expect(::Dir).to receive(:glob).with('b*.txt').and_return(['bar.txt']) 85 | expect(provider.files).to eq(['foo.txt', 'bar.txt']) 86 | end 87 | end 88 | 89 | describe "#needs_key?" do 90 | example do 91 | expect(provider.needs_key?).to eq(false) 92 | end 93 | end 94 | 95 | describe "#check_app" do 96 | example "Without $TRAVIS_TAG" do 97 | allow(provider).to receive(:travis_tag).and_return(nil) 98 | allow(provider).to receive(:slug).and_return("foo/bar") 99 | allow(provider).to receive(:get_tag).and_return("foo") 100 | 101 | expect(provider.context).to receive(:shell).with("git fetch --tags") 102 | expect(provider).to receive(:log).with("Deploying to repo: foo/bar") 103 | expect(provider).to receive(:log).with("Current tag is: foo") 104 | 105 | provider.check_app 106 | end 107 | 108 | example "With $TRAVIS_TAG" do 109 | allow(provider).to receive(:travis_tag).and_return("bar") 110 | allow(provider).to receive(:slug).and_return("foo/bar") 111 | 112 | expect(provider.context).not_to receive(:shell).with("git fetch --tags") 113 | expect(provider).to receive(:log).with("Deploying to repo: foo/bar") 114 | expect(provider).to receive(:log).with("Current tag is: bar") 115 | 116 | provider.check_app 117 | end 118 | end 119 | 120 | describe "#get_tag" do 121 | example "Without $TRAVIS_TAG" do 122 | allow(provider).to receive(:travis_tag).and_return(nil) 123 | allow(provider).to receive(:`).and_return("bar") 124 | 125 | expect(provider.get_tag).to eq("bar") 126 | end 127 | 128 | example "With $TRAVIS_TAG" do 129 | allow(provider).to receive(:travis_tag).and_return("foo") 130 | 131 | expect(provider.get_tag).to eq("foo") 132 | end 133 | end 134 | 135 | describe "#check_auth" do 136 | example "With proper permissions" do 137 | allow_message_expectations_on_nil 138 | allow(provider).to receive(:user) 139 | allow(provider).to receive(:setup_auth) 140 | expect(provider.api).to receive(:scopes).and_return(["public_repo"]) 141 | expect(provider.user).to receive(:name).and_return("foo") 142 | expect(provider).to receive(:log).with("Logged in as foo") 143 | provider.check_auth 144 | end 145 | 146 | example "With improper permissions" do 147 | allow_message_expectations_on_nil 148 | allow(provider).to receive(:user) 149 | allow(provider).to receive(:setup_auth) 150 | expect(provider.api).to receive(:scopes).exactly(2).times.and_return([]) 151 | expect { provider.check_auth }.to raise_error(DPL::Error) 152 | end 153 | end 154 | 155 | describe "#push_app" do 156 | example "When Release Exists but has no Files" do 157 | allow_message_expectations_on_nil 158 | 159 | provider.options.update(:file => ["test/foo.bar", "bar.txt"]) 160 | 161 | allow(provider).to receive(:releases).and_return([""]) 162 | allow(provider).to receive(:get_tag).and_return("v0.0.0") 163 | 164 | provider.releases.map do |release| 165 | allow(release).to receive(:tag_name).and_return("v0.0.0") 166 | allow(release).to receive(:rels).and_return({:self => nil}) 167 | allow(release.rels[:self]).to receive(:href) 168 | end 169 | 170 | allow(provider.api).to receive(:release) 171 | allow(provider.api.release).to receive(:rels).and_return({:assets => nil}) 172 | allow(provider.api.release.rels[:assets]).to receive(:get).and_return({:data => [""]}) 173 | allow(provider.api.release.rels[:assets].get).to receive(:data).and_return([]) 174 | 175 | expect(provider.api).to receive(:upload_asset).with(anything, "test/foo.bar", {:name=>"foo.bar", :content_type=>"application/octet-stream"}) 176 | expect(provider.api).to receive(:upload_asset).with(anything, "bar.txt", {:name=>"bar.txt", :content_type=>"text/plain"}) 177 | 178 | provider.push_app 179 | end 180 | 181 | example "When Release Exists and has Files" do 182 | allow_message_expectations_on_nil 183 | 184 | provider.options.update(:file => ["test/foo.bar", "bar.txt"]) 185 | 186 | allow(provider).to receive(:releases).and_return([""]) 187 | allow(provider).to receive(:get_tag).and_return("v0.0.0") 188 | 189 | provider.releases.map do |release| 190 | allow(release).to receive(:tag_name).and_return("v0.0.0") 191 | allow(release).to receive(:rels).and_return({:self => nil}) 192 | allow(release.rels[:self]).to receive(:href) 193 | end 194 | 195 | allow(provider.api).to receive(:release) 196 | allow(provider.api.release).to receive(:rels).and_return({:assets => nil}) 197 | allow(provider.api.release.rels[:assets]).to receive(:get).and_return({:data => [""]}) 198 | allow(provider.api.release.rels[:assets].get).to receive(:data).and_return([double(:name => "foo.bar"), double(:name => "foo.foo")]) 199 | 200 | expect(provider.api).to receive(:upload_asset).with(anything, "bar.txt", {:name=>"bar.txt", :content_type=>"text/plain"}) 201 | expect(provider).to receive(:log).with("foo.bar already exists, skipping.") 202 | 203 | provider.push_app 204 | end 205 | 206 | example "When Release Doesn't Exist" do 207 | allow_message_expectations_on_nil 208 | 209 | provider.options.update(:file => ["test/foo.bar", "bar.txt"]) 210 | 211 | allow(provider).to receive(:releases).and_return([""]) 212 | 213 | provider.releases.map do |release| 214 | allow(release).to receive(:tag_name).and_return("foo") 215 | allow(release).to receive(:rels).and_return({:self => nil}) 216 | allow(release.rels[:self]).to receive(:href) 217 | end 218 | 219 | allow(provider.api).to receive(:create_release) 220 | allow(provider.api.create_release).to receive(:rels).and_return({:self => nil}) 221 | allow(provider.api.create_release.rels[:slef]).to receive(:href) 222 | 223 | allow(provider.api).to receive(:release) 224 | allow(provider.api.release).to receive(:rels).and_return({:assets => nil}) 225 | allow(provider.api.release.rels[:assets]).to receive(:get).and_return({:data => nil}) 226 | allow(provider.api.release.rels[:assets].get).to receive(:data).and_return([]) 227 | 228 | expect(provider.api).to receive(:upload_asset).with(anything, "test/foo.bar", {:name=>"foo.bar", :content_type=>"application/octet-stream"}) 229 | expect(provider.api).to receive(:upload_asset).with(anything, "bar.txt", {:name=>"bar.txt", :content_type=>"text/plain"}) 230 | 231 | provider.push_app 232 | end 233 | 234 | example "With Release Number" do 235 | allow_message_expectations_on_nil 236 | 237 | provider.options.update(:file => ["bar.txt"]) 238 | provider.options.update(:release_number => "1234") 239 | 240 | allow(provider).to receive(:slug).and_return("foo/bar") 241 | 242 | allow(provider.api).to receive(:release) 243 | allow(provider.api.release).to receive(:rels).and_return({:assets => nil}) 244 | allow(provider.api.release.rels[:assets]).to receive(:get).and_return({:data => nil}) 245 | allow(provider.api.release.rels[:assets].get).to receive(:data).and_return([]) 246 | 247 | expect(provider.api).to receive(:upload_asset).with("https://api.github.com/repos/foo/bar/releases/1234", "bar.txt", {:name=>"bar.txt", :content_type=>"text/plain"}) 248 | 249 | provider.push_app 250 | end 251 | end 252 | end 253 | -------------------------------------------------------------------------------- /spec/provider/code_deploy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'aws-sdk' 3 | require 'dpl/error' 4 | require 'dpl/provider' 5 | require 'dpl/provider/code_deploy' 6 | 7 | describe DPL::Provider::CodeDeploy do 8 | 9 | subject :provider do 10 | described_class.new(DummyContext.new, :access_key_id => 'qwertyuiopasdfghjklz', :secret_access_key => 'qwertyuiopasdfghjklzqwertyuiopasdfghjklz') 11 | end 12 | 13 | describe '#code_deploy_options' do 14 | context 'without region' do 15 | example do 16 | options = provider.code_deploy_options 17 | expect(options[:region]).to eq('us-east-1') 18 | end 19 | end 20 | 21 | context 'with region' do 22 | example do 23 | region = 'us-west-1' 24 | provider.options.update(:region => region) 25 | options = provider.code_deploy_options 26 | expect(options[:region]).to eq(region) 27 | end 28 | end 29 | 30 | context 'without endpoint' do 31 | example do 32 | options = provider.code_deploy_options 33 | expect(options[:endpoint]).to eq(nil) 34 | end 35 | end 36 | 37 | context 'with endpoint' do 38 | example do 39 | endpoint = 's3test.com.s3-website-us-west-2.amazonaws.com' 40 | provider.options.update(:endpoint => endpoint) 41 | options = provider.code_deploy_options 42 | expect(options[:endpoint]).to eq(endpoint) 43 | end 44 | end 45 | end 46 | end 47 | 48 | describe DPL::Provider::CodeDeploy do 49 | access_key_id = 'someaccesskey' 50 | secret_access_key = 'somesecretaccesskey' 51 | application = 'app' 52 | deployment_group = 'group' 53 | description = 'description' 54 | revision = '23jkljkl' 55 | client_options = { 56 | :stub_responses => true, 57 | :region => 'us-east-1', 58 | :credentials => ::Aws::Credentials.new(access_key_id, secret_access_key), 59 | :endpoint => 'https://codedeploy.us-east-1.amazonaws.com' 60 | } 61 | 62 | subject :provider do 63 | described_class.new(DummyContext.new, { 64 | :access_key_id => access_key_id, 65 | :secret_access_key => secret_access_key 66 | }) 67 | end 68 | 69 | before :each do 70 | provider.stub(:code_deploy_options).and_return(client_options) 71 | end 72 | 73 | describe '#code_deploy' do 74 | example do 75 | expect(::Aws::CodeDeploy::Client).to receive(:new).with(client_options).once 76 | provider.code_deploy 77 | end 78 | end 79 | 80 | describe '#needs_key?' do 81 | example do 82 | expect(provider.needs_key?).to eq(false) 83 | end 84 | end 85 | 86 | describe '#revision' do 87 | expected_s3_revision = { 88 | revision_type: 'S3', 89 | s3_location: { 90 | bucket: 'bucket', 91 | bundle_type: 'tar', 92 | key: 'key' 93 | } 94 | } 95 | 96 | expected_github_revision = { 97 | revision_type: 'GitHub', 98 | git_hub_location: { 99 | commit_id: '2lk3j4k2j3k4j23k4j', 100 | repository: 'travis-ci/dpl' 101 | } 102 | } 103 | 104 | before(:each) do 105 | provider.stub(:s3_revision).and_return(expected_s3_revision) 106 | provider.stub(:github_revision).and_return(expected_github_revision) 107 | end 108 | 109 | context 'when s3' do 110 | before do 111 | provider.options.update(:revision_type => :s3) 112 | end 113 | 114 | example do 115 | expect(provider.revision).to eq(expected_s3_revision) 116 | end 117 | end 118 | 119 | context 'when github' do 120 | before do 121 | provider.options.update(:revision_type => :github) 122 | end 123 | 124 | example do 125 | expect(provider.revision).to eq(expected_github_revision) 126 | end 127 | end 128 | 129 | context 'when not specified' do 130 | before do 131 | provider.options.update(:bucket => 'bucket') 132 | end 133 | 134 | example do 135 | expect(provider.revision).to eq(expected_s3_revision) 136 | end 137 | end 138 | 139 | context 'when revision and bucket are not specified' do 140 | example do 141 | expect(provider.revision).to eq(expected_github_revision) 142 | end 143 | end 144 | 145 | context 'when not a known revision type' do 146 | type = :bad 147 | 148 | before do 149 | provider.options.update(:revision_type => type) 150 | end 151 | 152 | example do 153 | expect(provider).to receive(:error).with(/unknown revision type :#{type}/) 154 | provider.revision 155 | end 156 | end 157 | end 158 | 159 | describe '#s3_revision' do 160 | bucket = 'bucket' 161 | bundle_type = 'tar' 162 | key = "/some/key.#{bundle_type}" 163 | 164 | before(:each) do 165 | expect(provider).to receive(:option).with(:bucket).and_return(bucket) 166 | expect(provider).to receive(:bundle_type).and_return(bundle_type) 167 | expect(provider).to receive(:s3_key).and_return(key) 168 | end 169 | 170 | example do 171 | expect(provider.s3_revision).to eq({ 172 | revision_type: 'S3', 173 | s3_location: { 174 | bucket: bucket, 175 | bundle_type: bundle_type, 176 | key: key 177 | } 178 | }) 179 | end 180 | end 181 | 182 | describe '#github_revision' do 183 | commit_id = '432s35s3' 184 | repository = 'git@github.com/org/repo.git' 185 | 186 | context 'with options set' do 187 | before(:each) do 188 | expect(provider.options).to receive(:[]).with(:commit_id).and_return(commit_id) 189 | expect(provider.options).to receive(:[]).with(:repository).and_return(repository) 190 | end 191 | 192 | example do 193 | expect(provider.github_revision).to eq({ 194 | revision_type: 'GitHub', 195 | git_hub_location: { 196 | commit_id: commit_id, 197 | repository: repository 198 | } 199 | }) 200 | end 201 | end 202 | 203 | context 'with environment variables' do 204 | before(:each) do 205 | expect(provider.options).to receive(:[]).with(:commit_id).and_return(nil) 206 | expect(provider.options).to receive(:[]).with(:repository).and_return(nil) 207 | expect(provider.context.env).to receive(:[]).with('TRAVIS_COMMIT').and_return(commit_id) 208 | expect(provider.context.env).to receive(:[]).with('TRAVIS_REPO_SLUG').and_return(repository) 209 | end 210 | 211 | example do 212 | expect(provider.github_revision).to eq({ 213 | revision_type: 'GitHub', 214 | git_hub_location: { 215 | commit_id: commit_id, 216 | repository: repository 217 | } 218 | }) 219 | end 220 | end 221 | 222 | context 'without required options' do 223 | before(:each) do 224 | expect(provider.options).to receive(:[]).with(:commit_id).and_return(nil) 225 | provider.options.stub(:[]).with(:repository) { nil } 226 | expect(provider.context.env).to receive(:[]).with('TRAVIS_COMMIT').and_return(nil) 227 | expect(provider.context.env).to receive(:[]).with('TRAVIS_REPO_SLUG').and_return(nil) 228 | end 229 | 230 | example do 231 | expect{provider.github_revision}.to raise_error(DPL::Error) 232 | end 233 | end 234 | end 235 | 236 | describe '#push_app' do 237 | before(:each) do 238 | old_options = provider.options 239 | provider.stub(:options) {old_options.merge({ 240 | :application_name => application, 241 | :deployment_group_name => deployment_group, 242 | :description => description, 243 | :repository => 'git@github.com:travis-ci/dpl.git' 244 | })} 245 | end 246 | 247 | context 'without an error' do 248 | deployment_id = 'some-deployment-id' 249 | 250 | before do 251 | provider.code_deploy.stub_responses(:create_deployment, :deployment_id => deployment_id) 252 | end 253 | 254 | example do 255 | expect(provider).to receive(:log).with(/Triggered deployment \"#{deployment_id}\"\./) 256 | provider.push_app 257 | end 258 | end 259 | 260 | context 'with an error' do 261 | before do 262 | provider.code_deploy.stub_responses(:create_deployment, 'DeploymentLimitExceededException') 263 | end 264 | 265 | example do 266 | expect(provider).to receive(:error).once 267 | provider.push_app 268 | end 269 | end 270 | end 271 | 272 | describe '#bundle_type' do 273 | context 'with s3_key' do 274 | format = 'zip' 275 | s3_key = "/some/key/name.#{format}" 276 | 277 | before(:each) do 278 | expect(provider).to receive(:s3_key).and_return(s3_key) 279 | end 280 | 281 | example do 282 | expect(provider.bundle_type).to eq(format) 283 | end 284 | end 285 | 286 | context 'without s3_key' do 287 | bundle_type = 'tar' 288 | 289 | before do 290 | expect(provider).to receive(:s3_key).and_return('') 291 | expect(provider).to receive(:option).with(:bundle_type).and_return(bundle_type) 292 | end 293 | 294 | example do 295 | expect(provider.bundle_type).to eq(bundle_type) 296 | end 297 | end 298 | end 299 | 300 | describe '#s3_key' do 301 | key = '/some/key/name.zip' 302 | 303 | context 'with key option' do 304 | before do 305 | expect(provider.options).to receive(:[]).with(:key).and_return(key) 306 | end 307 | 308 | example do 309 | expect(provider.s3_key).to eq(key) 310 | end 311 | end 312 | 313 | context 'with s3_key option' do 314 | before do 315 | expect(provider).to receive(:option).with(:s3_key).and_return(key) 316 | end 317 | 318 | example do 319 | expect(provider.s3_key).to eq(key) 320 | end 321 | end 322 | end 323 | 324 | describe '#default_description' do 325 | build_number = 2 326 | 327 | before do 328 | provider.context.env.stub(:[]).with('TRAVIS_BUILD_NUMBER').and_return(build_number) 329 | end 330 | 331 | example do 332 | expect(provider.default_description).to eq("Deploy build #{build_number} via Travis CI") 333 | end 334 | end 335 | 336 | describe '#check_auth' do 337 | example do 338 | expect(provider).to receive(:log).with("Logging in with Access Key: #{access_key_id[-4..-1].rjust(20, '*')}") 339 | provider.check_auth 340 | end 341 | end 342 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dpl [![Build Status](https://travis-ci.org/travis-ci/dpl.svg?branch=master)](https://travis-ci.org/travis-ci/dpl) [![Code Climate](https://codeclimate.com/github/travis-ci/dpl.png)](https://codeclimate.com/github/travis-ci/dpl) [![Gem Version](https://badge.fury.io/rb/dpl.png)](http://badge.fury.io/rb/dpl) 2 | Dpl (dee-pee-ell) is a deploy tool made for continuous deployment. Developed and used by Travis CI. 3 | 4 | ## Supported Providers: 5 | Dpl supports the following providers: 6 | 7 | * [AppFog](#appfog) 8 | * [Biicode](#biicode) 9 | * [BitBalloon](#bitballoon) 10 | * [Cloud 66](#cloud-66) 11 | * [Cloud Foundry](#cloud-foundry) 12 | * [cloudControl](#cloudcontrol) 13 | * [dotCloud (experimental)](#dotcloud) 14 | * [Engine Yard](#engine-yard) 15 | * [Heroku](#heroku) 16 | * [Nodejitsu](#nodejitsu) 17 | * [NPM](#npm) 18 | * [OpenShift](#openshift) 19 | * [PyPi](#pypi) 20 | * [RubyGems](#rubygems) 21 | * [S3](#s3) 22 | * [Divshot.io](#divshotio) 23 | * [Rackspace Cloud Files](#rackspace-cloud-files) 24 | * [AWS OpsWorks](#opsworks) 25 | * [Modulus](#modulus) 26 | * [Github Releases](#github-releases) 27 | * [Ninefold](#ninefold) 28 | * [Hackage](#hackage) 29 | * [Deis](#deis) 30 | * [Google Cloud Storage](#google-cloud-storage) 31 | * [Elastic Beanstalk](#elastic-beanstalk) 32 | * [Puppet Forge](#puppet-forge) 33 | * [packagecloud](#packagecloud) 34 | * [Chef Supermarket](#chef-supermarket) 35 | 36 | ## Installation: 37 | 38 | Dpl is published to rubygems. 39 | 40 | * Dpl requires ruby with a version greater than 1.8.7 41 | * To install: `gem install dpl` 42 | 43 | ## Usage: 44 | 45 | ###Security Warning: 46 | 47 | Running dpl in a terminal that saves history is insecure as your password/api key will be saved as plain text by it. 48 | 49 | ###Global Flags 50 | * `--provider=` sets the provider you want to deploy to. Every provider has slightly different flags, which are documented in the section about your provider following. 51 | * Dpl will deploy by default from the latest commit. Use the `--skip_cleanup` flag to deploy from the current file state. Note that many providers deploy by git and could ignore this option. 52 | 53 | 54 | ### Heroku: 55 | 56 | #### Options: 57 | * **api-key**: Heroku API Key 58 | * **strategy[git/anvil]**: Deployment strategy for Dpl. Defaults to anvil. 59 | * **app**: Heroku app name. Defaults to the name of your git repo. 60 | * **username**: heroku username. Not necessary if api-key is used. Requires git strategy. 61 | * **password**: heroku password. Not necessary if api-key is used. Requires git strategy. 62 | 63 | #### Git vs Anvil Deploy: 64 | * Anvil will run the [buildpack](https://devcenter.heroku.com/articles/buildpacks) compilation step on the Travis CI VM, whereas the Git strategy will run it on a Heroku dyno, which provides the same environment the application will then run under and might be slightly faster. 65 | * The Git strategy allows using *user* and *password* instead of *api-key*. 66 | * When using Git, Heroku might send you an email for every deploy, as it adds a temporary SSH key to your account. 67 | 68 | As a rule of thumb, you should switch to the Git strategy if you run into issues with Anvil or if you're using the [user-env-compile](https://devcenter.heroku.com/articles/labs-user-env-compile) plugin. 69 | 70 | #### Examples: 71 | 72 | dpl --provider=heroku --api-key=`heroku auth:token` 73 | dpl --provider=heroku --strategy=git --username= --password= --app= 74 | 75 | 76 | 77 | 78 | ### Nodejitsu: 79 | 80 | #### Options: 81 | 82 | * **username**: Nodejitsu Username 83 | * **api-key**: Nodejitsu API Key 84 | 85 | #### Examples: 86 | dpl --provider=nodejitsu --username= --api-key= 87 | 88 | 89 | ### Modulus 90 | 91 | #### Options: 92 | 93 | * **api-key** Modulus Authentication Token 94 | * **project-name** Modulus Project to Deploy 95 | 96 | #### Example: 97 | dpl --provider=modulus --api-key= --project-name= 98 | 99 | 100 | ### Engine Yard: 101 | 102 | #### Options: 103 | 104 | * **api-key**: Engine Yard Api Key 105 | * **username**: Engine Yard username. Not necessary if api-key is used. Requires git strategy. 106 | * **password**: Engine Yard password. Not necessary if api-key is used. 107 | * **app**: Engine Yard Application name. Defaults to git repo's name. 108 | * **environment**: Engine Yard Application Environment. Optional. 109 | * **migrate**: Engine Yard migration commands. Optional. 110 | 111 | #### Examples: 112 | 113 | dpl --provider=engineyard --api-key= 114 | dpl --provider=engineyard --username= --password= --environment=staging 115 | dpl --provider=engineyard --api-key= --app= --migrate=`rake db:migrate` 116 | 117 | ### OpenShift: 118 | 119 | #### Options: 120 | 121 | * **user**: Openshift Username. 122 | * **password**: Openshift Password. 123 | * **domain**: Openshift Application Domain. 124 | * **app**: Openshift Application. Defaults to git repo's name. 125 | 126 | ####Examples: 127 | 128 | dpl --provider=openshift --user= --password= --domain= 129 | dpl --provider=openshift --user= --password= --domain= --app= 130 | 131 | ### cloudControl: 132 | 133 | #### Options: 134 | 135 | * **email**: cloudControl email. 136 | * **password**: cloudControl password. 137 | * **deployment**: cloudControl Deployment. Follows the format "APP_NAME/DEP_NAME". 138 | 139 | #### Examples: 140 | 141 | dpl --provider=cloudcontrol --email= --password --deployment=`APP_NAME/DEP_NAME` 142 | 143 | ### RubyGems: 144 | 145 | #### Options: 146 | 147 | * **api-key**: Rubygems Api Key. 148 | 149 | #### Examples: 150 | 151 | dpl --provider=rubygems --api-key= 152 | 153 | ### PyPI: 154 | 155 | #### Options: 156 | 157 | * **user**: PyPI Username. 158 | * **password**: PyPI Password. 159 | * **server**: Optional. Only required if you want to release to a different index. Follows the form of "https://mypackageindex.com/index". 160 | * **distributions**: A space-separated list of distributions to be uploaded to PyPI. Defaults to 'sdist'. 161 | * **docs_dir**: A path to the directory to upload documentation from. Defaults to 'build/docs' 162 | 163 | #### Examples: 164 | 165 | dpl --provider=pypi --user= --password= 166 | dpl --provider=pypi --user= --password= --server='https://mypackageindex.com/index' --distributions='sdist bdist_wheel' 167 | 168 | ### NPM: 169 | 170 | #### Options: 171 | 172 | * **email**: NPM email. 173 | * **api-key**: NPM api key. Can be retrieved from your ~/.npmrc file. 174 | 175 | #### Examples: 176 | 177 | dpl --provider=npm --email= --api-key= 178 | 179 | ### biicode: 180 | 181 | #### Options: 182 | 183 | * **user**: biicode username. 184 | * **password**: biicode password. 185 | 186 | #### Examples: 187 | 188 | dpl --provider=biicode --user= --password= 189 | 190 | 191 | ### S3: 192 | 193 | #### Options: 194 | 195 | * **access-key-id**: AWS Access Key ID. Can be obtained from [here](https://console.aws.amazon.com/iam/home?#security_credential). 196 | * **secret-access-key**: AWS Secret Key. Can be obtained from [here](https://console.aws.amazon.com/iam/home?#security_credential). 197 | * **bucket**: S3 Bucket. 198 | * **region**: S3 Region. Defaults to us-east-1. 199 | * **endpoint**: S3 Endpoint. Defaults to s3.amazonaws.com. 200 | * **upload-dir**: S3 directory to upload to. Defaults to root directory. 201 | * **local-dir**: Local directory to upload from. Can be set from a global perspective (~/travis/build) or relative perspective (build) Defaults to project root. 202 | * **detect-encoding**: Set HTTP header `Content-Encoding` for files compressed with `gzip` and `compress` utilities. Defaults to not set. 203 | * **cache_control**: Set HTTP header `Cache-Control` to suggest that the browser cache the file. Defaults to `no-cache`. Valid options are `no-cache`, `no-store`, `max-age=`,`s-maxage=` `no-transform`, `public`, `private`. 204 | * **expires**: This sets the date and time that the cached object is no longer cacheable. Defaults to not set. The date must be in the format `YYYY-MM-DD HH:MM:SS -ZONE`. 205 | * **acl**: Sets the access control for the uploaded objects. Defaults to `private`. Valid options are `private`, `public_read`, `public_read_write`, `authenticated_read`, `bucket_owner_read`, `bucket_owner_full_control`. 206 | * **dot_match**: When set to `true`, upload files starting a `.`. 207 | * **index_document_suffix**: Set the index document of a S3 website. 208 | 209 | #### File-specific `Cache-Control` and `Expires` headers 210 | 211 | It is possible to set file-specific `Cache-Control` and `Expires` headers using `value: file[, file]` format. 212 | 213 | ##### Example: 214 | 215 | --cache_control="no-cache: index.html" 216 | --expires="\"2012-12-21 00:00:00 -0000\": *.css, *.js" 217 | 218 | #### Examples: 219 | 220 | dpl --provider=s3 --access-key-id= --secret-access-key= --bucket= --acl=public_read 221 | dpl --provider=s3 --access-key-id= --secret-access-key= --bucket= --detect-encoding --cache_control=max-age=99999 --expires="2012-12-21 00:00:00 -0000" 222 | dpl --provider=s3 --access-key-id= --secret-access-key= --bucket= --region=us-west-2 --local-dir= BUILD --upload-dir=BUILDS 223 | 224 | ### OpsWorks: 225 | 226 | #### Options: 227 | 228 | * **access-key-id**: AWS Access Key ID. Can be obtained from [here](https://console.aws.amazon.com/iam/home?#security_credential). 229 | * **secret-access-key**: AWS Secret Key. Can be obtained from [here](https://console.aws.amazon.com/iam/home?#security_credential). 230 | * **app-id**: The app ID. 231 | * **migrate**: Migrate the database. (Default: false) 232 | * **wait-until-deployed**: Wait until the app is deployed and return the deployment status. (Default: false) 233 | #### Examples: 234 | 235 | dpl --provider=opsworks --access-key-id= --secret-access-key= --app-id= --migrate --wait-until-deployed 236 | 237 | 238 | ### Appfog: 239 | 240 | #### Options: 241 | 242 | * **email**: Appfog Email. 243 | * **password**: Appfog Password. 244 | * **app**: Appfog App. Defaults to git repo's name. 245 | 246 | #### Examples: 247 | 248 | dpl --provider=appfog --email= --password= 249 | dpl --provider=appfog --email= --password= --app= 250 | 251 | ### Divshot.io: 252 | 253 | #### Options: 254 | 255 | * **api-key**: Divshot.io API key 256 | * **environment**: Which environment (development, staging, production) to deploy to 257 | 258 | #### Examples: 259 | 260 | dpl --provider=divshot --api-key= --environment= 261 | 262 | ### Cloud Foundry: 263 | 264 | #### Options: 265 | 266 | * **username**: Cloud Foundry username. 267 | * **password**: Cloud Foundry password. 268 | * **organization**: Cloud Foundry target organization. 269 | * **api**: Cloud Foundry api URL 270 | * **space**: Cloud Foundry target space 271 | 272 | #### Examples: 273 | 274 | dpl --provider=cloudfoundry --username= --password= --organization= --api= --space= 275 | 276 | ### dotCloud: 277 | 278 | #### Options: 279 | 280 | * **api_key**: dotCloud api key. 281 | * **app**: dotcloud app. 282 | * **service**: dotcloud service to run commands on. Defaults to 'www'. 283 | 284 | #### Examples: 285 | 286 | dpl --provider=dotcloud --api_key= --app= 287 | dpl --provider=dotcloud --api_key= --app= --service= 288 | 289 | ### Rackspace Cloud Files: 290 | 291 | #### Options: 292 | 293 | * **username**: Rackspace Username. 294 | * **api-key**: Rackspace API Key. 295 | * **region**: Cloud Files Region. The region in which your Cloud Files container exists. 296 | * **container**: Container Name. The container where you would like your files to be uploaded. 297 | * **dot_match**: When set to `true`, upload files starting a `.`. 298 | 299 | #### Examples: 300 | 301 | dpl --provider=cloudfiles --username= --api-key= --region= --container= 302 | 303 | ### GitHub Releases: 304 | 305 | #### Options: 306 | 307 | * **api-key**: GitHub oauth token with `public_repo` or`repo` permission. 308 | * **user**: GitHub username. Not necessary if `api-key` is used. 309 | * **password**: GitHub Password. Not necessary if `api-key` is used. 310 | * **repo**: GitHub Repo. Defaults to git repo's name. 311 | * **file**: File to upload to GitHub Release. 312 | * **file_glob**: If files should be interpreted as globs (\* and \*\* wildcards). Defaults to false. 313 | * **release-number**: Overide automatic release detection, set a release manually. 314 | 315 | #### GitHub Two Factor Authentication 316 | 317 | For accounts using two factor authentication, you have to use an oauth token as a username and password will not work. 318 | 319 | #### Examples: 320 | 321 | dpl --provider=releases --api-key= --file=build.tar.gz 322 | 323 | ### Cloud 66 324 | 325 | #### Options: 326 | 327 | * **redeployment_hook**: The redeployment hook URL. Available from the Information menu within the Cloud 66 portal. 328 | 329 | #### Examples: 330 | 331 | dpl --provider=cloud66 --redeployment_hook= 332 | 333 | ### Ninefold 334 | 335 | #### Options: 336 | 337 | * **auth_token**: Ninefold deploy auth token 338 | * **app_id**: Ninefold deploy app ID 339 | 340 | #### Examples: 341 | 342 | dpl --provider=ninefold --auth_token= --app_id= 343 | 344 | ### Hackage: 345 | 346 | #### Options: 347 | 348 | * **username**: Hackage username. 349 | * **password**: Hackage password. 350 | 351 | #### Examples: 352 | 353 | dpl --provider=hackage --username= --password= 354 | 355 | ### Deis: 356 | 357 | #### Options: 358 | 359 | * **controller**: Deis controller e.g. deis.deisapps.com 360 | * **username**: Deis username 361 | * **password**: Deis password 362 | * **app**: Deis app 363 | * **cli_version**: Install a specific deis cli version 364 | 365 | #### Examples: 366 | 367 | dpl --provider=deis --controller=deis.deisapps.com --username=travis --password=secret --app=example 368 | 369 | ### Google Cloud Storage: 370 | 371 | #### Options: 372 | 373 | * **access-key-id**: GCS Interoperable Access Key ID. Info about Interoperable Access Key from [here](https://developers.google.com/storage/docs/migrating). 374 | * **secret-access-key**: GCS Interoperable Access Secret. 375 | * **bucket**: GCS Bucket. 376 | * **upload-dir**: GCS directory to upload to. Defaults to root directory. 377 | * **local-dir**: Local directory to upload from. Can be set from a global perspective (~/travis/build) or relative perspective (build) Defaults to project root. 378 | * **dot_match**: When set to `true`, upload files starting a `.`. 379 | * **detect-encoding**: Set HTTP header `Content-Encoding` for files compressed with `gzip` and `compress` utilities. Defaults to not set. 380 | * **cache_control**: Set HTTP header `Cache-Control` to suggest that the browser cache the file. Defaults to not set. Info is [here](https://developers.google.com/storage/docs/reference-headers#cachecontrol) 381 | * **acl**: Sets the access control for the uploaded objects. Defaults to not set. Info is [here](https://developers.google.com/storage/docs/reference-headers#xgoogacl) 382 | 383 | #### Examples: 384 | 385 | dpl --provider=gcs --access-key-id= --secret-access-key= --bucket= 386 | dpl --provider=gcs --access-key-id= --secret-access-key= --bucket= --local-dir= BUILD 387 | dpl --provider=gcs --access-key-id= --secret-access-key= --bucket= --acl=public-read 388 | dpl --provider=gcs --access-key-id= --secret-access-key= --bucket= --detect-encoding --cache_control=max-age=99999 389 | dpl --provider=gcs --access-key-id= --secret-access-key= --bucket= --local-dir=BUILD --upload-dir=BUILDS 390 | 391 | ### Elastic Beanstalk: 392 | 393 | #### Options: 394 | 395 | * **access-key-id**: AWS Access Key ID. Can be obtained from [here](https://console.aws.amazon.com/iam/home?#security_credential). 396 | * **secret-access-key**: AWS Secret Key. Can be obtained from [here](https://console.aws.amazon.com/iam/home?#security_credential). 397 | * **region**: AWS Region the Elastic Beanstalk app is running in. Defaults to 'us-east-1'. Please be aware that this must match the region of the elastic beanstalk app. 398 | * **app**: Elastic Beanstalk application name. 399 | * **env**: Elastic Beanstalk environment name which will be updated. 400 | * **bucket_name**: Bucket name to upload app to. 401 | 402 | #### Examples: 403 | 404 | dpl --provider=elasticbeanstalk --access-key-id= --secret-access-key="" --app="example-app-name" --env="example-app-environment" --region="us-west-2" 405 | 406 | ### BitBalloon: 407 | 408 | #### Options: 409 | 410 | * **access_token**: Optinoal. The access_token which can be found in the `.bitballoon` file after a deployment using the bitballoon CLI. Only required if no `.bitballoon` file is present. 411 | * **site_id**: Optional. The site_id which can be found in the .bitballoon file after a deployment using the bitballoon CLI. Only required if no `.bitballoon` file is present. 412 | * **local_dir**: Optional. The sub-directory of the built assets for deployment. Default to current path. 413 | 414 | #### Examples: 415 | 416 | dpl --access-token= --site-id=3f932c1e-708b-4573-938a-a07d9728c22e 417 | dpl --access-token= --site-id=3f932c1e-708b-4573-938a-a07d9728c22e --local-dir=build 418 | 419 | ### Puppet Forge: 420 | 421 | #### Options: 422 | 423 | * **user**: Required. The user name at Puppet forge. 424 | * **password**: Required. The Puppet forge password. 425 | * **url**: Optional. The forge URL to deploy to. Defaults to https://forgeapi.puppetlabs.com/ 426 | 427 | #### Examples: 428 | 429 | dpl --provider=puppetforge --user=puppetlabs --password=s3cr3t 430 | 431 | ### packagecloud: 432 | 433 | #### Options: 434 | 435 | * **username**: Required. The packagecloud.io username. 436 | * **token**: Required. The [packagecloud.io api token](https://packagecloud.io/docs/api#api_tokens). 437 | * **repository**: Required. The repository to push to. 438 | * **local_dir**: Optional. The sub-directory of the built assets for deployment. Default to current path. 439 | * **dist**: Required for deb and rpm. The complete list of supported strings can be found on the [packagecloud.io docs](https://packagecloud.io/docs#os_distro_version) 440 | 441 | #### Examples: 442 | 443 | dpl --provider=packagecloud --username=packageuser --token=t0k3n --repository=myrepo 444 | dpl --provider=packagecloud --username=packageuser --token=t0k3n --repository=myrepo --dist=ubuntu/precise 445 | dpl --provider=packagecloud --username=packageuser --token=t0k3n --repository=myrepo --local-dir="${TRAVIS_BUILD_DIR}/pkgs" --dist=ubuntu/precise 446 | 447 | ### Chef Supermarket: 448 | 449 | #### Options: 450 | 451 | * **user_id**: Required. The user name at Chef Supermarket. 452 | * **client_key**: Required. The client API key file name. 453 | * **cookbook_category**: Required. The cookbook category in Supermarket (see: https://docs.getchef.com/knife_cookbook_site.html#id12 ) 454 | 455 | #### Examples: 456 | 457 | dpl --provider=chef-supermarket --user-id=chef --client-key=.travis/client.pem --cookbook-category=Others 458 | 459 | --------------------------------------------------------------------------------