diff --git a/.gitignore b/.gitignore index 4fc5d3fa0..eebb386cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ /.bundle /.rvmrc /coverage -/pkg +/dist /rdoc /tags /vendor /.rbenv-version +/.cache +/resources/exe/heroku-codesign-cert.pvk diff --git a/.travis.yml b/.travis.yml index 635721991..ee73af1a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,22 +1,24 @@ -before_script: - - git config --global user.email "bot@heroku.com" - - git config --global user.name "Heroku Bot (Travis CI)" - -bundler_args: --without development - language: ruby -notifications: - email: false - webhooks: - on_success: always - on_failure: always - urls: - - http://dx-helper.herokuapp.com/travis - rvm: - - 1.8.7 - - 1.9.2 - 1.9.3 + - 2.0.0 + - 2.1.5 + - 2.2.0 + +sudo: false + +cache: bundler + +before_script: + - git config --global user.email "bot@heroku.com" + - git config --global user.name "Heroku Bot (Travis CI)" + +script: bundle exec rspec spec --color -script: bundle exec rspec -bfs spec +deploy: + provider: rubygems + on: + tags: true + api_key: + secure: ALsBCGGvdAiIEJR9zTzxumcgCaS5eqOs7Oee7e4SiDgHrT/DRSsFJBtNp9mJvQvHzW3FqSFZU7NO6tSRkwHGdGGw7pf/emjZ2ua0exuyCQ3LaCJBdwSQXl0GTMhhaMCCd2NYWJ+Fa3Q9jWWAdCfV8rqz5AX4ZG6fi3C2uubppVs= diff --git a/CHANGELOG b/CHANGELOG index 50c19b84b..a338ea73f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,485 @@ +3.37.6 2015-06-03 +================= +Fix non-ascii windows username issue for Ruby 2.0+ +Increased pgbackups polling sleep interval to prevent rate limiting + +3.37.5 2015-06-01 +================= +Fixed auth:whoami on first run + +3.37.4 2015-06-01 +================= +Use toolbelt v4 for auth:whoami +Fix ps:scale with Standard-1X dynos +Fix arm support on some machines +Allow pg:backups cancel to take an optional transfer name +Fix sorting on pg:backups +Fixed config:unset for paranoid apps + +3.37.3 2015-05-28 +================= +Added arm support for Toolbelt v4 + +3.37.2 2015-05-27 +================= +Made confirmation language clearer for pg:copy +Add --force-colors to logs +Removed switzerland variant +Display price of add-on plan on some commands + +3.37.1 2015-05-18 +================= +Hide hidden commands +Fixed bug with copying cacert when `~/.heroku` does not exist + +3.37.0 2015-05-16 +================= +Fixed bug in updater checking version strings + +3.36.11 2015-05-15 +================== +Allow HEROKU_SSL_VERIFY=disable for v4 setup + +3.36.10 2015-05-15 +================== +Updated CA Certs +Add cacert.pem to ~/.heroku for use in v4 CLI +Fix pgbackups:unschedule issues + +3.36.8 2015-05-14 +================= +Changed fork to use --from and --to instead of --app +Added note that process types must be alphanumeric for ps:scale +Fixed issues with DATABASE_URL +Fixed display of addon attachments when specifying attachment name +Improved updating text + +3.36.7 2015-05-12 +================= +Update plugins if they fail to load +Show message for ps validation +Moved heroku status to v4 CLI + +3.36.6 2015-05-12 +================= +Fixed bug with pgbackups polling +Fixed plugin command help +Fixed rbconfig bug on ruby 1.9.2 + +3.36.5 2015-05-11 +================= +Added redis commands +Added plugins:link v4 shim +Bug fixe for users with partial privileges on apps:info +Removed usage tracking + +3.36.4 2015-05-07 +================= +Made v4 autofix safer + +3.36.3 2015-05-07 +================= +Autofix broken v4 installs +More robust backup polling + +3.36.2 2015-05-07 +================= +Documented new run functionality +Fixed issue on windows with spaces in username +Show warning if Toolbelt is currently updating + +3.36.1 2015-05-07 +================= +Big performance boost to v4 commands by running them before v3 is setup +Optimize ruby require order + +3.36.0 2015-05-06 +================= +Included hpg addon:create shortcuts from heroku-pg-extras +Add quota indicator for ps +Show release update warnings less frequently +Switch to v4 version of maintenance commands +Switch to v4 version of git commands + +3.35.1 2015-05-05 +================= +Enabled v4 version of run for commands with a '--' argument + +3.35.0 2015-05-05 +================= +Added --exit-code flag to run command +Fixed addons:open for paranoid apps + +3.34.0 2015-05-04 +================= +Pull in heroku-addon-attachments plugin + +3.33.0 2015-05-01 +================= +Added shell flag to config:get +Renamed buildpack to buildpacks +Removed shell escaping from config:get --shell if it is not a tty +Fixed bug when api returned no certificate on certs:info +Added output for pg:backups commands +Merged the dyno-types plugin +Allow cedar-10 as stack in apps:create + +3.32.0 2015-04-21 +================= +Added new fork implementation +Added --verbose option to pg:ps +Fixed bug with pg in_maintenance? flag +Fixed issue with windows home folders and non-ascii characters +No longer exits if netrc is missing but HEROKU_API_KEY is provided + +3.31.3 2015-04-09 +================= +Fixed some bugs around pg:backups +Fixed v4 download on windows +Add FreeBSD as supported os for v4 + +3.31.2 2015-04-08 +================= +Downgraded bundled version of rest-client to one that does not require ffi on windows + +3.31.1 2015-04-08 +================= +Removed windows-specific gems from Gemfile.lock + +3.31.0 2015-04-08 +================= +Removed support for Ruby 1.8.7 +Updated all dependencies to latest +Show backups that will be migrated from PGBackups + +3.30.6 2015-04-02 +================= +Skipped calling of `stty icanon echo` on Windows +Updated 1.8.7 warning + +3.30.5 2015-04-02 +================= +Added warning for ruby 1.8.7 + +3.30.4 2015-04-01 +================= +Postgres backups cleanup and fixes + +3.30.3 2015-03-21 +================= +Reverted v4 fork implmentation + +3.30.2 2015-03-19 +================= +Made updater also update v4 if it is setup + +3.30.1 2015-03-19 +================= +Updated gems + +3.30.0 2015-03-18 +================= +New fork implementation +Only show request ids on error + +3.29.0 2015-03-17 +================= +Fixed architecture detection on jruby for jsplugins +Relaxed version requirement for multi_json +Added request ID logging on API errors + +3.28.6 2015-03-11 +================= +Changed fork to use new pgbackup implementation +Show who is using psql and how +Present cedar as cedar-10 +Use full_host_uri.host for checking netrc + +3.28.4 2015-03-10 +================= +Reverted new pgbackup implementation for fork + +3.28.3 2015-03-09 +================= +Show message that toolbelt v4 is installing +Changed fork to use new pgbackup implementation + +3.28.2 2015-03-02 +================= +Fixed bug with --wait-interval flag for pg:wait + +3.28.1 2015-02-27 +================= +Renamed local:start to just local + +3.28.0 2015-02-27 +================= +Added `heroku local` +Added flag to customize poll interval for pg:wait +Fixed error message for default orgs +Allow org for all commands +Improved help formatting for v4 plugins + +3.27.2 2015-02-24 +================= +Added restart to primary commands in help +Fixed issue with argument passing to v4 plugins +Added full help for v4 plugins + +3.27.1 2015-02-24 +================= +Bumped gem version number to 3.27.1 + +3.27.0 2015-02-24 +================= +Make pgbackups work with config vars other than DATABASE_URL +Require confirmation for dangerous pg:backups commands +Updated netrc to 0.10.3 +Fix pg:backups unschedule +Deprecated heroku-push plugin + +3.26.1 2015-02-18 +================= +Added buildpack command +Ignore HEROKU_ORGANIZATION env var if blank +Added OpenBSD to v4 plugins + +3.26.0 2015-02-10 +================= +Removed default orgs in place of HEROKU_ORGANIZATION env var (#1395) +Display errors if rollbar does not accept it (#1412) +Fix case-sensitive reading of X-Confirmation-Required header (#1410) +Fix v4 plugin commands without command name and topic only +Bug fixes for v4 plugins on Windows +Show rollbar errors in ~/.heroku/error.log (#1408) +Allow db:push and db:pull to work with remote databases (#1386) +Cleaner error messages when failing to read netrc files (#1404) +Create heroku directory if it does not exist when writing ~/.heroku/error.log (#1403) +More descriptive error message when heroku run has an SSL error (#1401) +Change plugin example to use heroku-production-check instead of heroku-accounts (#1400) +Updated help text for twofactor commands (#1398) +Show warning if CLI is run under jruby (#1396) +Show out of date warning if toolbelt is not autoupdatable (#1394) + +3.25.0 2015-01-29 +================= +Added `plugins:uninstall` for toolbelt v4 plugins +Prevent fork from deleting an existing app +Added okjson back in for users that have not updated their plugins +Show app name in `run:detached` + +3.24.5 2015-01-28 +================= +Better errors when git is not found or not working +Fixed bug with unhandled EPIPE exception +Fixed credential warning message +Added ruby version to exception tracking +Added disabling of error tracking with HEROKU_DISABLE_ERROR_REPORTING + +3.24.4 2015-01-27 +================= +Rolled Rollbar creds + +3.24.3 2015-01-27 +================= +Reverted db:push and db:pull feature that allowed remote databases due to bugs +Enabled error tracking on unhandled exceptions + +3.24.2 2015-01-27 +================= +Temporarily disable rollbar error reporting since it's too noisy + +3.24.1 2015-01-27 +================= +Skip error reporting for errors like ctrl-c and command failures. + +3.24.0 2015-01-27 +================= +Added error tracking with rollbar +Added pg:backups from pg-extras plugin +Allow db:push and db:pull to use remote databases (for setups like docker) +Fixed apps:info for paranoid apps +Upgraded excon to 0.43.0 + +3.23.3 2015-01-16 +================= +Fixed bug where jsplugins could override core commands +Prevent non-autoupdatable clients from autoupdating + +3.23.2 2015-01-16 +================= +Added certs:generate command +Fixed bug with plugins +Show request-id on HEROKU_DEBUG + +3.23.1 2015-01-13 +================= +Fixed authentication failure in release + +3.23.0 2015-01-13 +================= +Added help for jsplugins +Fixed remote setting in git:remote +Fixed bug with newlines in .bashrc on OSX + +3.22.1 2015-01-05 +================= +Updated cacert.pem + +3.22.0 2015-01-05 +================= +Added JavaScript-based plugin support + +3.21.4 2014-12-31 +================= +Fixed bug with git warnings +Removed git warnings for non-osx and non-windows environments +Happy New Year! + +3.21.3 2014-12-23 +================= +Show warning for insecure Git clients +Fix issue with MultiJson require +Happy Festivus! + +3.21.1 2014-12-17 +================= +No changes, needed to bump the version + +3.21.0 2014-12-17 +================= +Upgraded heroku-api gem +Explicitly preauth for 2fa commands instead of automatically on every failure +Show warning when using heroku-accounts (since it is incompatible with http-git) +Added HTTP instrumentor for debugging with HEROKU_DEBUG=true + +3.20.0 2014-12-10 +================= +Upgraded excon and netrc gems +Use Dir.home for Ruby 1.9+ +Show plugins in `heroku version` + +3.19.0 2014-12-09 +================= +Simplified updating by performing updates synchonously instead of in a separate process + +3.18.0 2014-12-05 +================= +Upgraded gems (notably netrc, heroku-api and excon) +Show warning if HEROKU_API_KEY is set +Tweaks to manager url + +3.17.1 2014-12-04 +================= +Added debug logging for auth + +3.17.0 2014-12-03 +================= +Default to http git +Reduced update check duration to 10 minutes + +3.16.2 2014-11-23 +================= +Clean build dist directory before releasing + +3.16.1 2014-11-23 +================= +Fixed bug with nil release description + +3.16.0 2014-11-20 +================= +Fixed update spawn command on some windows installs +Added warning for https git when netrc doesn't have credentials +Made runnable without readline + +3.15.3 2014-11-10 +================= +Removed bamboo from stack command +Reverted 2fa input masking + +3.15.2 2014-11-04 +================= +Mask 2fa inputs longer than 12 characters + +3.15.1 2014-11-04 +================= +Upgraded launchy to 2.4.3 +Only lock for updates when updating, not checking for updates + +3.15.0 2014-10-24 +================= +Skip preauth with no app context +Fixed Debian control file dependencies + +3.14.0 2014-10-21 +================= +Use preauth instead of 2fa for all API calls + +3.13.0 2014-10-20 +================= +Switch to multi_json +Overwrite git remotes with `heroku git:remote` + +3.12.1 2014-10-07 +================= +Fixed Excon 0.40.0 in Gemfile +Fixed git finders to work with env vars +Symbolic config vars + +3.12.0 2014-10-06 +================= +Excon 0.40.0 +Unique output warnings +More git options + +3.11.3 2014-10-02 +================= +Replace code. with git. in .netrc +Always use preauth for 2fa + +3.11.2 2014-09-26 +================= +Use server-side connection for pg:killall +Send orgs requests to api.heroku.com + +3.10.6 2014-09-04 +================= +Added ssh-keygen shim for Windows + +3.10.5 2014-08-27 +================= +Ocra support for building standalone heroku-ocra.exe + +3.10.4 2014-08-22 +================= +Upgraded excon to 0.39.5 + +3.10.3 2014-08-21 +================= +Fixed minor issue with recording fork source + +3.10.2 2014-08-21 +================= +Removed lazy-loading of heroku-api and rest_client (was swallowing errors) +Fail fast for issue with pgbackups:transfer +Removed price tier from info +Help info for two factor topic +Send deploy type and source metadata for forks + +3.10.1 2014-08-14 +================= +No changes, just verifying new release code is in order + +3.10.0 2014-08-14 +================= +Fixed beta releases +Upgrade heroku-api and excon + +3.9.7 2014-08-12 +================ +Bring 2fa:disable back +Several pg and pgbackups command improvements + 3.9.4 2014-07-21 ================ Actually fix a bug where setting HEROKU_API_KEY would cause failures diff --git a/Gemfile b/Gemfile index 77d3fd114..2125d3971 100644 --- a/Gemfile +++ b/Gemfile @@ -3,21 +3,13 @@ source "https://rubygems.org" gemspec group :development, :test do - gem "rake", ">= 0.8.7" - gem "rr", "~> 1.0.2" -end - -group :development do + gem "rake" + gem "rr" gem "aws-s3" - gem "fpm" - gem "rubyzip" -end - -group :test do + gem "mime-types" gem "fakefs" - gem "jruby-openssl", :platform => :jruby gem "json" - gem "rspec", ">= 2.0" - gem "sqlite3" + gem "rspec" gem "webmock" + gem "coveralls", :require => false end diff --git a/Gemfile.lock b/Gemfile.lock index c41b2a239..3e2624e9c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,84 +1,90 @@ PATH remote: . specs: - heroku (3.9.4) - heroku-api (= 0.3.17) + heroku (3.37.6) + heroku-api (>= 0.3.19) launchy (>= 0.3.2) - netrc (~> 0.7.7) - rest-client (~> 1.6.1) - rubyzip + multi_json (>= 1.10) + netrc (>= 0.10.0) + rest-client (>= 1.6.0) + rubyzip (>= 0.9.9) GEM remote: https://rubygems.org/ specs: - addressable (2.3.2) - arr-pm (0.0.7) - cabin (> 0) + addressable (2.3.8) aws-s3 (0.6.3) builder mime-types xml-simple - backports (2.3.0) - builder (3.1.4) - cabin (0.4.4) - json - clamp (0.5.0) - crack (0.3.2) - diff-lcs (1.1.3) - excon (0.38.0) - fakefs (0.4.2) - fpm (0.4.6) - arr-pm (~> 0.0.7) - backports (= 2.3.0) - cabin (~> 0.4.3) - clamp - json - heroku-api (0.3.17) - excon (~> 0.27) - multi_json (~> 1.8.2) - json (1.7.7) - launchy (2.4.2) + builder (3.2.2) + coveralls (0.8.0) + multi_json (~> 1.10) + rest-client (>= 1.6.8, < 2) + simplecov (~> 0.9.1) + term-ansicolor (~> 1.3) + thor (~> 0.19.1) + crack (0.4.2) + safe_yaml (~> 1.0.0) + diff-lcs (1.2.5) + docile (1.1.5) + excon (0.45.2) + fakefs (0.6.7) + heroku-api (0.3.23) + excon (~> 0.44) + multi_json (~> 1.8) + json (1.8.2) + launchy (2.4.3) addressable (~> 2.3) - mime-types (1.21) - multi_json (1.8.4) - netrc (0.7.7) - rake (10.0.3) - rdoc (4.1.1) - json (~> 1.4) + mime-types (1.25.1) + multi_json (1.11.0) + netrc (0.10.3) + rake (10.4.2) + rdoc (4.2.0) rest-client (1.6.8) mime-types (~> 1.16) rdoc (>= 2.4.2) - rr (1.0.4) - rspec (2.12.0) - rspec-core (~> 2.12.0) - rspec-expectations (~> 2.12.0) - rspec-mocks (~> 2.12.0) - rspec-core (2.12.2) - rspec-expectations (2.12.1) - diff-lcs (~> 1.1.3) - rspec-mocks (2.12.2) - rubyzip (0.9.9) - sqlite3 (1.3.7) - sqlite3 (1.3.7-x86-mingw32) - webmock (1.9.0) - addressable (>= 2.2.7) - crack (>= 0.1.7) - xml-simple (1.1.2) + rr (1.1.2) + rspec (3.2.0) + rspec-core (~> 3.2.0) + rspec-expectations (~> 3.2.0) + rspec-mocks (~> 3.2.0) + rspec-core (3.2.3) + rspec-support (~> 3.2.0) + rspec-expectations (3.2.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-mocks (3.2.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-support (3.2.2) + rubyzip (1.1.7) + safe_yaml (1.0.4) + simplecov (0.9.2) + docile (~> 1.1.0) + multi_json (~> 1.0) + simplecov-html (~> 0.9.0) + simplecov-html (0.9.0) + term-ansicolor (1.3.0) + tins (~> 1.0) + thor (0.19.1) + tins (1.3.5) + webmock (1.21.0) + addressable (>= 2.3.6) + crack (>= 0.3.2) + xml-simple (1.1.5) PLATFORMS ruby - x86-mingw32 DEPENDENCIES aws-s3 + coveralls fakefs - fpm heroku! - jruby-openssl json - rake (>= 0.8.7) - rr (~> 1.0.2) - rspec (>= 2.0) - rubyzip - sqlite3 + mime-types + rake + rr + rspec webmock diff --git a/LICENSE b/LICENSE index e47d0a1f6..ffaa7a305 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © Heroku 2008 - 2012 +Copyright © Heroku 2008 - 2014 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 775604fc3..d4111f8ef 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,19 @@ -Heroku CLI +![](https://d4yt8xl9b7in.cloudfront.net/assets/home/logotype-heroku.png) Heroku CLI ========== The Heroku CLI is used to manage Heroku apps from the command line. -For more about Heroku see . +For more about Heroku see -To get started see +To get started see -[![Build Status](https://secure.travis-ci.org/heroku/heroku.png)](http://travis-ci.org/heroku/heroku) -[![Dependency Status](https://gemnasium.com/heroku/heroku.png)](https://gemnasium.com/heroku/heroku) +[![Build Status](https://travis-ci.org/heroku/heroku.svg?branch=master)](https://travis-ci.org/heroku/heroku) +[![Coverage Status](https://img.shields.io/coveralls/heroku/heroku.svg)](https://coveralls.io/r/heroku/heroku?branch=master) Setup ----- - - - - - - - - - - - - - - - - - - - - - -
If you have...Install with...
Mac OS XDownload OS X package
WindowsDownload Windows .exe installer
Ubuntu Linuxapt-get repository
OtherTarball (add contents to your $PATH)
+First, [Install the Heroku CLI with the Toolbelt](https://toolbelt.heroku.com). Once installed, you'll have access to the `heroku` command from your command shell. Log in using the email address and password you used when creating your Heroku account: @@ -54,13 +33,6 @@ API For additional information about the API see [Heroku API Quickstart](https://devcenter.heroku.com/articles/platform-api-quickstart) and [Heroku API Reference](https://devcenter.heroku.com/articles/platform-api-reference). -Development ------------ - -If you're working on the CLI and you can smoke-test your changes: - - $ bundle exec heroku - Meta ---- diff --git a/RELEASE.md b/RELEASE.md index a98ec01fb..5cafc9e8b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,40 +1,117 @@ Heroku CLI Release Process ========================== -### Ensure tests are passing +Releasing the CLI involves releasing a few different things. The important tasks can all be done on the buildserver. -* `bundle exec rake spec` - -### Prepare new version +## Releasing with buildserver +* Run test suite: `bundle exec rake` * Update version number in `lib/heroku/version.rb` to `X.Y.Z` * Bump the patch level `Z` if the release contains bugfixes that do not change functionality * Bump the minor level `Y` if the release contains new functionality or changes to existing functionality -* Run `bundle install` to update the version of heroku in the Gemfile.lock +* Run `bundle install` to update the version of heroku in the `Gemfile.lock` * Update `CHANGELOG` * Commit the changes `git commit -m "vX.Y.Z" -a` * Push changes to master `git push origin master` +* Go to the buildserver and release http://54.148.200.17/. [Here is the code for the buildserver.](https://github.com/heroku/toolbelt-build-server) +* [optional] Release the OSX pkg (instructions below) +* [optional] Release the WIN pkg (instructions below) + +## Notes + +The last 2 are optional because existing toolbelts will autoupdate after the first command is run. This isn't the case for deb packages which is why they're included in the main process. There can still be situations (although minor ones) where not releasing the osx/win packages can cause problems so they normally should always be run. + +The release process will prevent you from releasing an already released version. If you have a bad/incomplete release, you may need to bump the version number again. + +## Main Release + +This process releases the tgz (standalone/homebrew), zip (for autoupdates), deb package and ruby gem. It's everything that is required to not end up with a partial release. This is what the buildserver does for you, so you shouldn't have to do this manually (this is just for reference). Because this builds a deb package, you must be on an Ubuntu box. + +Prerequisites: + +* Running from Ubuntu +* Make sure you have permissions to `heroku` gem through `gem` https://rubygems.org/gems/heroku. +* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` +* deb private key +* Ubuntu prerequisites: + +```sh +echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections +sudo apt-get install -y build-essential libpq-dev libsqlite3-dev curl xvfb wine +``` + +If this is your first time, you should first build the packages: `bundle exec rake build` Then look inside `./dist` to test each of the packages. + +Once you are confident it works, release: `bundle exec rake release`. Note that release will automatically build if the packages are not there (there is no need to run `rake build`). + +Note that you can look inside the `Rakefile` to test out each part of the step on your machine before it is built. + +## OSX Release + +Prerequisites: + +* OSX +* Heroku Developer ID Installer Certificate in Keychain +* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` + +To build for testing: `bundle exec rake pkg:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.pkg`. +To release: `bundle exec rake pkg:release`. + +## Windows Release + +This is run not from a Windows machine, but from a UNIX machine with Wine. + +Mac Prerequisites: + +* Heroku Developer ID Installer Certificate in Keychain +* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` +* Install [XQuartz](http://xquartz.macosforge.org/) manually, or via the terminal (restart required): + +```sh +curl -O# http://xquartz-dl.macosforge.org/SL/XQuartz-2.7.6.dmg +hdiutil attach XQuartz-2.7.6.dmg -mountpoint /Volumes/xquartz +sudo installer -store -pkg /Volumes/xquartz/XQuartz.pkg -target / +hdiutil detach /Volumes/xquartz +rm XQuartz-2.7.6.dmg +``` + +* `/opt/X11/bin` should be in your `$PATH` so `Xvfb` can be started. +* Install wine: `brew install wine` +* The pvk file: + +The certificate and private key for code signing are in the repo in: + +> dist/resources/exe/heroku-codesign-cert* + +which is in the format mono signcode wants. + +The pvk file is encrypted. If you want the build not to prompt you for +its passphrase, you'll need to decrypt it. See the `exe:pvk-nocrypt` task. + +Bewake the openssl version on the Mac doesn't work with `exe:pvk-nocrypt`. +See comments on the source code for details and solution. + +If you wanna leave the key encrypted, you still have to link it before +building; run the `exe:pvk` task for that. + +You'll have to ask the right person for the passphrase to the key. + +You then need to initialize a custom wine build environment. The `exe:init-wine` +task will do that for you. -### Release the gem +To build for testing: `bundle exec rake exe:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.exe`. +To release: `bundle exec rake pkg:release`. -* Ask @ddollar for: - * Permissions to Rubygems.org - * Access to the `toolbelt` Heroku app - * `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` config var values (export values in terminal) - * Access and permissions to run builds on http://dx-jenkins.herokai.com/ -* Release the gem `bundle exec rake release` - * Enter password for `sudo` during release - * Confirm gem release at http://rubygems.org/gems/heroku/versions +## Ruby versions -### Release the toolbelt +Toolbelt bundles Ruby using different sources according to the OS: -* Move to a checkout of the toolbelt repo and make sure everything is up to date `git pull` - - If this is a new checkout, run `git submodule init` and `git submodule update` -* Move to the components/heroku directory, `git fetch` and `git reset --hard HASH` where HASH is commit hash of vX.Y.Z -* Move back to the root dir of the toolbelt repo, stage `git add .`, commit `git commit -m "bump heroku submodule to vX.Y.Z"`, and push `git push` submodule changes -* Start toolbelt-build build at http://dx-jenkins.herokai.com/ (this will be opened by rake release automatically) +- Windows: fetches [rubyinstaller.exe](http://rubyinstaller.org/) from S3. +- Mac: fetches ruby.pkg from S3. That file was extracted from +[RailsInstaller](http://railsinstaller.org/en). +- Linux: uses system debs for Ruby. -### Changelog (only if there is at least one major new feature) +## Changelog (only if there is at least one major new feature) * Create a [new changelog](http://devcenter.heroku.com/admin/changelog_items/new) * Set the title to `Heroku CLI vX.Y.Z released with #{highlights}` diff --git a/Rakefile b/Rakefile index c381f9c28..8162d22e4 100644 --- a/Rakefile +++ b/Rakefile @@ -1,204 +1,43 @@ -require "rubygems" +require "bundler/setup" PROJECT_ROOT = File.expand_path("..", __FILE__) $:.unshift "#{PROJECT_ROOT}/lib" - -require "heroku/version" -begin - require "rspec/core/rake_task" - - desc "Run all specs" - RSpec::Core::RakeTask.new(:spec) do |t| - t.verbose = true - end -rescue LoadError - # The test gem group fails to install on the platform for some reason -end - -task :default => :spec - -## dist - -require "erb" -require "fileutils" -require "tmpdir" - -def assemble(source, target, perms=0644) - FileUtils.mkdir_p(File.dirname(target)) - File.open(target, "w") do |f| - f.puts ERB.new(File.read(source)).result(binding) - end - File.chmod(perms, target) -end - -def assemble_distribution(target_dir=Dir.pwd) - distribution_files.each do |source| - target = source.gsub(/^#{project_root}/, target_dir) - FileUtils.mkdir_p(File.dirname(target)) - FileUtils.cp(source, target) - end -end - -GEM_BLACKLIST = %w( bundler heroku ) - -def assemble_gems(target_dir=Dir.pwd) - lines = %x{ bundle show }.strip.split("\n") - raise "error running bundler" unless $?.success? - - %x{ env BUNDLE_WITHOUT="development:test" bundle show }.split("\n").each do |line| - if line =~ /^ \* (.*?) \((.*?)\)/ - next if GEM_BLACKLIST.include?($1) - puts "vendoring: #{$1}-#{$2}" - gem_dir = %x{ bundle show #{$1} }.strip - FileUtils.mkdir_p "#{target_dir}/vendor/gems" - %x{ cp -R "#{gem_dir}" "#{target_dir}/vendor/gems" } - end - end.compact -end - -def beta? - Heroku::VERSION.to_s =~ /pre/ -end - -def clean(file) - rm file if File.exists?(file) -end - -def distribution_files(type=nil) - require "heroku/distribution" - base_files = Heroku::Distribution.files - type_files = type ? - Dir[File.expand_path("../dist/resources/#{type}/**/*", __FILE__)] : - [] - #base_files.concat(type_files) - base_files -end - -def mkchdir(dir) - FileUtils.mkdir_p(dir) - Dir.chdir(dir) do |dir| - yield(File.expand_path(dir)) - end -end - -def pkg(filename) - FileUtils.mkdir_p("pkg") - File.expand_path("../pkg/#{filename}", __FILE__) -end - -def project_root - File.dirname(__FILE__) -end - -def resource(name) - File.expand_path("../dist/resources/#{name}", __FILE__) -end - -def s3_connect - return if @s3_connected - - require "aws/s3" - - unless ENV["HEROKU_RELEASE_ACCESS"] && ENV["HEROKU_RELEASE_SECRET"] - puts "please set HEROKU_RELEASE_ACCESS and HEROKU_RELEASE_SECRET in your environment" - exit 1 - end - - AWS::S3::Base.establish_connection!( - :access_key_id => ENV["HEROKU_RELEASE_ACCESS"], - :secret_access_key => ENV["HEROKU_RELEASE_SECRET"] - ) - - @s3_connected = true -end - -def store(package_file, filename, bucket="assets.heroku.com") - s3_connect - puts "storing: #{filename}" - AWS::S3::S3Object.store(filename, File.open(package_file), bucket, :access => :public_read) -end - -def tempdir - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - yield(dir) - end - end -end +require "heroku" def version - require "heroku/version" Heroku::VERSION end -Dir[File.expand_path("../dist/**/*.rake", __FILE__)].each do |rake| - import rake -end - -def poll_ci - require("vendor/heroku/okjson") - require("net/http") - data = Heroku::OkJson.decode(Net::HTTP.get("travis-ci.org", "/heroku/heroku.json")) - case data["last_build_status"] - when nil - print(".") - sleep(1) - poll_ci - when 0 - puts("SUCCESS") - when 1 - puts("FAILURE") - end -end +Dir.glob('tasks/helpers/*.rb').each { |r| import r } +Dir.glob('tasks/*.rake').each { |r| import r } -desc("Check current ci status and/or wait for build to finish.") -task "ci" do - poll_ci +desc "clean" +task :clean do + rm_r "dist" + mkdir "dist" end -desc("Create a new changelog article") -task "changelog" do - changelog = <<-CHANGELOG -Heroku CLI v#{version} released with - -A new version of the Heroku CLI is available with - -See the [CLI changelog](https://github.com/heroku/heroku/blob/master/CHANGELOG) for details and update by using \\`heroku update\\`. -CHANGELOG - - `echo "#{changelog}" | pbcopy` - - `open http://devcenter.heroku.com/admin/changelog_items/new` +desc "release v#{version}" +task "release" => ["can_release", "clean", "build", "tgz:release", "zip:release", "manifest:update", "deb:release", "gem:release", "git:tag"] do + puts("released v#{version}") end -desc("Release the latest version") -task "release" => ["gem:release", "tgz:release", "zip:release", "manifest:update"] do - puts("Released v#{version}") +desc "build v#{version}" +task "build" => ["tgz:build", "zip:build", "deb:build", "gem:build"] do + puts("built v#{version}") end -desc("Display statistics") -task "stats" do - require "heroku/command" - Dir[File.join(File.dirname(__FILE__), 'lib', 'heroku', 'command', '*.rb')].each do |file| - require(file) +desc "check to see if v#{version} is releaseable" +task :can_release do + if ENV['HEROKU_RELEASE_ACCESS'].nil? || ENV['HEROKU_RELEASE_SECRET'].nil? + $stderr.puts "cannot release, #{version}, HEROKU_RELEASE_ACCESS and HEROKU_RELEASE_SECRET must be set" + exit(1) end - commands, namespaces = Hash.new {|hash, key| hash[key] = 0}, [] - Heroku::Command.commands.keys.each do |key| - data = key.split(':') - unless data.first == data.last - commands[data.last] += 1 - end - namespaces |= [data.first] - end - puts "#{namespaces.length} Namespaces:" - puts "#{namespaces.join(', ')}" - puts - puts "#{commands.keys.length} Commands:" - max = commands.values.max - max.downto(0).each do |count| - keys = commands.keys.select {|key| commands[key] == count} - unless keys.empty? - puts("#{count}x #{keys.join(', ')}") - end + system './bin/heroku auth:whoami' or exit 1 + if `gem list ^heroku$ --remote` == "heroku (#{version})\n" + $stderr.puts "cannot release #{version}, v#{version} is already released" + exit(1) end end + +task :default => :spec diff --git a/data/cacert.pem b/data/cacert.pem index b775088c0..23f4a8bcb 100644 --- a/data/cacert.pem +++ b/data/cacert.pem @@ -1,76 +1,23 @@ -## DOWNLOADED FROM: http://curl.haxx.se/ca/cacert.pem ## -## ca-bundle.crt -- Bundle of CA Root Certificates +## Bundle of CA Root Certificates ## -## Certificate data from Mozilla as of: Tue Apr 22 08:29:31 2014 +## Certificate data from Mozilla as of: Wed Apr 22 03:12:04 2015 ## ## This is a bundle of X.509 certificates of public Certificate Authorities ## (CA). These were automatically extracted from Mozilla's root certificates ## file (certdata.txt). This file can be found in the mozilla source tree: -## http://mxr.mozilla.org/mozilla-release/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1 +## http://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt ## ## It contains the certificates in PEM format and therefore ## can be directly used with curl / libcurl / php_curl, or with ## an Apache+mod_ssl webserver for SSL client authentication. ## Just configure this file as the SSLCACertificateFile. ## +## Conversion done with mk-ca-bundle.pl version 1.25. +## SHA1: ed3c0bbfb7912bcc00cd2033b0cb85c98d10559c +## -GTE CyberTrust Global Root -========================== ------BEGIN CERTIFICATE----- -MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9HVEUg -Q29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNvbHV0aW9ucywgSW5jLjEjMCEG -A1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJvb3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEz -MjM1OTAwWjB1MQswCQYDVQQGEwJVUzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQL -Ex5HVEUgQ3liZXJUcnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0 -IEdsb2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrHiM3dFw4u -sJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTSr41tiGeA5u2ylc9yMcql -HHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X404Wqk2kmhXBIgD8SFcd5tB8FLztimQID -AQABMA0GCSqGSIb3DQEBBAUAA4GBAG3rGwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMW -M4ETCJ57NE7fQMh017l93PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OF -NMQkpw0PlZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ ------END CERTIFICATE----- - -Thawte Server CA -================ ------BEGIN CERTIFICATE----- -MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT -DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs -dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UE -AxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5j -b20wHhcNOTYwODAxMDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNV -BAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29u -c3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcG -A1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0 -ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl -/Kj0R1HahbUgdJSGHg91yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg7 -1CcEJRCXL+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGjEzAR -MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG7oWDTSEwjsrZqG9J -GubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6eQNuozDJ0uW8NxuOzRAvZim+aKZuZ -GCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZqdq5snUb9kLy78fyGPmJvKP/iiMucEc= ------END CERTIFICATE----- - -Thawte Premium Server CA -======================== ------BEGIN CERTIFICATE----- -MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkExFTATBgNVBAgT -DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs -dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UE -AxMYVGhhd3RlIFByZW1pdW0gU2VydmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZl -ckB0aGF3dGUuY29tMB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYT -AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsGA1UEChMU -VGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2VydmljZXMgRGl2 -aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNlcnZlciBDQTEoMCYGCSqGSIb3DQEJARYZ -cHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2 -aovXwlue2oFBYo847kkEVdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIh -Udib0GfQug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMRuHM/ -qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQAm -SCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUIhfzJATj/Tb7yFkJD57taRvvBxhEf -8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JMpAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7t -UCemDaYj+bvLpgcUQg== ------END CERTIFICATE----- - Equifax Secure CA ================= -----BEGIN CERTIFICATE----- @@ -91,41 +38,6 @@ BIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee95 70+sB3c4 -----END CERTIFICATE----- -Verisign Class 3 Public Primary Certification Authority -======================================================= ------BEGIN CERTIFICATE----- -MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkGA1UEBhMCVVMx -FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5 -IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVow -XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz -IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94 -f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol -hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBAgUAA4GBALtMEivPLCYA -TxQT3ab7/AoRhIzzKBxnki98tsX63/Dolbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59Ah -WM1pF+NEHJwZRDmJXNycAA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2Omuf -Tqj/ZA1k ------END CERTIFICATE----- - -Verisign Class 3 Public Primary Certification Authority - G2 -============================================================ ------BEGIN CERTIFICATE----- -MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJBgNVBAYTAlVT -MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy -eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz -dCBOZXR3b3JrMB4XDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVT -MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy -eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz -dCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCO -FoUgRm1HP9SFIIThbbP4pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71 -lSk8UOg013gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwIDAQAB -MA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSkU01UbSuvDV1Ai2TT -1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7iF6YM40AIOw7n60RzKprxaZLvcRTD -Oaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpYoJ2daZH9 ------END CERTIFICATE----- - GlobalSign Root CA ================== -----BEGIN CERTIFICATE----- @@ -169,63 +81,6 @@ BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== -----END CERTIFICATE----- -ValiCert Class 1 VA -=================== ------BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp -b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh -bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIy -MjM0OFoXDTE5MDYyNTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 -d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEg -UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 -LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9YLqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIi -GQj4/xEjm84H9b9pGib+TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCm -DuJWBQ8YTfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0LBwG -lN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLWI8sogTLDAHkY7FkX -icnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPwnXS3qT6gpf+2SQMT2iLM7XGCK5nP -Orf1LXLI ------END CERTIFICATE----- - -ValiCert Class 2 VA -=================== ------BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp -b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh -bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw -MTk1NFoXDTE5MDYyNjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 -d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIg -UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 -LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDOOnHK5avIWZJV16vYdA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVC -CSRrCl6zfN1SLUzm1NZ9WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7Rf -ZHM047QSv4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9vUJSZ -SWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTuIYEZoDJJKPTEjlbV -UjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwCW/POuZ6lcg5Ktz885hZo+L7tdEy8 -W9ViH0Pd ------END CERTIFICATE----- - -RSA Root Certificate 1 -====================== ------BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp -b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh -bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw -MjIzM1oXDTE5MDYyNjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 -d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMg -UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 -LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDjmFGWHOjVsQaBalfDcnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td -3zZxFJmP3MKS8edgkpfs2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89H -BFx1cQqYJJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliEZwgs -3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJn0WuPIqpsHEzXcjF -V9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/APhmcGcwTTYJBtYze4D1gCCAPRX5r -on+jjBXu ------END CERTIFICATE----- - Verisign Class 3 Public Primary Certification Authority - G3 ============================================================ -----BEGIN CERTIFICATE----- @@ -274,33 +129,6 @@ RTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/bLvSHgCwIe34QWKCudiyxLtG UPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== -----END CERTIFICATE----- -Entrust.net Secure Server CA -============================ ------BEGIN CERTIFICATE----- -MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMCVVMxFDASBgNV -BAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5uZXQvQ1BTIGluY29ycC4gYnkg -cmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRl -ZDE6MDgGA1UEAxMxRW50cnVzdC5uZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhv -cml0eTAeFw05OTA1MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIG -A1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBi -eSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1p -dGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQ -aO2f55M28Qpku0f1BBc/I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5 -gXpa0zf3wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OCAdcw -ggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHboIHYpIHVMIHSMQsw -CQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5l -dC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF -bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENl -cnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu -dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0MFqBDzIwMTkw -NTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8BdiE1U9s/8KAGv7UISX8+1i0Bow -HQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAaMAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EA -BAwwChsEVjQuMAMCBJAwDQYJKoZIhvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyN -Ewr75Ji174z4xRAN95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9 -n9cd2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= ------END CERTIFICATE----- - Entrust.net Premium 2048 Secure Server CA ========================================= -----BEGIN CERTIFICATE----- @@ -346,40 +174,6 @@ Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp -----END CERTIFICATE----- -Equifax Secure Global eBusiness CA -================================== ------BEGIN CERTIFICATE----- -MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -RXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBTZWN1cmUgR2xvYmFsIGVCdXNp -bmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIwMDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMx -HDAaBgNVBAoTE0VxdWlmYXggU2VjdXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEds -b2JhbCBlQnVzaW5lc3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRV -PEnCUdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc58O/gGzN -qfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/o5brhTMhHD4ePmBudpxn -hcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAHMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j -BBgwFoAUvqigdHJQa0S3ySPY+6j/s1draGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hs -MA0GCSqGSIb3DQEBBAUAA4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okEN -I7SS+RkAZ70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv8qIY -NMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV ------END CERTIFICATE----- - -Equifax Secure eBusiness CA 1 -============================= ------BEGIN CERTIFICATE----- -MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -RXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENB -LTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQwMDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UE -ChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNz -IENBLTEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ -1MRoRvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBuWqDZQu4a -IZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKwEnv+j6YDAgMBAAGjZjBk -MBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEp4MlIR21kW -Nl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRKeDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQF -AAOBgQB1W6ibAxHm6VZMzfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5 -lSE/9dR+WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN/Bf+ -KpYrtWKmpj29f5JZzVoqgrI3eQ== ------END CERTIFICATE----- - AddTrust Low-Value Services Root ================================ -----BEGIN CERTIFICATE----- @@ -625,59 +419,6 @@ gn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwXQMAJKOSLakhT2+zNVVXxxvjpoixMptEm X36vWkzaH6byHCx+rgIW0lbQL1dTR+iS -----END CERTIFICATE----- -America Online Root Certification Authority 1 -============================================= ------BEGIN CERTIFICATE----- -MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkG -A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg -T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lkhsmj76CG -v2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym1BW32J/X3HGrfpq/m44z -DyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsWOqMFf6Dch9Wc/HKpoH145LcxVR5lu9Rh -sCFg7RAycsWSJR74kEoYeEfffjA3PlAb2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP -8c9GsEsPPt2IYriMqQkoO3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0T -AQH/BAUwAwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAUAK3Z -o/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBBQUAA4IBAQB8itEf -GDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkFZu90821fnZmv9ov761KyBZiibyrF -VL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAbLjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft -3OJvx8Fi8eNy1gTIdGcL+oiroQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43g -Kd8hdIaC2y+CMMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds -sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7 ------END CERTIFICATE----- - -America Online Root Certification Authority 2 -============================================= ------BEGIN CERTIFICATE----- -MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkG -A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg -T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC206B89en -fHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFciKtZHgVdEglZTvYYUAQv8 -f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2JxhP7JsowtS013wMPgwr38oE18aO6lhO -qKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JN -RvCAOVIyD+OEsnpD8l7eXz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0 -gBe4lL8BPeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67Xnfn -6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEqZ8A9W6Wa6897Gqid -FEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZo2C7HK2JNDJiuEMhBnIMoVxtRsX6 -Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3+L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnj -B453cMor9H124HhnAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3Op -aaEg5+31IqEjFNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE -AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmnxPBUlgtk87FY -T15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2LHo1YGwRgJfMqZJS5ivmae2p -+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzcccobGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXg -JXUjhx5c3LqdsKyzadsXg8n33gy8CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//Zoy -zH1kUQ7rVyZ2OuMeIjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgO -ZtMADjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2FAjgQ5ANh -1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUXOm/9riW99XJZZLF0Kjhf -GEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPbAZO1XB4Y3WRayhgoPmMEEf0cjQAPuDff -Z4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQlZvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuP -cX/9XhmgD0uRuMRUvAawRY8mkaKO/qk= ------END CERTIFICATE----- - Visa eCommerce Root =================== -----BEGIN CERTIFICATE----- @@ -954,30 +695,6 @@ nGQI0DvDKcWy7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw== -----END CERTIFICATE----- -TDC Internet Root CA -==================== ------BEGIN CERTIFICATE----- -MIIEKzCCAxOgAwIBAgIEOsylTDANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJESzEVMBMGA1UE -ChMMVERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTAeFw0wMTA0MDUx -NjMzMTdaFw0yMTA0MDUxNzAzMTdaMEMxCzAJBgNVBAYTAkRLMRUwEwYDVQQKEwxUREMgSW50ZXJu -ZXQxHTAbBgNVBAsTFFREQyBJbnRlcm5ldCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAxLhAvJHVYx/XmaCLDEAedLdInUaMArLgJF/wGROnN4NrXceO+YQwzho7+vvOi20j -xsNuZp+Jpd/gQlBn+h9sHvTQBda/ytZO5GhgbEaqHF1j4QeGDmUApy6mcca8uYGoOn0a0vnRrEvL -znWv3Hv6gXPU/Lq9QYjUdLP5Xjg6PEOo0pVOd20TDJ2PeAG3WiAfAzc14izbSysseLlJ28TQx5yc -5IogCSEWVmb/Bexb4/DPqyQkXsN/cHoSxNK1EKC2IeGNeGlVRGn1ypYcNIUXJXfi9i8nmHj9eQY6 -otZaQ8H/7AQ77hPv01ha/5Lr7K7a8jcDR0G2l8ktCkEiu7vmpwIDAQABo4IBJTCCASEwEQYJYIZI -AYb4QgEBBAQDAgAHMGUGA1UdHwReMFwwWqBYoFakVDBSMQswCQYDVQQGEwJESzEVMBMGA1UEChMM -VERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTENMAsGA1UEAxMEQ1JM -MTArBgNVHRAEJDAigA8yMDAxMDQwNTE2MzMxN1qBDzIwMjEwNDA1MTcwMzE3WjALBgNVHQ8EBAMC -AQYwHwYDVR0jBBgwFoAUbGQBx/2FbazI2p5QCIUItTxWqFAwHQYDVR0OBBYEFGxkAcf9hW2syNqe -UAiFCLU8VqhQMAwGA1UdEwQFMAMBAf8wHQYJKoZIhvZ9B0EABBAwDhsIVjUuMDo0LjADAgSQMA0G -CSqGSIb3DQEBBQUAA4IBAQBOQ8zR3R0QGwZ/t6T609lN+yOfI1Rb5osvBCiLtSdtiaHsmGnc540m -gwV5dOy0uaOXwTUA/RXaOYE6lTGQ3pfphqiZdwzlWqCE/xIWrG64jcN7ksKsLtB9KOy282A4aW8+ -2ARVPp7MVdK6/rtHBNcK2RYKNCn1WBPVT8+PVkuzHu7TmHnaCB4Mb7j4Fifvwm899qNLPg7kbWzb -O0ESm70NRyN/PErQr8Cv9u8btRXE64PECV90i9kR+8JWsTz4cMo0jUNAE4z9mQNUecYu6oah9jrU -Cbz0vGbMPVjQV0kK7iXiQe4T+Zs4NNEA9X7nlB38aQNiuJkFBT1reBK9sG9l ------END CERTIFICATE----- - UTN DATACorp SGC Root CA ======================== -----BEGIN CERTIFICATE----- @@ -1118,64 +835,6 @@ KuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK8CtmdWOMovsEPoMOmzbwGOQmIMOM 8CgHrTwXZoi1/baI -----END CERTIFICATE----- -NetLock Business (Class B) Root -=============================== ------BEGIN CERTIFICATE----- -MIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUxETAPBgNVBAcT -CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV -BAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQDEylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikg -VGFudXNpdHZhbnlraWFkbzAeFw05OTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYD -VQQGEwJIVTERMA8GA1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRv -bnNhZ2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5ldExvY2sg -VXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB -iQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xKgZjupNTKihe5In+DCnVMm8Bp2GQ5o+2S -o/1bXHQawEfKOml2mrriRBf8TKPV/riXiK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr -1nGTLbO/CVRY7QbrqHvcQ7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNV -HQ8BAf8EBAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZ -RUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRh -dGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQuIEEgaGl0 -ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRv -c2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUg -YXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh -c2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBz -Oi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6ZXNA -bmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhl -IHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2 -YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBj -cHNAbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06sPgzTEdM -43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXan3BukxowOR0w2y7jfLKR -stE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKSNitjrFgBazMpUIaD8QFI ------END CERTIFICATE----- - -NetLock Express (Class C) Root -============================== ------BEGIN CERTIFICATE----- -MIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUxETAPBgNVBAcT -CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV -BAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQDEytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBD -KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJ -BgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6 -dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMrTmV0TG9j -ayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzANBgkqhkiG9w0BAQEFAAOB -jQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNAOoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3Z -W3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63 -euyucYT2BDMIJTLrdKwWRMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQw -DgYDVR0PAQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEWggJN -RklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0YWxhbm9zIFN6b2xn -YWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBB -IGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBOZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1i -aXp0b3NpdGFzYSB2ZWRpLiBBIGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0 -ZWxlIGF6IGVsb2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs -ZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25sYXBqYW4gYSBo -dHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kga2VyaGV0byBheiBlbGxlbm9y -emVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4gSU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5k -IHRoZSB1c2Ugb2YgdGhpcyBjZXJ0aWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQ -UyBhdmFpbGFibGUgYXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwg -YXQgY3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmYta3UzbM2 -xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2gpO0u9f38vf5NNwgMvOOW -gyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4Fp1hBWeAyNDYpQcCNJgEjTME1A== ------END CERTIFICATE----- - XRamp Global CA Root ==================== -----BEGIN CERTIFICATE----- @@ -1930,40 +1589,6 @@ PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== -----END CERTIFICATE----- -AC Ra\xC3\xADz Certic\xC3\xA1mara S.A. -====================================== ------BEGIN CERTIFICATE----- -MIIGZjCCBE6gAwIBAgIPB35Sk3vgFeNX8GmMy+wMMA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNVBAYT -AkNPMUcwRQYDVQQKDD5Tb2NpZWRhZCBDYW1lcmFsIGRlIENlcnRpZmljYWNpw7NuIERpZ2l0YWwg -LSBDZXJ0aWPDoW1hcmEgUy5BLjEjMCEGA1UEAwwaQUMgUmHDrXogQ2VydGljw6FtYXJhIFMuQS4w -HhcNMDYxMTI3MjA0NjI5WhcNMzAwNDAyMjE0MjAyWjB7MQswCQYDVQQGEwJDTzFHMEUGA1UECgw+ -U29jaWVkYWQgQ2FtZXJhbCBkZSBDZXJ0aWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJh -IFMuQS4xIzAhBgNVBAMMGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMIICIjANBgkqhkiG9w0B -AQEFAAOCAg8AMIICCgKCAgEAq2uJo1PMSCMI+8PPUZYILrgIem08kBeGqentLhM0R7LQcNzJPNCN -yu5LF6vQhbCnIwTLqKL85XXbQMpiiY9QngE9JlsYhBzLfDe3fezTf3MZsGqy2IiKLUV0qPezuMDU -2s0iiXRNWhU5cxh0T7XrmafBHoi0wpOQY5fzp6cSsgkiBzPZkc0OnB8OIMfuuzONj8LSWKdf/WU3 -4ojC2I+GdV75LaeHM/J4Ny+LvB2GNzmxlPLYvEqcgxhaBvzz1NS6jBUJJfD5to0EfhcSM2tXSExP -2yYe68yQ54v5aHxwD6Mq0Do43zeX4lvegGHTgNiRg0JaTASJaBE8rF9ogEHMYELODVoqDA+bMMCm -8Ibbq0nXl21Ii/kDwFJnmxL3wvIumGVC2daa49AZMQyth9VXAnow6IYm+48jilSH5L887uvDdUhf -HjlvgWJsxS3EF1QZtzeNnDeRyPYL1epjb4OsOMLzP96a++EjYfDIJss2yKHzMI+ko6Kh3VOz3vCa -Mh+DkXkwwakfU5tTohVTP92dsxA7SH2JD/ztA/X7JWR1DhcZDY8AFmd5ekD8LVkH2ZD6mq093ICK -5lw1omdMEWux+IBkAC1vImHFrEsm5VoQgpukg3s0956JkSCXjrdCx2bD0Omk1vUgjcTDlaxECp1b -czwmPS9KvqfJpxAe+59QafMCAwEAAaOB5jCB4zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE -AwIBBjAdBgNVHQ4EFgQU0QnQ6dfOeXRU+Tows/RtLAMDG2gwgaAGA1UdIASBmDCBlTCBkgYEVR0g -ADCBiTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5jZXJ0aWNhbWFyYS5jb20vZHBjLzBaBggrBgEF -BQcCAjBOGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW507WFzIGRlIGVzdGUgY2VydGlmaWNhZG8gc2Ug -cHVlZGVuIGVuY29udHJhciBlbiBsYSBEUEMuMA0GCSqGSIb3DQEBBQUAA4ICAQBclLW4RZFNjmEf -AygPU3zmpFmps4p6xbD/CHwso3EcIRNnoZUSQDWDg4902zNc8El2CoFS3UnUmjIz75uny3XlesuX -EpBcunvFm9+7OSPI/5jOCk0iAUgHforA1SBClETvv3eiiWdIG0ADBaGJ7M9i4z0ldma/Jre7Ir5v -/zlXdLp6yQGVwZVR6Kss+LGGIOk/yzVb0hfpKv6DExdA7ohiZVvVO2Dpezy4ydV/NgIlqmjCMRW3 -MGXrfx1IebHPOeJCgBbT9ZMj/EyXyVo3bHwi2ErN0o42gzmRkBDI8ck1fj+404HGIGQatlDCIaR4 -3NAvO2STdPCWkPHv+wlaNECW8DYSwaN0jJN+Qd53i+yG2dIPPy3RzECiiWZIHiCznCNZc6lEc7wk -eZBWN7PGKX6jD/EpOe9+XCgycDWs2rjIdWb8m0w5R44bb5tNAlQiM+9hup4phO9OSzNHdpdqy35f -/RWmnkJDW2ZaiogN9xa5P1FlK2Zqi9E4UqLWRhH6/JocdJ6PlwsCT2TG9WjTSy3/pDceiz+/RL5h -RqGEPQgnTIEgd4kI6mdAXmwIUV80WoyWaM3X94nCHNMyAK9Sy9NgWyo6R35rMDOhYil/SrnhLecU -Iw4OGEfhefwVVdCx/CVxY3UzHCMrr1zZ7Ud3YA47Dx7SwNxkBYn8eNZcLCZDqQ== ------END CERTIFICATE----- - TC TrustCenter Class 2 CA II ============================ -----BEGIN CERTIFICATE----- @@ -1991,33 +1616,6 @@ JOzHdiEoZa5X6AeIdUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfk vQ== -----END CERTIFICATE----- -TC TrustCenter Class 3 CA II -============================ ------BEGIN CERTIFICATE----- -MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjELMAkGA1UEBhMC -REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNVBAsTGVRDIFRydXN0Q2VudGVy -IENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYw -MTEyMTQ0MTU3WhcNMjUxMjMxMjI1OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1 -c3RDZW50ZXIgR21iSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UE -AxMcVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJWHt4bNwcwIi9v8Qbxq63W -yKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+QVl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo -6SI7dYnWRBpl8huXJh0obazovVkdKyT21oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZ -uV3bOx4a+9P/FRQI2AlqukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk -2ZyqBwi1Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NXXAek0CSnwPIA1DCB -7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRydXN0Y2VudGVyLmRlL2NybC92Mi90 -Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBU -cnVzdENlbnRlciUyMENsYXNzJTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21i -SCxPVT1yb290Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u -TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlNirTzwppVMXzE -O2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8TtXqluJucsG7Kv5sbviRmEb8 -yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9 -IJqDnxrcOfHFcqMRA/07QlIp2+gB95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal -092Y+tTmBvTwtiBjS+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc -5A== ------END CERTIFICATE----- - TC TrustCenter Universal CA I ============================= -----BEGIN CERTIFICATE----- @@ -2431,7 +2029,7 @@ A2gAMGUCMGYhDBgmYFo4e1ZC4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIx AJw9SDkjOVgaFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== -----END CERTIFICATE----- -NetLock Arany (Class Gold) Főtanúsítvány +NetLock Arany (Class Gold) FÅ‘tanúsítvány ============================================ -----BEGIN CERTIFICATE----- MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G @@ -2611,22 +2209,6 @@ MCwXEGCSn1WHElkQwg9naRHMTh5+Spqtr0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3o tkYNbn5XOmeUwssfnHdKZ05phkOTOPu220+DkdRgfks+KzgHVZhepA== -----END CERTIFICATE----- -Verisign Class 3 Public Primary Certification Authority -======================================================= ------BEGIN CERTIFICATE----- -MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkGA1UEBhMCVVMx -FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5 -IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVow -XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz -IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94 -f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol -hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBABByUqkFFBky -CEHwxWsKzH4PIRnN5GfcX6kb5sroc50i2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWX -bj9T/UWZYB2oK0z5XqcJ2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/ -D/xwzoiQ ------END CERTIFICATE----- - Microsec e-Szigno Root CA 2009 ============================== -----BEGIN CERTIFICATE----- @@ -2651,28 +2233,6 @@ yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi LXpUq3DDfSJlgnCW -----END CERTIFICATE----- -E-Guven Kok Elektronik Sertifika Hizmet Saglayicisi -=================================================== ------BEGIN CERTIFICATE----- -MIIDtjCCAp6gAwIBAgIQRJmNPMADJ72cdpW56tustTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQG -EwJUUjEoMCYGA1UEChMfRWxla3Ryb25payBCaWxnaSBHdXZlbmxpZ2kgQS5TLjE8MDoGA1UEAxMz -ZS1HdXZlbiBLb2sgRWxla3Ryb25payBTZXJ0aWZpa2EgSGl6bWV0IFNhZ2xheWljaXNpMB4XDTA3 -MDEwNDExMzI0OFoXDTE3MDEwNDExMzI0OFowdTELMAkGA1UEBhMCVFIxKDAmBgNVBAoTH0VsZWt0 -cm9uaWsgQmlsZ2kgR3V2ZW5saWdpIEEuUy4xPDA6BgNVBAMTM2UtR3V2ZW4gS29rIEVsZWt0cm9u -aWsgU2VydGlmaWthIEhpem1ldCBTYWdsYXlpY2lzaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAMMSIJ6wXgBljU5Gu4Bc6SwGl9XzcslwuedLZYDBS75+PNdUMZTe1RK6UxYC6lhj71vY -8+0qGqpxSKPcEC1fX+tcS5yWCEIlKBHMilpiAVDV6wlTL/jDj/6z/P2douNffb7tC+Bg62nsM+3Y -jfsSSYMAyYuXjDtzKjKzEve5TfL0TW3H5tYmNwjy2f1rXKPlSFxYvEK+A1qBuhw1DADT9SN+cTAI -JjjcJRFHLfO6IxClv7wC90Nex/6wN1CZew+TzuZDLMN+DfIcQ2Zgy2ExR4ejT669VmxMvLz4Bcpk -9Ok0oSy1c+HCPujIyTQlCFzz7abHlJ+tiEMl1+E5YP6sOVkCAwEAAaNCMEAwDgYDVR0PAQH/BAQD -AgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ/uRLOU1fqRTy7ZVZoEVtstxNulMA0GCSqG -SIb3DQEBBQUAA4IBAQB/X7lTW2M9dTLn+sR0GstG30ZpHFLPqk/CaOv/gKlR6D1id4k9CnU58W5d -F4dvaAXBlGzZXd/aslnLpRCKysw5zZ/rTt5S/wzw9JKp8mxTq5vSR6AfdPebmvEvFZ96ZDAYBzwq -D2fK/A+JYZ1lpTzlvBNbCNvj/+27BrtqBrF6T2XGgv0enIu1De5Iu7i9qgi0+6N8y5/NkHZchpZ4 -Vwpm+Vganf2XKWDeEaaQHBkc7gGWIjQ0LpH5t8Qn0Xvmv/uARFoW5evg1Ao4vOSR49XrXMGs3xtq -fJ7lddK2l4fbzIcrQzqECK+rPNv3PGYxhrCdU3nt+CPeQuMtgvEP5fqX ------END CERTIFICATE----- - GlobalSign Root CA - R3 ======================= -----BEGIN CERTIFICATE----- @@ -3009,7 +2569,7 @@ Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI 03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= -----END CERTIFICATE----- -Certinomis - Autorité Racine +Certinomis - Autorité Racine ============================= -----BEGIN CERTIFICATE----- MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjETMBEGA1UEChMK @@ -3865,3 +3425,564 @@ TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G 3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed -----END CERTIFICATE----- + +QuoVadis Root CA 1 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE +PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm +PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 +Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN +ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l +g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV +7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX +9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f +iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg +t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI +hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 +GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct +Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP ++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh +3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa +wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 +O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 +FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV +hMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +QuoVadis Root CA 2 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh +ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY +NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t +oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o +MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l +V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo +L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ +sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD +6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh +lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI +hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K +pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 +x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz +dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X +U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw +mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD +zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN +JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr +O3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +QuoVadis Root CA 3 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 +IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL +Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe +6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 +I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U +VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 +5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi +Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM +dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt +rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI +hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS +t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ +TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du +DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib +Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD +hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX +0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW +dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 +PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +DigiCert Assured ID Root G2 +=========================== +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw +MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH +35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq +bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw +VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP +YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn +lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO +w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv +0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz +d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW +hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M +jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +DigiCert Assured ID Root G3 +=========================== +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD +VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb +RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs +KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF +UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy +YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy +1vUhZscv6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +DigiCert Global Root G2 +======================= +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx +MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ +kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO +3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV +BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM +UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu +5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr +F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U +WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH +QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ +iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +DigiCert Global Root G3 +======================= +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD +VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw +MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k +aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O +YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp +Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y +3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 +VOKa5Vt8sycX +-----END CERTIFICATE----- + +DigiCert Trusted Root G4 +======================== +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw +HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp +pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o +k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa +vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY +QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 +MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm +mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 +f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH +dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 +oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY +ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr +yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy +7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah +ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN +5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb +/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa +5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK +G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP +82Z+ +-----END CERTIFICATE----- + +WoSign +====== +-----BEGIN CERTIFICATE----- +MIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQG +EwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNVBAMTIUNlcnRpZmljYXRpb24g +QXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJ +BgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +vcqNrLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1UfcIiePyO +CbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcSccf+Hb0v1naMQFXQoOXXDX +2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2ZjC1vt7tj/id07sBMOby8w7gLJKA84X5 +KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4Mx1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR ++ScPewavVIMYe+HdVHpRaG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ez +EC8wQjchzDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDaruHqk +lWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221KmYo0SLwX3OSACCK2 +8jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvASh0JWzko/amrzgD5LkhLJuYwTKVY +yrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWvHYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0C +AwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R +8bNLtwYgFP6HEtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1 +LOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJMuYhOZO9sxXq +T2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2eJXLOC62qx1ViC777Y7NhRCOj +y+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VNg64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC +2nz4SNAzqfkHx5Xh9T71XXG68pWpdIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes +5cVAWubXbHssw1abR80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/ +EaEQPkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGcexGATVdVh +mVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+J7x6v+Db9NpSvd4MVHAx +kUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMlOtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGi +kpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWTee5Ehr7XHuQe+w== +-----END CERTIFICATE----- + +WoSign China +============ +-----BEGIN CERTIFICATE----- +MIIFWDCCA0CgAwIBAgIQUHBrzdgT/BtOOzNy0hFIjTANBgkqhkiG9w0BAQsFADBGMQswCQYDVQQG +EwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNVBAMMEkNBIOayg+mAmuagueiv +geS5pjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgwMTAwMDFaMEYxCzAJBgNVBAYTAkNOMRowGAYD +VQQKExFXb1NpZ24gQ0EgTGltaXRlZDEbMBkGA1UEAwwSQ0Eg5rKD6YCa5qC56K+B5LmmMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0EkhHiX8h8EqwqzbdoYGTufQdDTc7WU1/FDWiD+k +8H/rD195L4mx/bxjWDeTmzj4t1up+thxx7S8gJeNbEvxUNUqKaqoGXqW5pWOdO2XCld19AXbbQs5 +uQF/qvbW2mzmBeCkTVL829B0txGMe41P/4eDrv8FAxNXUDf+jJZSEExfv5RxadmWPgxDT74wwJ85 +dE8GRV2j1lY5aAfMh09Qd5Nx2UQIsYo06Yms25tO4dnkUkWMLhQfkWsZHWgpLFbE4h4TV2TwYeO5 +Ed+w4VegG63XX9Gv2ystP9Bojg/qnw+LNVgbExz03jWhCl3W6t8Sb8D7aQdGctyB9gQjF+BNdeFy +b7Ao65vh4YOhn0pdr8yb+gIgthhid5E7o9Vlrdx8kHccREGkSovrlXLp9glk3Kgtn3R46MGiCWOc +76DbT52VqyBPt7D3h1ymoOQ3OMdc4zUPLK2jgKLsLl3Az+2LBcLmc272idX10kaO6m1jGx6KyX2m ++Jzr5dVjhU1zZmkR/sgO9MHHZklTfuQZa/HpelmjbX7FF+Ynxu8b22/8DU0GAbQOXDBGVWCvOGU6 +yke6rCzMRh+yRpY/8+0mBe53oWprfi1tWFxK1I5nuPHa1UaKJ/kR8slC/k7e3x9cxKSGhxYzoacX +GKUN5AXlK8IrC6KVkLn9YDxOiT7nnO4fuwECAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFOBNv9ybQV0T6GTwp+kVpOGBwboxMA0GCSqGSIb3DQEBCwUA +A4ICAQBqinA4WbbaixjIvirTthnVZil6Xc1bL3McJk6jfW+rtylNpumlEYOnOXOvEESS5iVdT2H6 +yAa+Tkvv/vMx/sZ8cApBWNromUuWyXi8mHwCKe0JgOYKOoICKuLJL8hWGSbueBwj/feTZU7n85iY +r83d2Z5AiDEoOqsuC7CsDCT6eiaY8xJhEPRdF/d+4niXVOKM6Cm6jBAyvd0zaziGfjk9DgNyp115 +j0WKWa5bIW4xRtVZjc8VX90xJc/bYNaBRHIpAlf2ltTW/+op2znFuCyKGo3Oy+dCMYYFaA6eFN0A +kLppRQjbbpCBhqcqBT/mhDn4t/lXX0ykeVoQDF7Va/81XwVRHmyjdanPUIPTfPRm94KNPQx96N97 +qA4bLJyuQHCH2u2nFoJavjVsIE4iYdm8UXrNemHcSxH5/mc0zy4EZmFcV5cjjPOGG0jfKq+nwf/Y +jj4Du9gqsPoUJbJRa4ZDhS4HIxaAjUz7tGM7zMN07RujHv41D198HRaG9Q7DlfEvr10lO1Hm13ZB +ONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Leie2uPAmvylezkolwQOQv +T8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR12KvxAmLBsX5VYc8T1yaw15zLKYs4SgsO +kI26oQ== +-----END CERTIFICATE----- + +COMODO RSA Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn +dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ +FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+ +5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG +x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX +2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL +OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3 +sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C +GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5 +WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt +rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+ +nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg +tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW +sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp +pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA +zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq +ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52 +7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I +LaZRfyHBNVOFBkpdn627G190 +-----END CERTIFICATE----- + +USERTrust RSA Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz +0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j +Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn +RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O ++T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq +/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE +Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM +lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 +yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ +eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW +FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ +7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ +Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM +8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi +FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi +yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c +J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw +sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx +Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +USERTrust ECC Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 +0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez +nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV +HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB +HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu +9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R4 +=========================== +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprl +OQcJFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAwDgYDVR0P +AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61FuOJAf/sKbvu+M8k8o4TV +MAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGXkPoUVy0D7O48027KqGx2vKLeuwIgJ6iF +JzWbVsaj8kfSt24bAgAXqmemFZHe+pTsewv4n4Q= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R5 +=========================== +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 +SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS +h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx +uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 +yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +Staat der Nederlanden Root CA - G3 +================================== +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +Um9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloXDTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMC +TkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5l +ZGVybGFuZGVuIFJvb3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4y +olQPcPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WWIkYFsO2t +x1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqXxz8ecAgwoNzFs21v0IJy +EavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFyKJLZWyNtZrVtB0LrpjPOktvA9mxjeM3K +Tj215VKb8b475lRgsGYeCasH/lSJEULR9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUur +mkVLoR9BvUhTFXFkC4az5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU5 +1nus6+N86U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7Ngzp +07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHPbMk7ccHViLVlvMDo +FxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXtBznaqB16nzaeErAMZRKQFWDZJkBE +41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTtXUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleu +yjWcLhL75LpdINyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwpLiniyMMB8jPq +KqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8Ipf3YF3qKS9Ysr1YvY2WTxB1 +v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixpgZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA +8KCWAg8zxXHzniN9lLf9OtMJgwYh/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b +8KKaa8MFSu1BYBQw0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0r +mj1AfsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq4BZ+Extq +1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR1VmiiXTTn74eS9fGbbeI +JG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/QFH1T/U67cjF68IeHRaVesd+QnGTbksV +tzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM94B7IWcnMFk= +-----END CERTIFICATE----- + +Staat der Nederlanden EV Root CA +================================ +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +RVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0yMjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5M +MR4wHAYDVQQKDBVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRl +cmxhbmRlbiBFViBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkk +SzrSM4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nCUiY4iKTW +O0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3dZ//BYY1jTw+bbRcwJu+r +0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46prfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8 +Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13lpJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gV +XJrm0w912fxBmJc+qiXbj5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr +08C+eKxCKFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS/ZbV +0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0XcgOPvZuM5l5Tnrmd +74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH1vI4gnPah1vlPNOePqc7nvQDs/nx +fRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrPpx9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwa +ivsnuL8wbqg7MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u2dfOWBfoqSmu +c0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHSv4ilf0X8rLiltTMMgsT7B/Zq +5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTCwPTxGfARKbalGAKb12NMcIxHowNDXLldRqAN +b/9Zjr7dn3LDWyvfjFvO5QxGbJKyCqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tN +f1zuacpzEPuKqf2evTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi +5Dp6Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIaGl6I6lD4 +WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeLeG9QgkRQP2YGiqtDhFZK +DyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGy +eUN51q1veieQA6TqJIc/2b3Z6fJfUEkc7uzXLg== +-----END CERTIFICATE----- + +IdenTrust Commercial Root CA 1 +============================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS +b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES +MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB +IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld +hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ +mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi +1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C +XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl +3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy +NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV +WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg +xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix +uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI +hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg +ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt +ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV +YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX +feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro +kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe +2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz +Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R +cGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +IdenTrust Public Sector Root CA 1 +================================= +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv +ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV +UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS +b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy +P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 +Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI +rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf +qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS +mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn +ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh +LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v +iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL +4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B +Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw +DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A +mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt +GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt +m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx +NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 +Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI +ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC +ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ +3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy +bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug +b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw +HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT +DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx +OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP +/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz +HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU +s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y +TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx +AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 +0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z +iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi +nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ +vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO +e4pIb4tF9g== +-----END CERTIFICATE----- + +Entrust Root Certification Authority - EC1 +========================================== +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx +FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn +YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw +FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs +LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg +dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy +AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef +9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h +vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 +kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +CFCA EV ROOT +============ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE +CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB +IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw +MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD +DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV +BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD +7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN +uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW +ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 +xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f +py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K +gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol +hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ +tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf +BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q +ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua +4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG +E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX +BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn +aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy +PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX +kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C +ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- diff --git a/dist/deb.rake b/dist/deb.rake deleted file mode 100644 index ad9a13571..000000000 --- a/dist/deb.rake +++ /dev/null @@ -1,32 +0,0 @@ -file pkg("/apt-#{version}/heroku-#{version}.deb") => distribution_files("deb") do |t| - mkchdir(File.dirname(t.name)) do - mkchdir("usr/local/heroku") do - assemble_distribution - assemble_gems - assemble resource("deb/heroku"), "bin/heroku", 0755 - end - - assemble resource("deb/control"), "control" - assemble resource("deb/postinst"), "postinst" - - sh "tar czvf data.tar.gz usr/local/heroku --owner=root --group=root" - sh "tar czvf control.tar.gz control postinst" - - File.open("debian-binary", "w") do |f| - f.puts "2.0" - end - - deb = File.basename(t.name) - - sh "ar -r #{t.name} debian-binary control.tar.gz data.tar.gz" - end -end - -desc "Build a .deb package" -task "deb:build" => pkg("/apt-#{version}/heroku-#{version}.deb") - -desc "Remove build artifacts for .deb" -task "deb:clean" do - clean pkg("heroku-#{version}.deb") - FileUtils.rm_rf("pkg/apt-#{version}") if Dir.exists?("pkg/apt-#{version}") -end diff --git a/dist/gem.rake b/dist/gem.rake deleted file mode 100644 index 293b4d249..000000000 --- a/dist/gem.rake +++ /dev/null @@ -1,16 +0,0 @@ -file pkg("heroku-#{version}.gem") => distribution_files("gem") do |t| - sh "gem build heroku.gemspec" - sh "mv heroku-#{version}.gem #{t.name}" -end - -task "gem:build" => pkg("heroku-#{version}.gem") - -task "gem:clean" do - clean pkg("heroku-#{version}.gem") -end - -task "gem:release" => "gem:build" do |t| - sh "gem push #{pkg("heroku-#{version}.gem")}" - sh "git tag v#{version}" - sh "git push origin master --tags" -end diff --git a/dist/manifest.rake b/dist/manifest.rake deleted file mode 100644 index 0d3b46e0e..000000000 --- a/dist/manifest.rake +++ /dev/null @@ -1,9 +0,0 @@ -task "manifest:update" do - tempdir do |dir| - File.open("VERSION", "w") do |file| - file.puts version - end - puts "Current version: #{version}" - store "#{dir}/VERSION", "heroku-client/VERSION" - end -end diff --git a/dist/pkg.rake b/dist/pkg.rake deleted file mode 100644 index 01142f7a3..000000000 --- a/dist/pkg.rake +++ /dev/null @@ -1,56 +0,0 @@ -require "erb" - -file pkg("heroku-#{version}.pkg") => distribution_files("pkg") do |t| - tempdir do |dir| - mkchdir("heroku-client") do - assemble_distribution - assemble_gems - assemble resource("pkg/heroku"), "bin/heroku", 0755 - end - - kbytes = %x{ du -ks heroku-client | cut -f 1 } - num_files = %x{ find heroku-client | wc -l } - - mkdir_p "pkg" - mkdir_p "pkg/Resources" - mkdir_p "pkg/heroku-client.pkg" - - dist = File.read(resource("pkg/Distribution.erb")) - dist = ERB.new(dist).result(binding) - File.open("pkg/Distribution", "w") { |f| f.puts dist } - - dist = File.read(resource("pkg/PackageInfo.erb")) - dist = ERB.new(dist).result(binding) - File.open("pkg/heroku-client.pkg/PackageInfo", "w") { |f| f.puts dist } - - mkdir_p "pkg/heroku-client.pkg/Scripts" - cp resource("pkg/postinstall"), "pkg/heroku-client.pkg/Scripts/postinstall" - chmod 0755, "pkg/heroku-client.pkg/Scripts/postinstall" - - sh %{ mkbom -s heroku-client pkg/heroku-client.pkg/Bom } - - Dir.chdir("heroku-client") do - sh %{ pax -wz -x cpio . > ../pkg/heroku-client.pkg/Payload } - end - - sh %{ curl http://heroku-toolbelt.s3.amazonaws.com/ruby.pkg -o ruby.pkg } - sh %{ pkgutil --expand ruby.pkg ruby } - mv "ruby/ruby-1.9.3-p194.pkg", "pkg/ruby.pkg" - - sh %{ pkgutil --flatten pkg heroku-#{version}.pkg } - - cp_r "heroku-#{version}.pkg", t.name - end -end - -desc "build pkg" -task "pkg:build" => pkg("heroku-#{version}.pkg") - -desc "clean pkg" -task "pkg:clean" do - clean pkg("heroku-#{version}.pkg") -end - -task "pkg:release" do - raise "pkg:release moved to toolbelt repo" -end diff --git a/dist/resources/deb/control b/dist/resources/deb/control deleted file mode 100644 index 5672073b7..000000000 --- a/dist/resources/deb/control +++ /dev/null @@ -1,8 +0,0 @@ -Package: heroku -Version: <%= version %> -Section: main -Priority: standard -Architecture: all -Depends: ruby2.1|ruby2.0|ruby1.9.1, libopenssl-ruby2.1|libopenssl-ruby2.0|libopenssl-ruby1.9.1, libreadline-ruby2.1|libreadline-ruby2.0|libreadline-ruby1.9.1, libssl0.9.8 (>= 0.9.8k) | libssl1.0.0 -Maintainer: Heroku -Description: Client library and CLI to deploy apps on Heroku. diff --git a/dist/resources/deb/heroku-release-key.txt b/dist/resources/deb/heroku-release-key.txt deleted file mode 100644 index e22a1c2a8..000000000 --- a/dist/resources/deb/heroku-release-key.txt +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.11 (Darwin) - -mQENBE5SfAEBCADLp056ZgfdtAMXLWpEuL9zY+dIHIY5qLQcDmUivjHLVE4l3Bi3 -Mn570K0W9rfk7fHBPEO2XJEDdjk8Bg6mWTAeGjdfZgZaL+qO9NjqQ5QmVR+vgp7s -yxJYlfY+JYTZvl/JiDWGhuPHSPggXILCMf3SpqWMHGPqe/3RAK+CHCNv/94uaoS4 -vi4HQT+k4sRceiM8WqkSRYSoc7rzdDejZn+InCYFfR56VeSFF4G4I6neZs/q5T9d -Ty2i5d0gZLaX/Iqc+3Dy0vDKClc0HUQJ6ajDPuUqKLHFUpqyuwfJij60+C3GMi8K -ckRPti31EPFVzq3GPHU+GqA+e9j84WHr4uJ5ABEBAAG0L0hlcm9rdSBSZWxlYXNl -IEVuZ2luZWVyaW5nIDxyZWxlYXNlQGhlcm9rdS5jb20+iQE4BBMBAgAiBQJOUnwB -AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDJJ+vgDxsFIChECAC9h4Ay -Nx4AQFu85cjR9rijyBflPeVqi7Xhzd7IvLg2+kZSexlb2oidj7iVSMy+vy5tG9g9 -8Az/JqMCVjcZ7ltn60OGU8gIYpJqt6VmH3vfJBxXu/Sm9tym3UCYGVvMAN5Oq6yB -HlQkQ8F3p0cW69PmF+fibkgo9RE0EYlBIt2rUHNilTGFS6vXGr5reFFp3/rRHq3k -bixnUwFSqNujJgnBKDPwtSYKc4pMpnhuv88xEpLH7vU8NLXQZMitKQguV8XEmcsu -43LXlsx5uVr239/XNW+h412gIHFDSzB/YuLWlVUXMfquC96z/wxMqWWZyskDNgr0 -WDdMgzK6CUfXSqQhuQENBE5SfAEBCADbnGKcXpdVauQpINQLtRnrT0BJIrIo1Yxv -LQRb3G7RU+Eq6aHXwk9fSKa6nEv9RsmqiW874yODnr0d/DTUWMHT+jRvPHm1wlbE -pGR1aPSo7GgkSUdaT6CVBN3JWZ2kVJGqohNoJMYbfVaWd/kpa/LiMFWzS8LfWT2K -xiO2vIh4qBfeRCGR7s8rADCHuHJ0eibADrgqcRfdPrChB1JiYLeTdV4yRmSzJ7TM -zWX7OVpGfIFLbCw9NeN65pI9ePs2mSPM7DYkhhKSXWMwJNXFzn1blOGiwAwKb48P -a/QpE6TG3PQzbYyTTP0Td1XgKAHcprvbc89a/nAk3a+PJQ/MqvDzABEBAAGJAR8E -GAECAAkFAk5SfAECGwwACgkQySfr4A8bBSD4mAgAnCT5WRiDl0259Px9Z9J9Wk8Z -SxugDct2Yhzca4aw1Ou4cfaIFCDXzFlBzSJfqk0HoVhp9r2gzEPUCKnSjRDyxaMo -wZCUtqigBua+z4NB4AWgeOl/2S06I2ki1K7pfl4piYcHtEThHamnhVPJ2Hi6HsHq -mUU+8SxleHE4GCXmKkuvxelUq9jrhHikIkm1RoqFOPb9zV3WRy4YzVHQSYfHmfk0 -9kXlM/CS0sfNv2UKCX+5e6eFIZv0rdtpp6VEh0tsFmsIClY6Z9MX7bgp8MnUJpyk -OeIzOzQgkb4aeT0Whl+EPcTeDZfqIhVBoNXupUanmWNppFcMngxfqG2NGi1vvQ== -=aUAq ------END PGP PUBLIC KEY BLOCK----- diff --git a/dist/rpm.rake b/dist/rpm.rake deleted file mode 100644 index 96b12edaf..000000000 --- a/dist/rpm.rake +++ /dev/null @@ -1,35 +0,0 @@ -# TODO -# * signing -# * yum repository for updates -# * foreman - -file pkg("/yum-#{version}/heroku-#{version}.rpm") => "deb:build" do |t| - mkchdir(File.dirname(t.name)) do - deb = pkg("/apt-#{version}/heroku-#{version}.deb") - sh "alien --keep-version --scripts --generate --to-rpm #{deb}" - - spec = "heroku-#{version}/heroku-#{version}-1.spec" - spec_contents = File.read(spec) - File.open(spec, "w") do |f| - # Add ruby requirement, remove benchmark file with ugly filename - f.puts spec_contents.sub(/\n\n/m, "\nRequires: ruby\nBuildArch: noarch\n\n"). - sub(/^.+has_key-vs-hash\[key\].+$/, ""). - sub(/^License: .*/, "License: MIT\nURL: http://heroku.com\n"). - sub(/^%description/, "%description\nClient library and CLI to deploy apps on Heroku.") - end - sh "sed -i s/ruby1.9.1/ruby/ heroku-#{version}/usr/local/heroku/bin/heroku" - - chdir("heroku-#{version}") do - sh "rpmbuild --buildroot $PWD -bb heroku-#{version}-1.spec" - end - end -end - -desc "Build an .rpm package" -task "rpm:build" => pkg("/yum-#{version}/heroku-#{version}.rpm") - -desc "Remove build artifacts for .rpm" -task "rpm:clean" do - clean pkg("heroku-#{version}.rpm") - FileUtils.rm_rf("pkg/yum-#{version}") if Dir.exists?("pkg/yum-#{version}") -end diff --git a/dist/tgz.rake b/dist/tgz.rake deleted file mode 100644 index 315c192e1..000000000 --- a/dist/tgz.rake +++ /dev/null @@ -1,26 +0,0 @@ -file pkg("heroku-#{version}.tgz") => distribution_files("tgz") do |t| - tempdir do |dir| - mkchdir("heroku-client") do - assemble_distribution - assemble_gems - assemble resource("tgz/heroku"), "bin/heroku", 0755 - end - - sh "chmod -R go+r heroku-client" - sh "sudo chown -R 0:0 heroku-client" - sh "tar czf #{t.name} heroku-client" - sh "sudo chown -R $(whoami) heroku-client" - end -end - -task "tgz:build" => pkg("heroku-#{version}.tgz") - -task "tgz:clean" do - clean pkg("heroku-#{version}.tgz") -end - -task "tgz:release" => "tgz:build" do |t| - store pkg("heroku-#{version}.tgz"), "heroku-client/heroku-client-#{version}.tgz" - store pkg("heroku-#{version}.tgz"), "heroku-client/heroku-client-beta.tgz" if beta? - store pkg("heroku-#{version}.tgz"), "heroku-client/heroku-client.tgz" unless beta? -end diff --git a/dist/zip.rake b/dist/zip.rake deleted file mode 100644 index 9f7252c23..000000000 --- a/dist/zip.rake +++ /dev/null @@ -1,40 +0,0 @@ -require "zip/zip" - -file pkg("heroku-#{version}.zip") => distribution_files("zip") do |t| - tempdir do |dir| - mkchdir("heroku-client") do - assemble_distribution - assemble_gems - Zip::ZipFile.open(t.name, Zip::ZipFile::CREATE) do |zip| - Dir["**/*"].each do |file| - zip.add(file, file) { true } - end - end - end - end -end - -file pkg("heroku-#{version}.zip.sha256") => pkg("heroku-#{version}.zip") do |t| - File.open(t.name, "w") do |file| - file.puts Digest::SHA256.file(t.prerequisites.first).hexdigest - end -end - -task "zip:build" => pkg("heroku-#{version}.zip") -task "zip:sign" => pkg("heroku-#{version}.zip.sha256") - -def zip_signature - File.read(pkg("heroku-#{version}.zip.sha256")).chomp -end - -task "zip:clean" do - clean pkg("heroku-#{version}.zip") -end - -task "zip:release" => %w( zip:build zip:sign ) do |t| - store pkg("heroku-#{version}.zip"), "heroku-client/heroku-client-#{version}.zip" - store pkg("heroku-#{version}.zip"), "heroku-client/heroku-client-beta.zip" if beta? - store pkg("heroku-#{version}.zip"), "heroku-client/heroku-client.zip" unless beta? - - sh "heroku config:add UPDATE_HASH=#{zip_signature} -a toolbelt" -end diff --git a/heroku.gemspec b/heroku.gemspec index 650db705e..c5a258f6a 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -12,17 +12,19 @@ Gem::Specification.new do |gem| gem.description = "Client library and command-line tool to deploy and manage apps on Heroku." gem.executables = "heroku" gem.license = "MIT" + gem.required_ruby_version = ">= 1.9.0" gem.post_install_message = <<-MESSAGE ! The `heroku` gem has been deprecated and replaced with the Heroku Toolbelt. ! Download and install from: https://toolbelt.heroku.com ! For API access, see: https://github.com/heroku/heroku.rb MESSAGE - gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(License|README|bin/|data/|ext/|lib/|spec/|test/)} } + gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(LICENSE|README|bin/|data/|ext/|lib/|spec/|test/)} } - gem.add_dependency "heroku-api", "= 0.3.17" + gem.add_dependency "heroku-api", ">= 0.3.19" gem.add_dependency "launchy", ">= 0.3.2" - gem.add_dependency "netrc", "~> 0.7.7" - gem.add_dependency "rest-client", "~> 1.6.1" - gem.add_dependency "rubyzip" + gem.add_dependency "netrc", ">= 0.10.0" + gem.add_dependency "rest-client", ">= 1.6.0" + gem.add_dependency "rubyzip", ">= 0.9.9" + gem.add_dependency "multi_json", ">= 1.10" end diff --git a/lib/heroku.rb b/lib/heroku.rb index 6141d06e5..a01417932 100644 --- a/lib/heroku.rb +++ b/lib/heroku.rb @@ -1,8 +1,8 @@ -require "heroku/client" require "heroku/updater" require "heroku/version" module Heroku + @@app_name = nil USER_AGENT = "heroku-gem/#{Heroku::VERSION} (#{RUBY_PLATFORM}) ruby/#{RUBY_VERSION}" @@ -14,4 +14,11 @@ def self.user_agent=(agent) @@user_agent = agent end + def self.app_name + @@app_name + end + + def self.app_name=(app_name) + @@app_name = app_name + end end diff --git a/lib/heroku/api/apps_v3.rb b/lib/heroku/api/apps_v3.rb new file mode 100644 index 000000000..b0e865845 --- /dev/null +++ b/lib/heroku/api/apps_v3.rb @@ -0,0 +1,27 @@ +module Heroku + class API + def get_app_buildpacks_v3(app) + headers = { 'Accept' => 'application/vnd.heroku+json; version=3' } + request( + :expects => [ 200, 206 ], + :headers => headers, + :method => :get, + :path => "/apps/#{app}/buildpack-installations" + ) + end + + def put_app_buildpacks_v3(app, body={}) + headers = { + 'Accept' => 'application/vnd.heroku+json; version=3', + 'Content-Type' => 'application/json' + } + request( + :expects => 200, + :headers => headers, + :method => :put, + :path => "/apps/#{app}/buildpack-installations", + :body => Heroku::Helpers.json_encode(body) + ) + end + end +end diff --git a/lib/heroku/api/releases_v3.rb b/lib/heroku/api/releases_v3.rb deleted file mode 100644 index 41a4d0815..000000000 --- a/lib/heroku/api/releases_v3.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Heroku - class API - def get_releases_v3(app, range=nil) - headers = { 'Accept' => 'application/vnd.heroku+json; version=3' } - headers.merge!('Range' => range) if range - request( - :expects => [ 200, 206 ], - :headers => headers, - :method => :get, - :path => "/apps/#{app}/releases" - ) - end - - def post_release_v3(app, slug_id, description=nil) - body = { 'slug' => slug_id } - body.merge!('description' => description) if description - request( - :expects => 201, - :headers => { - 'Accept' => 'application/vnd.heroku+json; version=3', - 'Content-Type' => 'application/json' - }, - :method => :post, - :path => "/apps/#{app}/releases", - :body => Heroku::Helpers.json_encode(body) - ) - end - end -end diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index d759a9188..9c1e795ee 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -9,11 +9,11 @@ class Heroku::Auth class << self include Heroku::Helpers - attr_accessor :credentials, :two_factor_code + attr_accessor :credentials def api @api ||= begin - require("heroku-api") + debug "Using API with key: #{password[0,6]}..." api = Heroku::API.new(default_params.merge(:api_key => password)) def api.request(params, &block) @@ -54,6 +54,10 @@ def default_host "heroku.com" end + def http_git_host + ENV['HEROKU_HTTP_GIT_HOST'] || "git.#{host}" + end + def git_host ENV['HEROKU_GIT_HOST'] || host end @@ -62,6 +66,10 @@ def host ENV['HEROKU_HOST'] || default_host end + def subdomains + %w(api git) + end + def reauthorize @credentials = ask_for_and_save_credentials end @@ -74,10 +82,16 @@ def password # :nodoc: get_credentials[1] end - def api_key(user = get_credentials[0], password = get_credentials[1]) - require("heroku-api") - api = Heroku::API.new(default_params) - api.post_login(user, password).body["api_key"] + def api_key(user=get_credentials[0], password=get_credentials[1]) + @api ||= Heroku::API.new(default_params) + api_key = @api.post_login(user, password).body["api_key"] + @api = nil + api_key + rescue Heroku::API::Errors::Forbidden => e + if e.response.headers.has_key?("Heroku-Two-Factor-Required") + ask_for_second_factor + retry + end rescue Heroku::API::Errors::Unauthorized => e id = json_decode(e.response.body)["id"] raise if id != "invalid_two_factor_code" @@ -86,11 +100,6 @@ def api_key(user = get_credentials[0], password = get_credentials[1]) display "Please check your code was typed correctly and that your" display "authenticator's time keeping is accurate." exit 1 - rescue Heroku::API::Errors::Forbidden => e - if e.response.headers.has_key?("Heroku-Two-Factor-Required") - ask_for_second_factor - retry - end end def get_credentials # :nodoc: @@ -102,8 +111,9 @@ def delete_credentials FileUtils.rm_f(legacy_credentials_path) end if netrc - netrc.delete("api.#{host}") - netrc.delete("code.#{host}") + subdomains.each do |sub| + netrc.delete("#{sub}.#{host}") + end netrc.save end @api, @client, @credentials = nil, nil @@ -131,11 +141,13 @@ def netrc # :nodoc: @netrc ||= begin File.exists?(netrc_path) && Netrc.read(netrc_path) rescue => error - if error.message =~ /^Permission bits for/ - perm = File.stat(netrc_path).mode & 0777 - abort("Permissions #{perm} for '#{netrc_path}' are too open. You should run `chmod 0600 #{netrc_path}` so that your credentials are NOT accessible by others.") + case error.message + when /^Permission bits for/ + abort("#{error.message}.\nYou should run `chmod 0600 #{netrc_path}` so that your credentials are NOT accessible by others.") + when /EACCES/ + error("Error reading #{netrc_path}\n#{error.message}\nMake sure this user can read/write this file.") else - raise error + error("Error reading #{netrc_path}\n#{error.message}\nYou may need to delete this file and run `heroku login` to recreate it.") end end end @@ -154,15 +166,17 @@ def read_credentials # read netrc credentials if they exist if netrc + netrc_host = full_host_uri.host + # force migration of long api tokens (80 chars) to short ones (40) # #write_credentials rewrites both api.* and code.* - credentials = netrc["api.#{host}"] + credentials = netrc[netrc_host] if credentials && credentials[1].length > 40 @credentials = [ credentials[0], credentials[1][0,40] ] write_credentials end - netrc["api.#{host}"] + netrc[netrc_host] end end end @@ -173,8 +187,9 @@ def write_credentials unless running_on_windows? FileUtils.chmod(0600, netrc_path) end - netrc["api.#{host}"] = self.credentials - netrc["code.#{host}"] = self.credentials + subdomains.each do |sub| + netrc["#{sub}.#{host}"] = self.credentials + end netrc.save end @@ -198,16 +213,23 @@ def ask_for_credentials print "Password (typing will be hidden): " password = running_on_windows? ? ask_for_password_on_windows : ask_for_password + HTTPInstrumentor.filter_parameter(password) [user, api_key(user, password)] end def ask_for_second_factor - display "Two-factor code: ", false - @two_factor_code = ask - @two_factor_code = nil if @two_factor_code == "" - @api = nil # reset it - @two_factor_code + $stderr.print "Two-factor code: " + api.second_factor = ask + end + + def preauth + if Heroku.app_name + second_factor = ask_for_second_factor + api.request(:method => :put, + :path => "/apps/#{Heroku.app_name}/pre-authorizations", + :headers => {"Heroku-Two-Factor-Code" => second_factor}) + end end def ask_for_password_on_windows @@ -240,45 +262,49 @@ def ask_for_password end def ask_for_and_save_credentials - require("heroku-api") # for the errors - begin - @credentials = ask_for_credentials - write_credentials - check - rescue Heroku::API::Errors::NotFound, Heroku::API::Errors::Unauthorized => e - delete_credentials - display "Authentication failed." - retry if retry_login? - exit 1 - rescue Exception => e - delete_credentials - raise e - end - check_for_associated_ssh_key unless Heroku::Command.current_command == "keys:add" + warn "WARNING: heroku-accounts plugin is installed. This plugin is known to have problems with HTTP Git." if defined?(Heroku::Command::Accounts) + @credentials = ask_for_credentials + debug "Logged in as #{@credentials[0]} with key: #{@credentials[1][0,6]}..." + write_credentials + check @credentials - end - - def check_for_associated_ssh_key - if api.get_keys.body.empty? - associate_or_generate_ssh_key - end + rescue Heroku::API::Errors::NotFound, Heroku::API::Errors::Unauthorized => e + delete_credentials + display "Authentication failed." + warn "WARNING: HEROKU_API_KEY is set to an invalid key." if ENV['HEROKU_API_KEY'] + retry if retry_login? + exit 1 + rescue => e + delete_credentials + raise e end def associate_or_generate_ssh_key - public_keys = Dir.glob("#{home_directory}/.ssh/*.pub").sort - - case public_keys.length - when 0 then - display "Could not find an existing public key." + unless File.exists?("#{home_directory}/.ssh/id_rsa.pub") + display "Could not find an existing public key at ~/.ssh/id_rsa.pub" display "Would you like to generate one? [Yn] ", false - unless ask.strip.downcase == "n" + unless ask.strip.downcase =~ /^n/ display "Generating new SSH public key." - generate_ssh_key("id_rsa") + generate_ssh_key("#{home_directory}/.ssh/id_rsa") associate_key("#{home_directory}/.ssh/id_rsa.pub") + return end - when 1 then - display "Found existing public key: #{public_keys.first}" - associate_key(public_keys.first) + end + + chosen = ssh_prompt + associate_key(chosen) if chosen + end + + def ssh_prompt + public_keys = Dir.glob("#{home_directory}/.ssh/*.pub").sort + case public_keys.length + when 0 + error("No SSH keys found") + return nil + when 1 + display "Found an SSH public key at #{public_keys.first}" + display "Would you like to upload it to Heroku? [Yn] ", false + return ask.strip.downcase =~ /^n/ ? nil : public_keys.first else display "Found the following SSH public keys:" public_keys.each_with_index do |key, index| @@ -290,19 +316,14 @@ def associate_or_generate_ssh_key if choice == -1 || chosen.nil? error("Invalid choice") end - associate_key(chosen) + return chosen end end def generate_ssh_key(keyfile) - ssh_dir = File.join(home_directory, ".ssh") - unless File.exists?(ssh_dir) - FileUtils.mkdir_p ssh_dir - unless running_on_windows? - File.chmod(0700, ssh_dir) - end - end - output = `ssh-keygen -t rsa -N "" -f \"#{home_directory}/.ssh/#{keyfile}\" 2>&1` + ssh_dir = File.dirname(keyfile) + FileUtils.mkdir_p ssh_dir, :mode => 0700 + output = `ssh-keygen -t rsa -N "" -f \"#{keyfile}\" 2>&1` if ! $?.success? error("Could not generate key: #{output}") end @@ -324,42 +345,41 @@ def retry_login? @login_attempts < 3 end - def verified_hosts - %w( heroku.com heroku-shadow.com ) - end - def base_host(host) parts = URI.parse(full_host(host)).host.split(".") return parts.first if parts.size == 1 parts[-2..-1].join(".") end - def full_host(host) - (host =~ /^http/) ? host : "https://api.#{host}" + def full_host(*args) + # backwards compat for when this took an arg + h = args.first || host + (h =~ /^http/) ? h : "https://api.#{h}" + end + + def full_host_uri + URI.parse(full_host) end def verify_host?(host) - hostname = base_host(host) - verified = verified_hosts.include?(hostname) - verified = false if ENV["HEROKU_SSL_VERIFY"] == "disable" - verified + return false if ENV["HEROKU_SSL_VERIFY"] == "disable" + base_host(host) == "heroku.com" end protected def default_params - uri = URI.parse(full_host(host)) - headers = { 'User-Agent' => Heroku.user_agent } - if two_factor_code - headers.merge!("Heroku-Two-Factor-Code" => two_factor_code) - end - { - :headers => headers, + uri = full_host_uri + params = { + :headers => {'User-Agent' => Heroku.user_agent}, :host => uri.host, :port => uri.port.to_s, :scheme => uri.scheme, :ssl_verify_peer => verify_host?(host) } + params[:instrumentor] = HTTPInstrumentor if debugging? + + params end end end diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index cb0a734d8..ec144dfc6 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -1,51 +1,43 @@ +if RUBY_VERSION < '1.9.0' # this is a string comparison, but it should work for any old ruby version + $stderr.puts "Heroku Toolbelt requires Ruby 1.9+." + exit 1 +end + load('heroku/helpers.rb') # reload helpers after possible inject_loadpath load('heroku/updater.rb') # reload updater after possible inject_loadpath -require "heroku" -require "heroku/command" -require "heroku/helpers" - -# workaround for rescue/reraise to define errors in command.rb failing in 1.8.6 -if RUBY_VERSION =~ /^1.8.6/ - require('heroku-api') - require('rest_client') -end - -begin - # attempt to load the JSON parser bundled with ruby for multi_json - # we're doing this because several users apparently have gems broken - # due to OS upgrades. see: https://github.com/heroku/heroku/issues/932 - require 'json' -rescue LoadError - # let multi_json fallback to yajl/oj/okjson -end +require 'heroku' +require 'heroku/jsplugin' +require 'heroku/rollbar' +require 'json' class Heroku::CLI extend Heroku::Helpers def self.start(*args) - begin - if $stdin.isatty - $stdin.sync = true - end - if $stdout.isatty - $stdout.sync = true - end - command = args.shift.strip rescue "help" - Heroku::Command.load - Heroku::Command.run(command, args) - rescue Interrupt => e - `stty icanon echo` - if ENV["HEROKU_DEBUG"] - styled_error(e) - else - error("Command cancelled.") - end - rescue => error - styled_error(error) - exit(1) + $stdin.sync = true if $stdin.isatty + $stdout.sync = true if $stdout.isatty + Heroku::Updater.warn_if_updating + command = args.shift.strip rescue "help" + Heroku::JSPlugin.try_takeover(command, args) if Heroku::JSPlugin.setup? + require 'heroku/command' + Heroku::Git.check_git_version + Heroku::Command.load + Heroku::Command.run(command, args) + Heroku::Updater.autoupdate + rescue Errno::EPIPE => e + error(e.message) + rescue Interrupt => e + `stty icanon echo` unless running_on_windows? + if ENV["HEROKU_DEBUG"] + styled_error(e) + else + error("Command cancelled.", false) end + rescue => error + styled_error(error) + exit(1) end end diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index 18594ac0a..81e1871f5 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -1,4 +1,3 @@ -require 'rexml/document' require 'uri' require 'time' require 'heroku/auth' @@ -32,7 +31,6 @@ def self.gem_version_string attr_accessor :host, :user, :password def initialize(user, password, host=Heroku::Auth.host) - require 'rest_client' @user = user @password = password @host = host @@ -225,7 +223,7 @@ def remove_all_keys delete("/user/keys").to_s end - # Retreive ps list for the given app name. + # Retrieve ps list for the given app name. def ps(app_name) deprecate # 07/31/2012 json_decode get("/apps/#{app_name}/ps", :accept => 'application/json').to_s @@ -349,7 +347,6 @@ class Service attr_accessor :attached def initialize(client, app) - require 'rest_client' @client = client @app = app end @@ -435,7 +432,6 @@ class AppCrashed < RuntimeError; end # support for console sessions class ConsoleSession def initialize(id, app, client) - require 'rest_client' @id = id; @app = app; @client = client end def run(cmd) @@ -453,7 +449,7 @@ def console(app_name, cmd=nil) else run_console_command("/apps/#{app_name}/console", cmd) end - rescue RestClient::BadGateway => e + rescue RestClient::BadGateway raise(AppCrashed, <<-ERROR) Unable to attach to a dyno to open a console session. Your application may have crashed. @@ -482,6 +478,7 @@ def run_console_command(url, command, prefix=nil) def read_logs(app_name, options=[]) query = "&" + options.join("&") unless options.empty? url = get("/apps/#{app_name}/logs?logplex=true#{query}").to_s + debug "Reading logs from: #{url}" if url == 'Use old logs' puts get("/apps/#{app_name}/logs").to_s else @@ -528,7 +525,8 @@ def read_logs(app_name, options=[]) end end end - rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError + rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => exception + debug "Error connecting to logging service: #{exception}" error("Could not connect to logging service") rescue Timeout::Error, EOFError error("\nRequest timed out") @@ -623,7 +621,7 @@ def process(method, uri, extra_headers={}, payload=nil) rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError host = URI.parse(realize_full_uri(uri)).host error "Unable to connect to #{host}" - rescue RestClient::SSLCertificateNotVerified => ex + rescue RestClient::SSLCertificateNotVerified host = URI.parse(realize_full_uri(uri)).host error "WARNING: Unable to verify SSL certificate for #{host}\nTo disable SSL verification, run with HEROKU_SSL_VERIFY=disable" end @@ -654,6 +652,7 @@ def heroku_headers # :nodoc: end def xml(raw) # :nodoc: + require 'rexml/document' REXML::Document.new(raw) end diff --git a/lib/heroku/client/heroku_postgresql.rb b/lib/heroku/client/heroku_postgresql.rb index 07038e70d..c812351e7 100644 --- a/lib/heroku/client/heroku_postgresql.rb +++ b/lib/heroku/client/heroku_postgresql.rb @@ -18,7 +18,6 @@ def self.headers attr_reader :attachment def initialize(attachment) @attachment = attachment - require 'rest_client' end def heroku_postgresql_host @@ -58,6 +57,10 @@ def reset http_put "#{resource_name}/reset" end + def connection_reset + http_post "#{resource_name}/connection_reset" + end + def rotate_credentials http_post "#{resource_name}/credentials_rotation" end @@ -95,6 +98,48 @@ def maintenance_window_set(description) http_put "#{resource_name}/maintenance_window", 'description' => description end + # backups + def backups + http_get "#{resource_name}/transfers" + end + + def backups_get(id, verbose=false) + http_get "#{resource_name}/transfers/#{URI.encode(id)}?verbose=#{verbose}" + end + + def backups_capture + http_post "#{resource_name}/backups" + end + + def backups_restore(backup_url) + http_post "#{resource_name}/restores", 'backup_url' => backup_url + end + + def backups_delete(id) + http_delete "#{resource_name}/backups/#{URI.encode(id)}" + end + + def pg_copy(source_name, source_url, target_name, target_url) + http_post "#{resource_name}/transfers", { + 'from_name' => source_name, + 'from_url' => source_url, + 'to_name' => target_name, + 'to_url' => target_url, + } + end + + def schedules + http_get "#{resource_name}/transfer-schedules" + end + + def schedule(opts={}) + http_post "#{resource_name}/transfer-schedules", opts + end + + def unschedule(id) + http_delete "#{resource_name}/transfer-schedules/#{URI.encode(id.to_s)}" + end + protected def sym_keys(c) @@ -151,6 +196,14 @@ def http_put(path, payload = {}) end end + def http_delete(path) + checking_client_version do + response = heroku_postgresql_resource[path].delete + display_heroku_warning response + sym_keys(json_decode(response.to_s)) + end + end + private def determine_host(value, default) diff --git a/lib/heroku/client/heroku_postgresql_backups.rb b/lib/heroku/client/heroku_postgresql_backups.rb new file mode 100644 index 000000000..237462911 --- /dev/null +++ b/lib/heroku/client/heroku_postgresql_backups.rb @@ -0,0 +1,115 @@ +class Heroku::Client::HerokuPostgresqlApp + + Version = 11 + + include Heroku::Helpers + + def self.headers + Heroku::Client::HerokuPostgresql.headers + end + + def initialize(app_name) + @app_name = app_name + end + + def transfers + http_get "#{@app_name}/transfers" + end + + def transfers_get(id, verbose=false) + http_get "#{@app_name}/transfers/#{URI.encode(id.to_s)}?verbose=#{verbose}" + end + + def transfers_delete(id) + http_delete "#{@app_name}/transfers/#{URI.encode(id.to_s)}" + end + + def transfers_cancel(id) + http_post "#{@app_name}/transfers/#{URI.encode(id.to_s)}/actions/cancel" + end + + def transfers_public_url(id) + http_post "#{@app_name}/transfers/#{URI.encode(id.to_s)}/actions/public-url" + end + + def heroku_postgresql_host + if ENV['SHOGUN'] + "shogun-#{ENV['SHOGUN']}.herokuapp.com" + else + determine_host(ENV["HEROKU_POSTGRESQL_HOST"], "postgres-api.heroku.com") + end + end + + def heroku_postgresql_resource + RestClient::Resource.new( + "https://#{heroku_postgresql_host}/client/v11/apps", + :user => Heroku::Auth.user, + :password => Heroku::Auth.password, + :headers => self.class.headers + ) + end + + def http_get(path) + checking_client_version do + retry_on_exception(RestClient::Exception) do + response = heroku_postgresql_resource[path].get + display_heroku_warning response + sym_keys(json_decode(response.to_s)) + end + end + end + + def http_post(path, payload = {}) + checking_client_version do + response = heroku_postgresql_resource[path].post(json_encode(payload)) + display_heroku_warning response + sym_keys(json_decode(response.to_s)) + end + end + + def http_delete(path) + checking_client_version do + response = heroku_postgresql_resource[path].delete + display_heroku_warning response + sym_keys(json_decode(response.to_s)) + end + end + + def display_heroku_warning(response) + warning = response.headers[:x_heroku_warning] + display warning if warning + response + end + + private + + def determine_host(value, default) + if value.nil? + default + else + "#{value}.herokuapp.com" + end + end + + def sym_keys(c) + if c.is_a?(Array) + c.map { |e| sym_keys(e) } + else + c.inject({}) do |h, (k, v)| + h[k.to_sym] = v; h + end + end + end + + def checking_client_version + begin + yield + rescue RestClient::BadRequest => e + if message = json_decode(e.response.to_s)["upgrade_message"] + abort(message) + else + raise e + end + end + end +end diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index 819ca9c3e..8048a4082 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -1,4 +1,3 @@ -require 'heroku-api' require "heroku/client" class Heroku::Client::Organizations @@ -57,7 +56,7 @@ def request params if response.body && !response.body.empty? decompress_response!(response) begin - response.body = Heroku::OkJson.decode(response.body) + response.body = MultiJson.load(response.body) rescue # leave non-JSON body as is end @@ -85,24 +84,6 @@ def get_orgs end end - def remove_default_org - api.request( - :expects => 204, - :method => :delete, - :path => "/v1/user/default-organization" - ) - end - - def set_default_org(org) - api.request( - :expects => 200, - :method => :post, - :path => "/v1/user/default-organization", - :body => Heroku::Helpers.json_encode( { "default_organization" => org } ), - :headers => {"Content-Type" => "application/json"} - ) - end - # Apps ################################# def get_apps(org) @@ -231,7 +212,7 @@ def decompress_response!(response) end def manager_url - ENV['HEROKU_MANAGER_URL'] || "https://manager-api.heroku.com" + Heroku::Auth.full_host end end diff --git a/lib/heroku/client/pgbackups.rb b/lib/heroku/client/pgbackups.rb index be129f531..a031daa9d 100644 --- a/lib/heroku/client/pgbackups.rb +++ b/lib/heroku/client/pgbackups.rb @@ -5,7 +5,6 @@ class Heroku::Client::Pgbackups include Heroku::Helpers def initialize(uri) - require 'rest_client' @uri = URI.parse(uri) end diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index f944955e9..45258c5d1 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -1,7 +1,12 @@ require 'heroku/helpers' require 'heroku/plugin' require 'heroku/version' -require "optparse" +require 'heroku/http_instrumentor' +require 'heroku/git' +require 'heroku-api' +require 'optparse' +require 'rest_client' +require 'multi_json' module Heroku module Command @@ -9,7 +14,12 @@ class CommandFailed < RuntimeError; end extend Heroku::Helpers + class << self + attr_accessor :requires_preauth + end + def self.load + Heroku::JSPlugin.load! Dir[File.join(File.dirname(__FILE__), "command", "*.rb")].each do |file| require file end @@ -99,7 +109,7 @@ def self.warnings def self.display_warnings unless warnings.empty? - $stderr.puts(warnings.map {|warning| " ! #{warning}"}.join("\n")) + $stderr.puts(warnings.uniq.map {|warning| " ! #{warning}"}.join("\n")) end end @@ -141,7 +151,7 @@ def self.prepare_run(cmd, args=[]) opts = {} invalid_options = [] - parser = OptionParser.new do |parser| + p = OptionParser.new do |parser| # remove OptionParsers Officious['version'] to avoid conflicts # see: https://github.com/ruby/ruby/blob/trunk/lib/optparse.rb#L814 parser.base.long.delete('version') @@ -159,7 +169,7 @@ def self.prepare_run(cmd, args=[]) end begin - parser.order!(args) do |nonopt| + p.order!(args) do |nonopt| invalid_options << nonopt @anonymized_args << '!' @normalized_args << '!' @@ -178,26 +188,11 @@ def self.prepare_run(cmd, args=[]) @invalid_arguments = invalid_options @anonymous_command = [ARGV.first, *@anonymized_args].join(' ') - begin - usage_directory = "#{home_directory}/.heroku/usage" - FileUtils.mkdir_p(usage_directory) - usage_file = usage_directory << "/#{Heroku::VERSION}" - usage = if File.exists?(usage_file) - json_decode(File.read(usage_file)) - else - {} - end - usage[@anonymous_command] ||= 0 - usage[@anonymous_command] += 1 - File.write(usage_file, json_encode(usage) + "\n") - rescue - # usage writing is not important, allow failures - end if command command_instance = command[:klass].new(args.dup, opts.dup) - if !@normalized_args.include?('--app _') && (implied_app = command_instance.app rescue nil) + if !@normalized_args.include?('--app _') && (command_instance.app rescue nil) @normalized_args << '--app _' end @normalized_command = [ARGV.first, @normalized_args.sort_by {|arg| arg.gsub('-', '')}].join(' ') @@ -213,15 +208,8 @@ def self.prepare_run(cmd, args=[]) end def self.run(cmd, arguments=[]) - begin - object, method = prepare_run(cmd, arguments.dup) - object.send(method) - rescue Interrupt, StandardError, SystemExit => error - # load likely error classes, as they may not be loaded yet due to defered loads - require 'heroku-api' - require 'rest_client' - raise(error) - end + object, method = prepare_run(cmd, arguments.dup) + object.send(method) rescue Heroku::API::Errors::Unauthorized, RestClient::Unauthorized => e retry_login = handle_auth_error(e) retry if retry_login @@ -236,7 +224,7 @@ def self.run(cmd, arguments=[]) e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" } rescue Heroku::API::Errors::Locked => e - app = e.response.headers[:x_confirmation_required] + app = e.response.headers["X-Confirmation-Required"] if confirm_command(app, extract_error(e.response.body)) arguments << '--confirm' << app retry @@ -251,7 +239,11 @@ def self.run(cmd, arguments=[]) error "API request timed out. Please try again, or contact support@heroku.com if this issue persists." rescue Heroku::API::Errors::Forbidden => e if e.response.headers.has_key?("Heroku-Two-Factor-Required") - Heroku::Auth.ask_for_second_factor + if requires_preauth + Heroku::Auth.preauth + else + Heroku::Auth.ask_for_second_factor + end retry else error extract_error(e.response.body) @@ -259,9 +251,14 @@ def self.run(cmd, arguments=[]) rescue Heroku::API::Errors::ErrorWithResponse => e error extract_error(e.response.body) rescue RestClient::RequestFailed => e - error extract_error(e.http_body) + if e.response.code == 403 && e.response.headers.has_key?(:heroku_two_factor_required) + Heroku::Auth.preauth + retry + else + error extract_error(e.http_body) + end rescue CommandFailed => e - error e.message + error e.message, false rescue OptionParser::ParseError commands[cmd] ? run("help", [cmd]) : run("help") rescue Excon::Errors::SocketError, SocketError => e @@ -272,10 +269,9 @@ def self.run(cmd, arguments=[]) def self.handle_auth_error(e) if ENV['HEROKU_API_KEY'] - puts "Authentication failure" + puts "Authentication failure with HEROKU_API_KEY" exit 1 - end - if wrong_two_factor_code?(e) + elsif wrong_two_factor_code?(e) puts "Invalid two-factor code" false else @@ -291,14 +287,15 @@ def self.parse(cmd) def self.extract_error(body, options={}) default_error = block_given? ? yield : "Internal server error.\nRun `heroku status` to check for known platform issues." - parse_error_xml(body) || parse_error_json(body) || parse_error_plain(body) || default_error + parse_error_json(body) || parse_error_xml(body) || parse_error_plain(body) || default_error end def self.parse_error_xml(body) + require 'rexml/document' xml_errors = REXML::Document.new(body).elements.to_a("//errors/error") msg = xml_errors.map { |a| a.text }.join(" / ") return msg unless msg.empty? - rescue Exception + rescue end def self.parse_error_json(body) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 300d3807b..295e44584 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -1,5 +1,8 @@ require "heroku/command/base" require "heroku/helpers/heroku_postgresql" +require "heroku/helpers/addons/api" +require "heroku/helpers/addons/display" +require "heroku/helpers/addons/resolve" module Heroku::Command @@ -8,178 +11,384 @@ module Heroku::Command class Addons < Base include Heroku::Helpers::HerokuPostgresql + include Heroku::Helpers::Addons::API + include Heroku::Helpers::Addons::Display + include Heroku::Helpers::Addons::Resolve - # addons + # addons [{--all,--app APP_NAME,--resource ADDON_NAME}] # - # list installed addons + # list installed add-ons + # + # NOTE: --all is the default unless in an application repository directory, in + # which case --all is inferred. + # + # --all # list add-ons across all apps in account + # --app APP_NAME # list add-ons associated with a given app + # --resource ADDON_NAME # view details about add-on and all of its attachments + # + #Examples: + # + # $ heroku addons --all + # $ heroku addons --app acme-inc-website + # $ heroku addons --resource @acme-inc-database # def index validate_arguments! - - installed = api.get_addons(app).body - if installed.empty? - display("#{app} has no add-ons.") + requires_preauth + + # Filters are mutually exclusive + error("Can not use --all with --app") if options[:app] && options[:all] + error("Can not use --all with --resource") if options[:resource] && options[:all] + error("Can not use --app with --resource") if options[:resource] && options[:app] + + app = (self.app rescue nil) + if (resource = options[:resource]) + show_for_resource(resource) + elsif app && !options[:all] + show_for_app(app) else - available, pending = installed.partition { |a| a['configured'] } + show_all + end + end - unless available.empty? - styled_header("#{app} Configured Add-ons") - styled_array(available.map do |a| - [a['name'], a['attachment_name'] || ''] - end) - end + # addons:services + # + # list all available add-on services + def services + if current_command == "addons:list" + deprecate("`heroku #{current_command}` has been deprecated. Please use `heroku addons:services` instead.") + end - unless pending.empty? - styled_header("#{app} Add-ons to Configure") - styled_array(pending.map do |a| - [a['name'], app_addon_url(a['name'])] - end) - end + display_table(get_services, %w[name human_name state], %w[Slug Name State]) + display "\nSee plans with `heroku addons:plans SERVICE`" + end + + alias_command "addons:list", "addons:services" + + # addons:plans SERVICE + # + # list all available plans for an add-on service + def plans + service = args.shift + raise CommandFailed.new("Missing add-on service") if service.nil? + + service = get_service!(service) + display_header("#{service['human_name']} Plans") + + plans = get_plans(:service => service['id']) + + plans = plans.sort_by { |p| [(!p['default']).to_s, p['price']['cents']] }.map do |plan| + { + "default" => ('default' if plan['default']), + "name" => plan["name"], + "human_name" => plan["human_name"], + "price" => format_price(plan["price"]) + } end + + display_table(plans, %w[default name human_name price], [nil, 'Slug', 'Name', 'Price']) end - # addons:list + # addons:create {SERVICE,PLAN} # - # list all available addons + # create an add-on resource # - # --region REGION # specify a region for addon availability + # --name ADDON_NAME # (optional) name for the add-on resource + # --as ATTACHMENT_NAME # (optional) name for the initial add-on attachment + # --confirm APP_NAME # (optional) ovewrite existing config vars or existing add-on attachments # - #Example: + def create + if current_command == "addons:add" + deprecate("`heroku #{current_command}` has been deprecated. Please use `heroku addons:create` instead.") + end + + requires_preauth + + service_plan = expand_hpg_shorthand(args.shift) + + raise CommandFailed.new("Missing requested service or plan") if service_plan.nil? || %w{--fork --follow --rollback}.include?(service_plan) + + config = parse_options(args) + raise CommandFailed.new("Unexpected arguments: #{args.join(' ')}") unless args.empty? + + addon = request( + :body => json_encode({ + "attachment" => { "name" => options[:as] }, + "config" => config, + "name" => options[:name], + "confirm" => options[:confirm], + "plan" => { "name" => service_plan } + }), + :headers => { + # Temporary hack for getting provider messages while a cleaner + # endpoint is designed to communicate this data. + # + # WARNING: Do not depend on this having any effect permanently. + "Accept-Expansion" => "plan", + "X-Heroku-Legacy-Provider-Messages" => "true" + }, + :expects => 201, + :method => :post, + :path => "/apps/#{app}/addons" + ) + @status = "(#{format_price addon['plan']['price']})" if addon['plan'].has_key?('price') + + action("Creating #{addon['name'].downcase}") {} + action("Adding #{addon['name'].downcase} to #{app}") {} + + if addon['config_vars'].any? + action("Setting #{addon['config_vars'].join(', ')} and restarting #{app}") do + @status = api.get_release(app, 'current').body['name'] + end + end + + display addon['provision_message'] unless addon['provision_message'].to_s.strip == "" + + display("Use `heroku addons:docs #{addon['addon_service']['name']}` to view documentation.") + end + + alias_command "addons:add", "addons:create" + + # addons:attach ADDON_NAME # - # $ heroku addons:list --region eu - # === available - # adept-scale:battleship, corvette... - # adminium:enterprise, petproject... + # attach add-on resource to an app # - def list - addons = heroku.addons(options) - if addons.empty? - display "No addons available currently" - else - partitioned_addons = partition_addons(addons) - partitioned_addons.each do |key, addons| - partitioned_addons[key] = format_for_display(addons) + # --as ATTACHMENT_NAME # (optional) name for add-on attachment + # --confirm APP_NAME # overwrite existing add-on attachment with same name + # + def attach + unless addon_name = args.shift + error("Usage: heroku addons:attach ADDON_NAME\nMust specify add-on resource to attach.") + end + addon = resolve_addon!(addon_name) + + requires_preauth + + attachment_name = options[:as] + + msg = attachment_name ? + "Attaching #{addon['name']} as #{attachment_name} to #{app}" : + "Attaching #{addon['name']} to #{app}" + + display("#{msg}... ", false) + + response = api.request( + :body => json_encode({ + "app" => {"name" => app}, + "addon" => {"name" => addon['name']}, + "confirm" => options[:confirm], + "name" => attachment_name + }), + :expects => [201, 422], + :headers => { "Accept" => "application/vnd.heroku+json; version=3" }, + :method => :post, + :path => "/addon-attachments" + ) + + case response.status + when 201 + display("done") + action("Setting #{response.body["name"]} vars and restarting #{app}") do + @status = api.get_release(app, 'current').body['name'] end - display_object(partitioned_addons) + when 422 # add-on resource not found or cannot be attached + display("failed") + output_with_bang(response.body["message"]) + output_with_bang("List available resources with `heroku addons`.") + output_with_bang("Provision a new add-on resource with `heroku addons:create ADDON_PLAN`.") end end - # addons:add ADDON + # addons:detach ATTACHMENT_NAME # - # install an addon + # detach add-on resource from an app # - def add - configure_addon('Adding') do |addon, config| - heroku.install_addon(app, addon, config) + def detach + attachment_name = args.shift + raise CommandFailed.new("Missing add-on attachment name") if attachment_name.nil? + requires_preauth + + addon_attachment = resolve_attachment!(attachment_name) + + attachment_name = addon_attachment['name'] # in case a UUID was passed in + addon_name = addon_attachment['addon']['name'] + app = addon_attachment['app']['name'] + + action("Removing #{attachment_name} attachment to #{addon_name} from #{app}") do + api.request( + :expects => 200..300, + :headers => { "Accept" => "application/vnd.heroku+json; version=3" }, + :method => :delete, + :path => "/addon-attachments/#{addon_attachment['id']}" + ).body + end + action("Unsetting #{attachment_name} vars and restarting #{app}") do + @status = api.get_release(app, 'current').body['name'] end end - # addons:upgrade ADDON + # addons:upgrade ADDON_NAME ADDON_SERVICE:PLAN # - # upgrade an existing addon + # upgrade an existing add-on resource to PLAN # def upgrade - configure_addon('Upgrading to') do |addon, config| - heroku.upgrade_addon(app, addon, config) + addon_name, plan = args.shift, args.shift + + if addon_name && !plan # If invocated as `addons:Xgrade service:plan` + deprecate("No add-on name specified (see `heroku help #{current_command}`)") + + addon = nil + plan = addon_name + service = plan.split(':').first + + action("Finding add-on from service #{service} on app #{app}") do + # resolve with the service only, because the user has passed in the + # *intended* plan, not the current plan. + addon = resolve_addon!(service) + addon_name = addon['name'] + end + display "Found #{addon_name} (#{addon['plan']['name']}) on #{app}." + else + raise CommandFailed.new("Missing add-on name") if addon_name.nil? + addon_name = addon_name.sub(/^@/, '') + end + + raise CommandFailed.new("Missing add-on plan") if plan.nil? + + action("Changing #{addon_name} plan to #{plan}") do + addon = api.request( + :body => json_encode({ + "plan" => { "name" => plan } + }), + :expects => 200..300, + :headers => { + "Accept" => "application/vnd.heroku+json; version=3", + "Accept-Expansion" => "plan" + }, + :method => :patch, + :path => "/apps/#{app}/addons/#{addon_name}" + ).body + @status = "(#{format_price addon['plan']['price']})" if addon['plan'].has_key?('price') end end - # addons:downgrade ADDON + # addons:downgrade ADDON_NAME ADDON_SERVICE:PLAN # - # downgrade an existing addon + # downgrade an existing add-on resource to PLAN # def downgrade - configure_addon('Downgrading to') do |addon, config| - heroku.upgrade_addon(app, addon, config) - end + upgrade end - # addons:remove ADDON1 [ADDON2 ...] + # addons:destroy ADDON_NAME [ADDON_NAME ...] # - # uninstall one or more addons + # destroy add-on resources # - def remove - return unless confirm_command + # -f, --force # allow destruction even if add-on is attached to other apps + # + def destroy + if current_command == "addons:remove" + deprecate("`heroku #{current_command}` has been deprecated. Please use `heroku addons:destroy` instead.") + end - args.each do |name| - messages = nil - if name.start_with? "HEROKU_POSTGRESQL_" - name = name.chomp("_URL").freeze + raise CommandFailed.new("Missing add-on name") if args.empty? + + requires_preauth + confirmed_apps = [] + + while addon_name = args.shift + addon = resolve_addon!(addon_name) + app = addon['app'] + + unless confirmed_apps.include?(app['name']) + return unless confirm_command(app['name']) + confirmed_apps << app['name'] end - action("Removing #{name} on #{app}") do - messages = addon_run { heroku.uninstall_addon(app, name, :confirm => app) } + + addon_attachments = get_attachments(:resource => addon['id']) + + action("Destroying #{addon['name']} on #{app['name']}") do + addon = api.request( + :body => json_encode({ + "force" => options[:force], + }), + :expects => 200..300, + :headers => { + "Accept" => "application/vnd.heroku+json; version=3", + "Accept-Expansion" => "plan" + }, + :method => :delete, + :path => "/apps/#{app['id']}/addons/#{addon['id']}" + ).body + @status = "(#{format_price addon['plan']['price']})" if addon['plan'].has_key?('price') + end + + if addon['config_vars'].any? # litmus test for whether the add-on's attachments have vars + # For each app that had an attachment, output a message indicating that + # the app has been restarted any any associated vars have been removed. + addon_attachments.group_by { |att| att['app']['name'] }.each do |app, attachments| + names = attachments.map { |att| att['name'] }.join(', ') + action("Removing vars for #{names} from #{app} and restarting") { + @status = api.get_release(app, 'current').body['name'] + } + end end - display(messages[:attachment]) if messages[:attachment] - display(messages[:message]) if messages[:message] end end - # addons:docs ADDON + alias_command "addons:remove", "addons:destroy" + + # addons:docs ADDON_NAME # - # open an addon's documentation in your browser + # open an add-on's documentation in your browser # def docs - unless addon = shift_argument + unless identifier = shift_argument error("Usage: heroku addons:docs ADDON\nMust specify ADDON to open docs for.") end validate_arguments! - addon_names = api.get_addons.body.map {|a| a['name']} - addon_types = addon_names.map {|name| name.split(':').first}.uniq - - name_matches = addon_names.select {|name| name =~ /^#{addon}/} - type_matches = addon_types.select {|name| name =~ /^#{addon}/} - - if name_matches.include?(addon) || type_matches.include?(addon) - type_matches = [addon] - end - - case type_matches.length - when 0 then - error([ - "`#{addon}` is not a heroku add-on.", - suggestion(addon, addon_names + addon_types), - "See `heroku addons:list` for all available addons." - ].compact.join("\n")) - when 1 - addon_type = type_matches.first - launchy("Opening #{addon_type} docs", addon_docs_url(addon_type)) + # If it looks like a plan, optimistically open docs, otherwise try to + # lookup a corresponding add-on and open the docs for its service. + if identifier.include?(':') + service = identifier.split(':')[0] + launchy("Opening #{service} docs", addon_docs_url(service)) else - error("Ambiguous addon name: #{addon}\nPerhaps you meant #{name_matches[0...-1].map {|match| "`#{match}`"}.join(', ')} or `#{name_matches.last}`.\n") + # searching by any number of things + matches = resolve_addon(identifier) + services = matches.map { |m| m['addon_service']['name'] }.uniq + + case services.count + when 0 + # Optimistically open docs for whatever they passed in + launchy("Opening #{identifier} docs", addon_docs_url(identifier)) + when 1 + service = services.first + launchy("Opening #{service} docs", addon_docs_url(service)) + else + error("Multiple add-ons match #{identifier.inspect}.\n" + + "Use the name of one of the add-on resources:\n\n" + + matches.map { |a| "- #{a['name']} (#{a['addon_service']['name']})" }.join("\n")) + end end end - # addons:open ADDON + # addons:open ADDON_NAME # - # open an addon's dashboard in your browser + # open an add-on's dashboard in your browser # def open - unless addon = shift_argument + unless addon_name = shift_argument error("Usage: heroku addons:open ADDON\nMust specify ADDON to open.") end validate_arguments! + requires_preauth - app_addons = api.get_addons(app).body.map {|a| a['name']} - matches = app_addons.select {|a| a =~ /^#{addon}/}.sort + addon = resolve_addon!(addon_name) + return addon if addon.is_a?(String) - case matches.length - when 0 then - addon_names = api.get_addons.body.map {|a| a['name']} - if addon_names.any? {|name| name =~ /^#{addon}/} - error("Addon not installed: #{addon}") - else - error([ - "`#{addon}` is not a heroku add-on.", - suggestion(addon, addon_names + addon_names.map {|name| name.split(':').first}.uniq), - "See `heroku addons:list` for all available addons." - ].compact.join("\n")) - end - when 1 then - addon_to_open = matches.first - launchy("Opening #{addon_to_open} for #{app}", app_addon_url(addon_to_open)) - else - error("Ambiguous addon name: #{addon}\nPerhaps you meant #{matches[0...-1].map {|match| "`#{match}`"}.join(', ')} or `#{matches.last}`.\n") - end + service = addon['addon_service']['name'] + launchy("Opening #{service} (#{addon['name']}) for #{addon['app']['name']}", addon["web_url"]) end private @@ -188,109 +397,16 @@ def addon_docs_url(addon) "https://devcenter.#{heroku.host}/articles/#{addon.split(':').first}" end - def app_addon_url(addon) - "https://addons-sso.heroku.com/apps/#{app}/addons/#{addon}" - end - - def partition_addons(addons) - addons.group_by{ |a| (a["state"] == "public" ? "available" : a["state"]) } - end - - def format_for_display(addons) - grouped = addons.inject({}) do |base, addon| - group, short = addon['name'].split(':') - base[group] ||= [] - base[group] << addon.merge('short' => short) - base - end - grouped.keys.sort.map do |name| - addons = grouped[name] - row = name.dup - if addons.any? { |a| a['short'] } - row << ':' - size = row.size - stop = false - row << addons.map { |a| a['short'] }.compact.sort.map do |short| - size += short.size - if size < 31 - short - else - stop = true - nil - end - end.compact.join(', ') - row << '...' if stop - end - row - end - end - - def addon_run - response = yield - - if response - price = "(#{ response['price'] })" if response['price'] - - if response['message'] =~ /(Attached as [A-Z0-9_]+)\n(.*)/m - attachment = $1 - message = $2 - else - attachment = nil - message = response['message'] - end - - begin - release = api.get_release(app, 'current').body - release = release['name'] - rescue Heroku::API::Errors::Error - release = nil - end - end - - status [ release, price ].compact.join(' ') - { :attachment => attachment, :message => message } - rescue RestClient::ResourceNotFound => e - error Heroku::Command.extract_error(e.http_body) { - e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" - } - rescue RestClient::Locked => ex - raise - rescue RestClient::RequestFailed => e - error Heroku::Command.extract_error(e.http_body) - end - - def configure_addon(label, &install_or_upgrade) - addon = args.shift - raise CommandFailed.new("Missing add-on name") if addon.nil? || %w{--fork --follow --rollback}.include?(addon) - - config = parse_options(args) - addon_name, plan = addon.split(':') - - # For Heroku Postgres, if no plan is specified with fork/follow/rollback, - # default to the plan of the current postgresql plan - if addon_name =~ /heroku-postgresql/ then - hpg_flag = %w{rollback fork follow}.select {|flag| config.keys.include? flag}.first - if plan.nil? && config[hpg_flag] =~ /^postgres:\/\// then - raise CommandFailed.new("Cross application database Forking/Following requires you specify a plan type") - elsif (hpg_flag && plan.nil?) then - resolver = Resolver.new(app, api) - addon = addon + ':' + resolver.resolve(config[hpg_flag]).plan - end + def expand_hpg_shorthand(addon_plan) + if addon_plan =~ /\Ahpg:/ + addon_plan = "heroku-postgresql:#{addon_plan.split(':').last}" end - - config.merge!(:confirm => app) if app == options[:confirm] - raise CommandFailed.new("Unexpected arguments: #{args.join(' ')}") unless args.empty? - - hpg_translate_db_opts_to_urls(addon, config) - - messages = nil - action("#{label} #{addon} on #{app}") do - messages = addon_run { install_or_upgrade.call(addon, config) } + if addon_plan =~ /\Aheroku-postgresql:[spe]\d+\z/ + addon_plan.gsub!(/:s/,':standard-') + addon_plan.gsub!(/:p/,':premium-') + addon_plan.gsub!(/:e/,':enterprise-') end - display(messages[:attachment]) unless messages[:attachment].to_s.strip == "" - display(messages[:message]) unless messages[:message].to_s.strip == "" - - display("Use `heroku addons:docs #{addon_name}` to view documentation.") + addon_plan end #this will clean up when we officially deprecate diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 6f10db1e2..66c9b022c 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -1,4 +1,5 @@ require "heroku/command/base" +require "heroku/command/stack" # manage apps (create, destroy) # @@ -84,24 +85,25 @@ def index # # $ heroku apps:info # === example - # Git URL: git@heroku.com:example.git + # Git URL: https://git.heroku.com/example.git # Repo Size: 5M # ... # # $ heroku apps:info --shell - # git_url=git@heroku.com:example.git + # git_url=https://git.heroku.com/example.git # repo_size=5000000 # ... # def info validate_arguments! + requires_preauth app_data = api.get_app(app).body unless options[:shell] styled_header(app_data["name"]) end - addons_data = api.get_addons(app).body.map {|addon| addon['name']}.sort + addons_data = api.get_addons(app).body.map {|addon| addon['name']}.sort rescue {} collaborators_data = api.get_collaborators(app).body.map {|collaborator| collaborator["email"]}.sort collaborators_data.reject! {|email| email == app_data["owner_email"]} @@ -111,6 +113,7 @@ def info end if options[:shell] + app_data['git_url'] = git_url(app_data['name']) if app_data['domain_name'] app_data['domain_name'] = app_data['domain_name']['domain'] end @@ -152,7 +155,7 @@ def info data["Database Size"] = format_bytes(app_data["database_size"]) end - data["Git URL"] = app_data["git_url"] + data["Git URL"] = git_url(app_data['name']) if app_data["database_tables"] data["Database Size"].gsub!('(empty)', '0K') + " in #{quantify("table", app_data["database_tables"])}" @@ -171,17 +174,13 @@ def info data["Slug Size"] = format_bytes(app_data["slug_size"]) if app_data["slug_size"] data["Cache Size"] = format_bytes(app_data["cache_size"]) if app_data["cache_size"] - data["Stack"] = app_data["stack"] - if data["Stack"] != "cedar" + data["Stack"] = Heroku::Command::Stack::Codex.out(app_data["stack"]) + if data["Stack"] != "cedar-10" data.merge!("Dynos" => app_data["dynos"], "Workers" => app_data["workers"]) end data["Web URL"] = app_data["web_url"] - if app_data["tier"] - data["Tier"] = app_data["tier"].capitalize - end - styled_hash(data) end end @@ -199,22 +198,25 @@ def info # -s, --stack STACK # the stack on which to create the app # --region REGION # specify region for this app to run in # -l, --locked # lock the app + # --ssh-git # Use SSH git protocol # -t, --tier TIER # HIDDEN: the tier for this app + # --http-git # HIDDEN: Use HTTP git protocol # #Examples: # # $ heroku apps:create # Creating floating-dragon-42... done, stack is cedar - # http://floating-dragon-42.heroku.com/ | git@heroku.com:floating-dragon-42.git + # http://floating-dragon-42.heroku.com/ | https://git.heroku.com/floating-dragon-42.git # - # $ heroku apps:create -s bamboo - # Creating floating-dragon-42... done, stack is bamboo-mri-1.9.2 - # http://floating-dragon-42.herokuapp.com/ | git@heroku.com:floating-dragon-42.git + # # specify a stack + # $ heroku create -s cedar + # Creating stormy-garden-5052... done, stack is cedar + # https://stormy-garden-5052.herokuapp.com/ | https://git.heroku.com/stormy-garden-5052.git # # # specify a name # $ heroku apps:create example # Creating example... done, stack is cedar - # http://example.heroku.com/ | git@heroku.com:example.git + # http://example.heroku.com/ | https://git.heroku.com/example.git # # # create a staging app # $ heroku apps:create example-staging --remote staging @@ -230,7 +232,7 @@ def create params = { "name" => name, "region" => options[:region], - "stack" => options[:stack], + "stack" => Heroku::Command::Stack::Codex.in(options[:stack]), "locked" => options[:locked] } @@ -254,7 +256,7 @@ def create status("region is #{region_from_app(info)}") else stack = (info['stack'].is_a?(Hash) ? info['stack']["name"] : info['stack']) - status("stack is #{stack}") + status("stack is #{Heroku::Command::Stack::Codex.out(stack)}") end end @@ -266,17 +268,17 @@ def create end if buildpack = options[:buildpack] - api.put_config_vars(info["name"], "BUILDPACK_URL" => buildpack) - display("BUILDPACK_URL=#{buildpack}") + api.put_app_buildpacks_v3(info['name'], {:updates => [{:buildpack => buildpack}]}) + display "Buildpack set. Next release on #{info['name']} will use #{buildpack}." end - hputs([ info["web_url"], info["git_url"] ].join(" | ")) + hputs([ info["web_url"], git_url(info['name']) ].join(" | ")) rescue Timeout::Error hputs("Timed Out! Run `heroku status` to check for known platform issues.") end unless options[:no_remote].is_a? FalseClass - create_git_remote(options[:remote] || "heroku", info["git_url"]) + create_git_remote(options[:remote] || "heroku", git_url(info['name'])) end end @@ -286,10 +288,13 @@ def create # # rename the app # + # --ssh-git # Use SSH git protocol + # --http-git # HIDDEN: Use HTTP git protocol + # #Example: # # $ heroku apps:rename example-newname - # http://example-newname.herokuapp.com/ | git@heroku.com:example-newname.git + # http://example-newname.herokuapp.com/ | https://git.heroku.com/example-newname.git # Git remote heroku updated # def rename @@ -304,13 +309,13 @@ def rename end app_data = api.get_app(newname).body - hputs([ app_data["web_url"], app_data["git_url"] ].join(" | ")) + hputs([ app_data["web_url"], git_url(newname) ].join(" | ")) if remotes = git_remotes(Dir.pwd) remotes.each do |remote_name, remote_app| next if remote_app != app git "remote rm #{remote_name}" - git "remote add #{remote_name} #{app_data["git_url"]}" + git "remote add #{remote_name} #{git_url(newname)}" hputs("Git remote #{remote_name} updated") end else diff --git a/lib/heroku/command/auth.rb b/lib/heroku/command/auth.rb index 4ace4b344..26b8cdd4c 100644 --- a/lib/heroku/command/auth.rb +++ b/lib/heroku/command/auth.rb @@ -77,10 +77,7 @@ def token # email@example.com # def whoami - validate_arguments! - - display Heroku::Auth.user + Heroku::JSPlugin.setup + Heroku::JSPlugin.run('whoami', nil, ARGV[1..-1]) end - end - diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index 11374ab19..c11f83930 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -20,7 +20,7 @@ def initialize(args=[], options={}) end def app - @app ||= if options[:confirm].is_a?(String) + @app ||= Heroku.app_name = if options[:confirm].is_a?(String) if options[:app] && (options[:app] != options[:confirm]) error("Mismatch between --app and --confirm") end @@ -41,26 +41,17 @@ def org @nil = false options[:ignore_no_app] = true - @org ||= if skip_org? - nil - elsif options[:org].is_a?(String) + @org ||= if options[:org].is_a?(String) options[:org] elsif options[:personal] || @nil nil - elsif org_from_app = extract_org_from_app - org_from_app + elsif ENV['HEROKU_ORGANIZATION'] && ENV['HEROKU_ORGANIZATION'].strip != "" + ENV['HEROKU_ORGANIZATION'] + elsif options[:ignore_no_org] + nil else - response = org_api.get_orgs.body - default = response['user']['default_organization'] - if default - options[:using_default_org] = true - default - elsif options[:ignore_no_org] - nil - else - # raise instead of using error command to enable rescuing when app is optional - raise Heroku::Command::CommandFailed.new("No org specified.\nRun this command from an app folder which belongs to an org or specify which org to use with --org ORG.") - end + # raise instead of using error command to enable rescuing when app is optional + raise Heroku::Command::CommandFailed.new("No org specified.\nRun this command from an app folder which belongs to an org or specify which org to use with --org ORG.") end @nil = true if @org == nil @@ -217,7 +208,7 @@ def extract_app_in_dir(dir) if remote = options[:remote] remotes[remote] - elsif remote = extract_app_from_git_config + elsif remote = extract_remote_from_git_config remotes[remote] else apps = remotes.values.uniq @@ -229,7 +220,7 @@ def extract_app_in_dir(dir) end end - def extract_app_from_git_config + def extract_remote_from_git_config remote = git("config heroku.remote") remote == "" ? nil : remote end @@ -254,10 +245,15 @@ def org_from_app! options[:personal] = true unless options[:org] end - def skip_org? - return false if ENV['HEROKU_CLOUD'].nil? || ENV['HEROKU_MANAGER_URL'] - - !%w{default production prod}.include? ENV['HEROKU_CLOUD'] + def git_url(app_name) + if options[:ssh_git] + "git@#{Heroku::Auth.git_host}:#{app_name}.git" + else + unless has_http_git_entry_in_netrc + warn "WARNING: Incomplete credentials detected, git may not work with Heroku. Run `heroku login` to update your credentials. See documentation for details: https://devcenter.heroku.com/articles/http-git#authentication" + end + "https://#{Heroku::Auth.http_git_host}/#{app_name}.git" + end end def git_remotes(base_dir=Dir.pwd) @@ -267,8 +263,9 @@ def git_remotes(base_dir=Dir.pwd) return unless File.exists?(".git") git("remote -v").split("\n").each do |remote| - name, url, method = remote.split(/\s/) - if url =~ /^git@#{Heroku::Auth.git_host}(?:[\.\w]*):([\w\d-]+)\.git$/ + name, url, _ = remote.split(/\s/) + if url =~ /^git@#{Heroku::Auth.git_host}(?:[\.\w]*):([\w\d-]+)\.git$/ || + url =~ /^https:\/\/#{Heroku::Auth.http_git_host}\/([\w\d-]+)\.git$/ remotes[name] = $1 end end @@ -284,6 +281,10 @@ def git_remotes(base_dir=Dir.pwd) def escape(value) heroku.escape(value) end + + def requires_preauth + Heroku::Command.requires_preauth = true + end end module Heroku::Command diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb new file mode 100644 index 000000000..19502d5b9 --- /dev/null +++ b/lib/heroku/command/buildpacks.rb @@ -0,0 +1,230 @@ +require "heroku/command/base" +require "heroku/api/apps_v3" + +module Heroku::Command + + # manage the buildpack for an app + # + class Buildpacks < Base + + # buildpacks + # + # display the buildpack_url(s) for an app + # + #Examples: + # + # $ heroku buildpacks + # https://github.com/heroku/heroku-buildpack-ruby + # + def index + validate_arguments! + + app_buildpacks = api.get_app_buildpacks_v3(app)[:body] + + if app_buildpacks.nil? or app_buildpacks.empty? + display("#{app} has no Buildpack URL set.") + else + styled_header("#{app} Buildpack URL#{app_buildpacks.size > 1 ? 's' : ''}") + display_buildpacks(app_buildpacks.map{|bp| bp["buildpack"]["url"]}, "") + end + end + + # buildpacks:set BUILDPACK_URL + # + # set new app buildpack, overwriting into list of buildpacks if neccessary + # + # -i, --index NUM # the 1-based index of the URL in the list of URLs + # + #Example: + # + # $ heroku buildpacks:set -i 1 https://github.com/heroku/heroku-buildpack-ruby + # + def set + unless buildpack_url = shift_argument + error("Usage: heroku buildpacks:set BUILDPACK_URL.\nMust specify target buildpack URL.") + end + + index = get_index(0) + + mutate_buildpacks_constructive(buildpack_url, index, "set") do |existing_url, ordinal| + if ordinal == index + buildpack_url + else + existing_url + end + end + end + + # buildpacks:add BUILDPACK_URL + # + # add new app buildpack, inserting into list of buildpacks if neccessary + # + # -i, --index NUM # the 1-based index of the URL in the list of URLs + # + #Example: + # + # $ heroku buildpacks:add -i 1 https://github.com/heroku/heroku-buildpack-ruby + # + def add + unless buildpack_url = shift_argument + error("Usage: heroku buildpacks:add BUILDPACK_URL.\nMust specify target buildpack URL.") + end + + index = get_index + + mutate_buildpacks_constructive(buildpack_url, index, "added") do |existing_url, ordinal| + if ordinal == index + [buildpack_url, existing_url] + else + existing_url + end + end + end + + # buildpacks:remove [BUILDPACK_URL] + # + # remove a buildpack set on the app + # + # -i, --index NUM # the 1-based index of the URL to remove from the list of URLs + # + def remove + if buildpack_url = shift_argument + if options[:index] + error("Please choose either index or Buildpack URL, but not both.") + end + elsif index = get_index + # cool! + else + error("Usage: heroku buildpacks:remove [BUILDPACK_URL].\nMust specify a buildpack to remove, either by index or URL.") + end + + mutate_buildpacks(buildpack_url, index, "removed") do |app_buildpacks| + if app_buildpacks.size == 0 + error("No buildpacks were found. Next release on #{app} will detect buildpack normally.") + end + + if index and (index < 0 or index > app_buildpacks.size) + if app_buildpacks.size == 1 + error("Invalid index. Only valid value is 1.") + else + error("Invalid index. Please choose a value between 1 and #{app_buildpacks.size}") + end + end + + buildpack_urls = app_buildpacks.map { |buildpack| + ordinal = buildpack["ordinal"].to_i + if ordinal == index + nil + elsif buildpack["buildpack"]["url"] == buildpack_url + nil + else + buildpack["buildpack"]["url"] + end + }.compact + + if buildpack_urls.size == app_buildpacks.size + error("Buildpack not found. Nothing was removed.") + end + + buildpack_urls + end + end + + # buildpacks:clear + # + # clear all buildpacks set on the app + # + def clear + api.put_app_buildpacks_v3(app, {:updates => []}) + display_no_buildpacks("cleared", true) + end + + private + + def mutate_buildpacks_constructive(buildpack_url, index, action) + mutate_buildpacks(buildpack_url, index, action) do |app_buildpacks| + buildpack_urls = app_buildpacks.map { |buildpack| + ordinal = buildpack["ordinal"] + existing_url = buildpack["buildpack"]["url"] + if existing_url == buildpack_url + error("The buildpack #{buildpack_url} is already set on your app.") + else + yield(existing_url, ordinal) + end + }.flatten.compact + + # default behavior if index is out of range, or list is previously empty + # is to add buildpack to the list + if app_buildpacks.empty? or index.nil? or app_buildpacks.size < index + buildpack_urls << buildpack_url + end + + buildpack_urls + end + end + + def mutate_buildpacks(buildpack_url, index, action) + app_buildpacks = api.get_app_buildpacks_v3(app)[:body] + + buildpack_urls = yield(app_buildpacks) + + update_buildpacks(buildpack_urls, action) + end + + def get_index(default=nil) + validate_arguments! + if options[:index] + index = options[:index].to_i + index -= 1 + if index < 0 + error("Invalid index. Must be greater than 0.") + end + index + else + default + end + end + + def update_buildpacks(buildpack_urls, action) + api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) + display_buildpack_change(buildpack_urls, action) + end + + def display_buildpacks(buildpacks, indent=" ") + if (buildpacks.size == 1) + display(buildpacks.first) + else + buildpacks.each_with_index do |bp, i| + display("#{indent}#{i+1}. #{bp}") + end + end + end + + def display_buildpack_change(buildpack_urls, action) + if buildpack_urls.size > 1 + display "Buildpack #{action}. Next release on #{app} will use:" + display_buildpacks(buildpack_urls) + display "Run `git push heroku master` to create a new release using these buildpacks." + elsif buildpack_urls.size == 1 + display "Buildpack #{action}. Next release on #{app} will use #{buildpack_urls.first}." + display "Run `git push heroku master` to create a new release using this buildpack." + else + display_no_buildpacks + end + end + + def display_no_buildpacks(action="removed", plural=false) + vars = api.get_config_vars(app).body + if vars.has_key?("BUILDPACK_URL") + display "Buildpack#{plural ? "s" : ""} #{action}." + warn "WARNING: The BUILDPACK_URL config var is still set and will be used for the next release" + elsif vars.has_key?("LANGUAGE_PACK_URL") + display "Buildpack#{plural ? "s" : ""} #{action}." + warn "WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release" + else + display "Buildpack#{plural ? "s" : ""} #{action}. Next release on #{app} will detect buildpack normally." + end + end + + end +end diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 573013832..1046089a6 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -1,4 +1,5 @@ require "heroku/command/base" +require "heroku/open_ssl" require "excon" # manage ssl endpoints for an app @@ -58,7 +59,7 @@ def chain # The first key that signs the certificate will be printed back. # def key - crt, key = read_crt_and_key_through_ssl_doctor("Testing for signing key") + _, key = read_crt_and_key_through_ssl_doctor("Testing for signing key") puts key rescue UsageError fail("Usage: heroku certs:key CRT KEY [KEY ...]\nMust specify one certificate file and at least one key file.") @@ -113,8 +114,12 @@ def info heroku.ssl_endpoint_info(app, cname) end - display "Certificate details:" - display_certificate_info(endpoint) + if endpoint + display "Certificate details:" + display_certificate_info(endpoint) + else + error "No certificate found." + end end # certs:remove @@ -153,6 +158,40 @@ def rollback display_certificate_info(endpoint) end + # certs:generate DOMAIN + # + # Generate a key and certificate signing request (or self-signed certificate) + # for an app. Prompts for information to put in the certificate unless --now + # is used, or at least one of the --subject, --owner, --country, --area, or + # --city options is specified. + # + # --selfsigned # generate a self-signed certificate instead of a CSR + # --keysize BITSIZE # RSA key size in bits (default: 2048) + # --owner NAME # name of organization certificate belongs to + # --country COUNTRY # country of owner, as a two-letter ISO country code + # --area AREA # sub-country area (state, province, etc.) of owner + # --city CITY # city of owner + # --subject SUBJECT # specify entire certificate subject + # --now # do not prompt for any owner information + def generate + request = Heroku::OpenSSL::CertificateRequest.new + + request.domain = args[0] || error("certs:generate must specify a domain") + request.subject = cert_subject_for_domain_and_options(request.domain, options) + request.self_signed = options[:selfsigned] || false + request.key_size = (options[:keysize] || request.key_size).to_i + + result = request.generate + + explain_step_after_generate result + + rescue Heroku::OpenSSL::NotInstalledError => ex + error("The OpenSSL command-line tools must be installed to use certs:generate.\n" + ex.installation_hint) + + rescue Heroku::OpenSSL::GenericError => ex + error(ex.message) + end + private def current_endpoint @@ -198,8 +237,8 @@ def post_to_ssl_doctor(path, action_text = nil) input = args.map { |arg| begin certbody=File.read(arg) - rescue Exception => e - error("Unable to read #{arg} file: #{e}") + rescue => e + error("Unable to read #{arg} file: #{e}") end certbody }.join("\n") @@ -212,7 +251,7 @@ def post_to_ssl_doctor(path, action_text = nil) def read_crt_and_key_through_ssl_doctor(action_text = nil) crt_and_key = post_to_ssl_doctor("resolve-chain-and-key", action_text) - Heroku::OkJson.decode(crt_and_key).values_at("pem", "key") + MultiJson.load(crt_and_key).values_at("pem", "key") end def read_crt_through_ssl_doctor(action_text = nil) @@ -230,4 +269,66 @@ def read_crt_and_key options[:bypass] ? read_crt_and_key_bypassing_ssl_doctor : read_crt_and_key_through_ssl_doctor end + def all_endpoint_domains + endpoints = heroku.ssl_endpoint_list(app) + endpoints.select { |endpoint| endpoint['ssl_cert'] && endpoint['ssl_cert']['cert_domains'] } \ + .map { |endpoint| endpoint['ssl_cert']['cert_domains'] } \ + .reduce(:+) + end + + def prompt(question) + display("#{question}: ", false) + ask + end + + def val_empty?(val) + val.nil? or val.empty? + end + + def cert_subject_for_domain_and_options(domain, options = {}) + raise ArgumentError, "domain cannot be empty" if domain.nil? || domain.empty? + + subject, country, area, city, owner, now = options.values_at(:subject, :country, :area, :city, :owner, :now) + + if val_empty? subject + if !now && [country, area, city, owner].all? { |v| val_empty? v } + owner = prompt "Owner of this certificate" + country = prompt "Country of owner (two-letter ISO code)" + area = prompt "State/province/etc. of owner" + city = prompt "City of owner" + end + + subject = "" + subject += "/C=#{country}" unless val_empty? country + subject += "/ST=#{area}" unless val_empty? area + subject += "/L=#{city}" unless val_empty? city + subject += "/O=#{owner}" unless val_empty? owner + + subject += "/CN=#{domain}" + end + + subject + end + + def explain_step_after_generate(result) + if result.csr_file.nil? + display "Your key and self-signed certificate have been generated." + display "Next, run:" + else + display "Your key and certificate signing request have been generated." + display "Submit the CSR in '#{result.csr_file}' to your preferred certificate authority." + display "When you've received your certificate, run:" + end + + needs_addon = false + command = "add" + begin + command = "update" if all_endpoint_domains.include? result.request.domain + rescue RestClient::Forbidden + needs_addon = true + end + + display "$ heroku addons:add ssl:endpoint" if needs_addon + display "$ heroku certs:#{command} #{result.crt_file || "CERTFILE"} #{result.key_file}" + end end diff --git a/lib/heroku/command/config.rb b/lib/heroku/command/config.rb index e3d160372..72e322439 100644 --- a/lib/heroku/command/config.rb +++ b/lib/heroku/command/config.rb @@ -1,4 +1,6 @@ require "heroku/command/base" +require "shellwords" + # manage app config vars # @@ -23,14 +25,25 @@ class Heroku::Command::Config < Heroku::Command::Base def index validate_arguments! - vars = api.get_config_vars(app).body + vars = if options[:shell] + api.get_config_vars(app).body + else + api.request( + :expects => 200, + :method => :get, + :path => "/apps/#{app}/config_vars", + :query => { "symbolic" => true } + ).body + end + if vars.empty? display("#{app} has no config vars.") else vars.each {|key, value| vars[key] = value.to_s} if options[:shell] vars.keys.sort.each do |key| - display(%{#{key}=#{vars[key]}}) + out = $stdout.tty? ? Shellwords.shellescape(vars[key]) : vars[key] + display(%{#{key}=#{out}}) end else styled_header("#{app} Config Vars") @@ -55,14 +68,15 @@ def index # B: two # def set + requires_preauth unless args.size > 0 and args.all? { |a| a.include?('=') } error("Usage: heroku config:set KEY1=VALUE1 [KEY2=VALUE2 ...]\nMust specify KEY and VALUE to set.") end - vars = args.inject({}) do |vars, arg| + vars = args.inject({}) do |v, arg| key, value = arg.split('=', 2) - vars[key] = value - vars + v[key] = value + v end action("Setting config vars and restarting #{app}") do @@ -72,7 +86,7 @@ def set if release = api.get_release(app, 'current').body release['name'] end - rescue Heroku::API::Errors::RequestFailed => e + rescue Heroku::API::Errors::RequestFailed end end @@ -86,6 +100,8 @@ def set # # display a config value for an app # + # -s, --shell # output config var in shell format + # #Examples: # # $ heroku config:get A @@ -99,7 +115,12 @@ def get vars = api.get_config_vars(app).body key, value = vars.detect {|k,v| k == key} - display(value.to_s) + if options[:shell] && value + out = $stdout.tty? ? Shellwords.shellescape(value) : value + display("#{key}=#{out}") + else + display(value.to_s) + end end # config:unset KEY1 [KEY2 ...] @@ -114,6 +135,7 @@ def get # Unsetting B and restarting example... done, v124 # def unset + requires_preauth if args.empty? error("Usage: heroku config:unset KEY1 [KEY2 ...]\nMust specify KEY to unset.") end @@ -126,7 +148,7 @@ def unset if release = api.get_release(app, 'current').body release['name'] end - rescue Heroku::API::Errors::RequestFailed => e + rescue Heroku::API::Errors::RequestFailed end end end diff --git a/lib/heroku/command/drains.rb b/lib/heroku/command/drains.rb index f22dd43ec..f3cd75f81 100644 --- a/lib/heroku/command/drains.rb +++ b/lib/heroku/command/drains.rb @@ -2,13 +2,13 @@ module Heroku::Command - # display syslog drains for an app + # display drains for an app # class Drains < Base # drains # - # list all syslog drains + # list all drains # def index puts heroku.list_drains(app) @@ -17,7 +17,7 @@ def index # drains:add URL # - # add a syslog drain + # add a drain # def add if url = args.shift @@ -30,7 +30,7 @@ def add # drains:remove URL # - # remove a syslog drain + # remove a drain # def remove if url = args.shift @@ -43,4 +43,3 @@ def remove end end - diff --git a/lib/heroku/command/features.rb b/lib/heroku/command/features.rb new file mode 100644 index 000000000..eead80b74 --- /dev/null +++ b/lib/heroku/command/features.rb @@ -0,0 +1,141 @@ +require "heroku/command/base" + +# manage optional features +# +class Heroku::Command::Features < Heroku::Command::Base + + # features + # + # list available features + # + #Example: + # + # === App Features (glacial-retreat-5913) + # [ ] preboot Provide seamless web dyno deploys + # + def index + validate_arguments! + + app_features = api.get_features(app).body.select do |feature| + feature["kind"] == "app" && feature["state"] == "general" + end + + app_features.sort_by! do |feature| + feature["name"] + end + + display_app = app || "no app specified" + + styled_header "App Features (#{display_app})" + display_features app_features + end + + alias_command "features:list", "features" + + # features:info FEATURE + # + # displays additional information about FEATURE + # + #Example: + # + # $ heroku features:info preboot + # === preboot + # Docs: https://devcenter.heroku.com/articles/preboot + # Summary: Provide seamless web dyno deploys + # + def info + unless feature_name = shift_argument + error("Usage: heroku features:info FEATURE\nMust specify FEATURE for info.") + end + validate_arguments! + + feature_data = api.get_feature(feature_name, app).body + styled_header(feature_data['name']) + styled_hash({ + 'Summary' => feature_data['summary'], + 'Docs' => feature_data['docs'] + }) + end + + # features:disable FEATURE + # + # disables a feature + # + #Example: + # + # $ heroku features:disable preboot + # Disabling preboot feature for me@example.org... done + # + def disable + feature_name = shift_argument + error "Usage: heroku features:disable FEATURE\nMust specify FEATURE to disable." unless feature_name + validate_arguments! + + feature = api.get_features(app).body.detect { |f| f["name"] == feature_name } + message = "Disabling #{feature_name} " + + error "No such feature: #{feature_name}" unless feature + + if feature["kind"] == "user" + message += "for #{Heroku::Auth.user}" + else + error "Must specify an app" unless app + message += "for #{app}" + end + + action message do + api.delete_feature feature_name, app + end + end + + # features:enable FEATURE + # + # enables an feature + # + #Example: + # + # $ heroku features:enable preboot + # Enabling preboot feature for me@example.org... done + # + def enable + feature_name = shift_argument + error "Usage: heroku features:enable FEATURE\nMust specify FEATURE to enable." unless feature_name + validate_arguments! + + feature = api.get_features.body.detect { |f| f["name"] == feature_name } + message = "Enabling #{feature_name} " + + error "No such feature: #{feature_name}" unless feature + + if feature["kind"] == "user" + message += "for #{Heroku::Auth.user}" + else + error "Must specify an app" unless app + message += "for #{app}" + end + + feature_data = action(message) do + api.post_feature(feature_name, app).body + end + + display "For more information see: #{feature_data["docs"]}" if feature_data["docs"] + end + +private + + # app is not required for these commands, so rescue if there is none + def app + super + rescue Heroku::Command::CommandFailed + nil + end + + def display_features(features) + longest_name = features.map { |f| f["name"].to_s.length }.sort.last + features.each do |feature| + toggle = feature["enabled"] ? "[+]" : "[ ]" + display "%s %-#{longest_name}s %s" % [ toggle, feature["name"], feature["summary"] ] + end + end + +end diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index f92bb2d29..11f209312 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -1,4 +1,3 @@ -require "heroku/api/releases_v3" require "heroku/command/base" module Heroku::Command @@ -7,174 +6,37 @@ module Heroku::Command # class Fork < Base - # fork [NEWNAME] + # fork # - # Fork an existing app -- copy config vars and Heroku Postgres data, and re-provision add-ons to a new app. + # --from FROM # app to fork from + # --to TO # app to create + # -s, --stack STACK # specify a stack for the new app + # --region REGION # specify a region + # --skip-pg # skip postgres databases + # + # Copy config vars and Heroku Postgres data, and re-provision add-ons to a new app. # New app name should not be an existing app. The new app will be created as part of the forking process. # - # -s, --stack STACK # specify a stack for the new app - # --region REGION # specify a region + #Example: # + # $ heroku fork --from my-production-app --to my-development-app + # Forking my-production-app... done. Forked to my-development-app + # Deploying 60a8b0f to my-development-app... done + # Adding addon memcachier:dev to my-development-app... done + # Adding addon heroku-postgresql:hobby-dev to my-development-app... done + # Transferring HEROKU_POSTGRESQL_AMBER to DATABASE... + # Progress: done + # Copying config vars: + # LANG + # RAILS_ENV + # RACK_ENV + # SECRET_KEY_BASE + # RAILS_SERVE_STATIC_FILES + # ... done + # Fork complete. View it at https://my-development-app.herokuapp.com/ def index - options[:ignore_no_org] = true - - from = app - to = shift_argument || "#{from}-#{(rand*1000).to_i}" - if from == to - raise Heroku::Command::CommandFailed.new("Cannot fork to the same app.") - end - - from_info = api.get_app(from).body - - to_info = action("Creating fork #{to}", :org => !!org) do - params = { - "name" => to, - "region" => options[:region] || from_info["region"], - "stack" => options[:stack] || from_info["stack"], - "tier" => from_info["tier"] == "legacy" ? "production" : from_info["tier"] - } - - info = if org - org_api.post_app(params, org).body - else - api.post_app(params).body - end - end - - action("Copying slug") do - copy_slug(from, to) - end - - from_config = api.get_config_vars(from).body - from_addons = api.get_addons(from).body - - from_addons.each do |addon| - print "Adding #{addon["name"]}... " - begin - to_addon = api.post_addon(to, addon["name"]).body - puts "done" - rescue Heroku::API::Errors::RequestFailed => ex - puts "skipped (%s)" % json_decode(ex.response.body)["error"] - rescue Heroku::API::Errors::NotFound - puts "skipped (not found)" - end - if addon["name"] =~ /^heroku-postgresql:/ - from_var_name = "#{addon["attachment_name"]}_URL" - from_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - if from_config[from_var_name] == from_config["DATABASE_URL"] - from_config["DATABASE_URL"] = api.get_config_vars(to).body["#{from_attachment}_URL"] - end - from_config.delete(from_var_name) - - plan = addon["name"].split(":").last - unless %w(dev basic hobby-dev hobby-basic).include? plan - wait_for_db to, to_addon - end - - check_for_pgbackups! from - check_for_pgbackups! to - migrate_db addon, from, to_addon, to - end - end - - to_config = api.get_config_vars(to).body - - action("Copying config vars") do - diff = from_config.inject({}) do |ax, (key, val)| - ax[key] = val unless to_config[key] - ax - end - api.put_config_vars to, diff - end - - puts "Fork complete, view it at #{to_info['web_url']}" - rescue Exception => e - raise if e.is_a?(Heroku::Command::CommandFailed) - - puts "Failed to fork app #{from} to #{to}." - message = "WARNING: Potentially Destructive Action\nThis command will destroy #{to} (including all add-ons)." - - if confirm_command(to, message) - action("Deleting #{to}") do - begin - api.delete_app(to) - rescue Heroku::API::Errors::NotFound - end - end - end - puts "Original exception below:" - raise e + Heroku::JSPlugin.install('heroku-fork') + Heroku::JSPlugin.run('fork', nil, ARGV[1..-1]) end - - private - - def copy_slug(from, to) - from_releases = api.get_releases_v3(from, 'version ..; order=desc,max=1;').body - raise Heroku::Command::CommandFailed.new("No releases on #{from}") if from_releases.empty? - from_slug = from_releases.first.fetch('slug', {}) - raise Heroku::Command::CommandFailed.new("No slug on #{from}") unless from_slug - api.post_release_v3(to, from_slug["id"], "Forked from #{from}") - end - - def check_for_pgbackups!(app) - unless api.get_addons(app).body.detect { |addon| addon["name"] =~ /^pgbackups:/ } - action("Adding pgbackups:plus to #{app}") do - api.post_addon app, "pgbackups:plus" - end - end - end - - def migrate_db(from_addon, from, to_addon, to) - transfer = nil - - action("Transferring database (this can take some time)") do - from_config = api.get_config_vars(from).body - from_attachment = from_addon["attachment_name"] - to_config = api.get_config_vars(to).body - to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - - pgb = Heroku::Client::Pgbackups.new(from_config["PGBACKUPS_URL"]) - transfer = pgb.create_transfer( - from_config["#{from_attachment}_URL"], - from_attachment, - to_config["#{to_attachment}_URL"], - to_attachment, - :expire => "true") - - error transfer["errors"].values.flatten.join("\n") if transfer["errors"] - loop do - transfer = pgb.get_transfer(transfer["id"]) - error transfer["errors"].values.flatten.join("\n") if transfer["errors"] - break if transfer["finished_at"] - sleep 1 - end - print " " - end - end - - def pg_api - require "rest_client" - host = "postgres-api.heroku.com" - RestClient::Resource.new "https://#{host}/client/v11/databases", Heroku::Auth.user, Heroku::Auth.password - end - - def wait_for_db(app, attachment) - attachments = api.get_attachments(app).body.inject({}) { |ax,att| ax.update(att["name"] => att["resource"]["name"]) } - attachment_name = attachment["message"].match(/Attached as (\w+)_URL\n/)[1] - action("Waiting for database to be ready (this can take some time)") do - loop do - begin - waiting = json_decode(pg_api["#{attachments[attachment_name]}/wait_status"].get.to_s)["waiting?"] - break unless waiting - sleep 5 - rescue RestClient::ResourceNotFound - rescue Interrupt - exit 0 - end - end - print " " - end - end - end end diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index b54e80076..2ccfb3b77 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -4,31 +4,26 @@ # class Heroku::Command::Git < Heroku::Command::Base - # git:clone APP [DIRECTORY] + # git:clone [DIRECTORY] # # clones a heroku app to your local machine at DIRECTORY (defaults to app name) # + # -a, --app APP # the Heroku app to use # -r, --remote REMOTE # the git remote to create, default "heroku" + # --ssh-git # use SSH git protocol + # --http-git # HIDDEN: Use HTTP git protocol + # # #Examples: # - # $ heroku git:clone example - # Cloning from app 'example'... + # $ heroku git:clone -a example # Cloning into 'example'... # remote: Counting objects: 42, done. # ... # def clone - remote = options[:remote] || "heroku" - - name = options[:app] || shift_argument || error("Usage: heroku git:clone APP [DIRECTORY]") - directory = shift_argument - validate_arguments! - - git_url = api.get_app(name).body["git_url"] - - puts "Cloning from app '#{name}'..." - system "git clone -o #{remote} #{git_url} #{directory}".strip + Heroku::JSPlugin.install('heroku-git') + Heroku::JSPlugin.run('git', 'clone', ARGV[1..-1]) end alias_command "clone", "git:clone" @@ -39,26 +34,18 @@ def clone # # if OPTIONS are specified they will be passed to git remote add # + # -a, --app APP # the Heroku app to use # -r, --remote REMOTE # the git remote to create, default "heroku" + # --ssh-git # use SSH git protocol + # --http-git # HIDDEN: Use HTTP git protocol # #Examples: # # $ heroku git:remote -a example - # Git remote heroku added - # - # $ heroku git:remote -a example - # ! Git remote heroku already exists + # set git remote heroku to https://git.heroku.com/example.git # def remote - git_options = args.join(" ") - remote = options[:remote] || 'heroku' - - if git('remote').split("\n").include?(remote) - error("Git remote #{remote} already exists") - else - app_data = api.get_app(app).body - create_git_remote(remote, app_data['git_url']) - end + Heroku::JSPlugin.install('heroku-git') + Heroku::JSPlugin.run('git', 'remote', ARGV[1..-1]) end - end diff --git a/lib/heroku/command/help.rb b/lib/heroku/command/help.rb index 2fa8449e2..d3488984d 100644 --- a/lib/heroku/command/help.rb +++ b/lib/heroku/command/help.rb @@ -5,7 +5,7 @@ # class Heroku::Command::Help < Heroku::Command::Base - PRIMARY_NAMESPACES = %w( auth apps ps run addons config releases domains logs sharing ) + PRIMARY_NAMESPACES = %w( auth apps ps run restart addons config releases domains logs sharing ) include Heroku::Deprecated::Help @@ -96,6 +96,7 @@ def skip_namespace?(ns) def skip_command?(command) return true if command[:help] =~ /DEPRECATED:/ return true if command[:help] =~ /^ HIDDEN:/ + return true if command[:hidden] false end diff --git a/lib/heroku/command/labs.rb b/lib/heroku/command/labs.rb index beb5a1da3..f9918c5e9 100644 --- a/lib/heroku/command/labs.rb +++ b/lib/heroku/command/labs.rb @@ -26,6 +26,9 @@ def index feature["kind"] == "user" end + # general availability features are managed via `settings`, not `labs` + app_features.reject! { |f| f["state"] == "general" } + display_app = app || "no app specified" styled_header "User Features (#{Heroku::Auth.user})" diff --git a/lib/heroku/command/local.rb b/lib/heroku/command/local.rb new file mode 100644 index 000000000..bcd7e7dbe --- /dev/null +++ b/lib/heroku/command/local.rb @@ -0,0 +1,31 @@ +require "heroku/command/base" + +module Heroku::Command + + # run heroku app locally + class Local < Base + + # local [PROCESSNAME] + # + # run heroku app locally + # + # Start the application specified by a Procfile (defaults to ./Procfile) + # + # Examples: + # + # heroku local + # heroku local web + # heroku local -f Procfile.test -e .env.test + # + # -f, --procfile PROCFILE + # -e, --env ENV + # -c, --concurrency CONCURRENCY + # -p, --port PORT + # -r, --r + # + def index + Heroku::JSPlugin.install('heroku-local') + Heroku::JSPlugin.run('local', nil, ARGV[1..-1]) + end + end +end diff --git a/lib/heroku/command/logs.rb b/lib/heroku/command/logs.rb index f4c7b776f..efe581d66 100644 --- a/lib/heroku/command/logs.rb +++ b/lib/heroku/command/logs.rb @@ -13,6 +13,7 @@ class Heroku::Command::Logs < Heroku::Command::Base # -p, --ps PS # only display logs from the given process # -s, --source SOURCE # only display logs from the given source # -t, --tail # continually stream logs + # --force-colors # Force use of ANSI color characters (even on non-tty outputs) # #Example: # @@ -29,7 +30,7 @@ def index opts << "ps=#{URI.encode(options[:ps])}" if options[:ps] opts << "source=#{URI.encode(options[:source])}" if options[:source] - log_displayer = ::Heroku::Helpers::LogDisplayer.new(heroku, app, opts) + log_displayer = ::Heroku::Helpers::LogDisplayer.new(heroku, app, opts, options[:force_colors]) log_displayer.display_logs end diff --git a/lib/heroku/command/maintenance.rb b/lib/heroku/command/maintenance.rb index eb7d887a4..e264f1dce 100644 --- a/lib/heroku/command/maintenance.rb +++ b/lib/heroku/command/maintenance.rb @@ -14,14 +14,8 @@ class Heroku::Command::Maintenance < Heroku::Command::Base # off # def index - validate_arguments! - - case api.get_app_maintenance(app).body['maintenance'] - when true - display('on') - when false - display('off') - end + Heroku::JSPlugin.install('heroku-apps') + Heroku::JSPlugin.run('maintenance', nil, ARGV[1..-1]) end # maintenance:on @@ -34,11 +28,8 @@ def index # Enabling maintenance mode for example # def on - validate_arguments! - - action("Enabling maintenance mode for #{app}") do - api.post_app_maintenance(app, '1') - end + Heroku::JSPlugin.install('heroku-apps') + Heroku::JSPlugin.run('maintenance', 'on', ARGV[1..-1]) end # maintenance:off @@ -51,11 +42,8 @@ def on # Disabling maintenance mode for example # def off - validate_arguments! - - action("Disabling maintenance mode for #{app}") do - api.post_app_maintenance(app, '0') - end + Heroku::JSPlugin.install('heroku-apps') + Heroku::JSPlugin.run('maintenance', 'off', ARGV[1..-1]) end end diff --git a/lib/heroku/command/orgs.rb b/lib/heroku/command/orgs.rb index 5800b3750..8759a21e6 100644 --- a/lib/heroku/command/orgs.rb +++ b/lib/heroku/command/orgs.rb @@ -22,13 +22,10 @@ def index end end - default = response['user']['default_organization'] || "" - orgs.map! do |org| name = org["organization_name"] t = [] t << org["role"] - t << 'default' if name == default [name, t.join(', ')] end @@ -48,33 +45,10 @@ def open launchy("Opening web interface for #{org}", "https://dashboard.heroku.com/orgs/#{org}/apps") end - # orgs:default [TARGET] - # - # sets the default org. - # TARGET can be an org you belong to or it can be "personal" - # for your personal account. If no argument or option is given, - # the default org is displayed - # + # HIDDEN: orgs:default # def default - options[:ignore_no_org] = true - if target = shift_argument - options[:org] = target - end - - if org == "personal" || options[:personal] - action("Setting personal account as default") do - org_api.remove_default_org - end - elsif org && !options[:using_default_org] - action("Setting #{org} as the default organization") do - org_api.set_default_org(org) - end - elsif org - display("#{org} is the default organization.") - else - display("Personal account is default.") - end + error("orgs:default is no longer in the CLI.\nUse the HEROKU_ORGANIZATION environment variable instead.\nSee https://devcenter.heroku.com/articles/develop-orgs#default-org for more info.") end end diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index ce46abee7..37ce69ea1 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -4,21 +4,31 @@ require "heroku/command/base" require "heroku/helpers/heroku_postgresql" require "heroku/helpers/pg_dump_restore" - +require "heroku/helpers/addons/resolve" +require "heroku/helpers/addons/api" require "heroku/helpers/pg_diagnose" # manage heroku-postgresql databases # class Heroku::Command::Pg < Heroku::Command::Base + module Hooks + extend self + def set_commands(shorthand) + '' + end + end include Heroku::Helpers::HerokuPostgresql include Heroku::Helpers::PgDiagnose + include Heroku::Helpers::Addons::Resolve + include Heroku::Helpers::Addons::API # pg # # list databases for an app # def index + requires_preauth validate_arguments! if hpg_databases_with_info.empty? @@ -41,6 +51,7 @@ def index def info db = shift_argument validate_arguments! + requires_preauth if db @resolver = generate_resolver @@ -58,6 +69,7 @@ def info # defaults to DATABASE_URL databases if no DATABASE is specified # if REPORT_ID is specified instead, a previous report is displayed def diagnose + requires_preauth db_id = shift_argument run_diagnose(db_id) end @@ -67,15 +79,40 @@ def diagnose # sets DATABASE as your DATABASE_URL # def promote + requires_preauth unless db = shift_argument error("Usage: heroku pg:promote DATABASE\nMust specify DATABASE to promote.") end validate_arguments! - attachment = generate_resolver.resolve(db) + db = db.sub(/_URL$/, '') # allow promoting with a var name + addon = resolve_addon!(db) { |addon| addon['addon_service']['name'] == 'heroku-postgresql' } + + promoted_name = 'DATABASE' + + action "Ensuring an alternate alias for existing #{promoted_name}" do + backup = find_or_create_non_database_attachment(app) + + if backup + @status = backup['name'] + else + @status = "not needed" + end - action "Promoting #{attachment.display_name} to DATABASE_URL" do - hpg_promote(attachment.url) + end + + action "Promoting #{addon['name']} to #{promoted_name}_URL on #{app}" do + request( + :body => json_encode({ + "app" => {"name" => app}, + "addon" => {"name" => addon['name']}, + "confirm" => app, + "name" => promoted_name + }), + :expects => 201, + :method => :post, + :path => "/addon-attachments" + ) end end @@ -88,6 +125,7 @@ def promote # defaults to DATABASE_URL databases if no DATABASE is specified # def psql + requires_preauth attachment = generate_resolver.resolve(shift_argument, "DATABASE_URL") validate_arguments! @@ -95,15 +133,17 @@ def psql begin ENV["PGPASSWORD"] = uri.password ENV["PGSSLMODE"] = 'require' + ENV["PGAPPNAME"] = "#{pgappname} interactive" if command = options[:command] command = %Q(-c "#{command}") end shorthand = "#{attachment.app}::#{attachment.name.sub(/^HEROKU_POSTGRESQL_/,'').gsub(/\W+/, '-')}" + set_commands = Hooks.set_commands(shorthand) prompt_expr = "#{shorthand}%R%# " prompt_flags = %Q(--set "PROMPT1=#{prompt_expr}" --set "PROMPT2=#{prompt_expr}") puts "---> Connecting to #{attachment.display_name}" - exec "psql -U #{uri.user} -h #{uri.host} -p #{uri.port || 5432} #{prompt_flags} #{command} #{uri.path[1..-1]}" + exec "psql -U #{uri.user} -h #{uri.host} -p #{uri.port || 5432} #{set_commands} #{prompt_flags} #{command} #{uri.path[1..-1]}" rescue Errno::ENOENT output_with_bang "The local psql command could not be located" output_with_bang "For help installing psql, see http://devcenter.heroku.com/articles/local-postgresql" @@ -116,6 +156,7 @@ def psql # delete all data in DATABASE # def reset + requires_preauth unless db = shift_argument error("Usage: heroku pg:reset DATABASE\nMust specify DATABASE to reset.") end @@ -137,6 +178,7 @@ def reset # stop a replica from following and make it a read/write database # def unfollow + requires_preauth unless db = shift_argument error("Usage: heroku pg:unfollow REPLICA\nMust specify REPLICA to unfollow.") end @@ -169,15 +211,20 @@ def unfollow # # defaults to all databases if no DATABASE is specified # + # --wait-interval SECONDS # how frequently to poll (to avoid rate-limiting) + # def wait + requires_preauth db = shift_argument validate_arguments! + interval = options[:wait_interval].to_i + interval = 1 if interval < 1 if db - wait_for generate_resolver.resolve(db) + wait_for(generate_resolver.resolve(db), interval) else generate_resolver.all_databases.values.each do |attach| - wait_for(attach) + wait_for(attach, interval) end end end @@ -189,6 +236,7 @@ def wait # --reset # Reset credentials on the specified database. # def credentials + requires_preauth unless db = shift_argument error("Usage: heroku pg:credentials DATABASE\nMust specify DATABASE to display credentials.") end @@ -220,7 +268,10 @@ def credentials # # view active queries with execution time # + # -v,--verbose # also show idle connections + # def ps + requires_preauth sql = %Q( SELECT #{pid_column}, @@ -233,11 +284,15 @@ def ps WHERE #{query_column} <> '' #{ - if nine_two? - "AND state <> 'idle'" - else - "AND current_query <> ''" - end + # Apply idle-backend filter appropriate to versions and options. + case + when options[:verbose] + '' + when nine_two? + "AND state <> 'idle'" + else + "AND current_query <> ''" + end } AND #{pid_column} <> pg_backend_pid() ORDER BY query_start DESC @@ -253,6 +308,7 @@ def ps # -f,--force # terminates the connection in addition to cancelling the query # def kill + requires_preauth procpid = shift_argument output_with_bang "procpid to kill is required" unless procpid && procpid.to_i != 0 procpid = procpid.to_i @@ -268,6 +324,15 @@ def kill # terminates ALL connections # def killall + requires_preauth + db = args.first + attachment = generate_resolver.resolve(db, "DATABASE_URL") + client = hpg_client(attachment) + client.connection_reset + display "Connections terminated" + rescue StandardError + # fall back to original mechanism if calling the reset endpoint + # fails sql = %Q( SELECT pg_terminate_backend(#{pid_column}) FROM pg_stat_activity @@ -279,51 +344,54 @@ def killall end - # pg:push + # pg:push # - # push from LOCAL_SOURCE_DATABASE to REMOTE_TARGET_DATABASE + # push from SOURCE_DATABASE to REMOTE_TARGET_DATABASE # REMOTE_TARGET_DATABASE must be empty. + # + # SOURCE_DATABASE must be either the name of a database + # existing on your localhost or the fully qualified URL of + # a remote database. def push + requires_preauth local, remote = shift_argument, shift_argument unless [remote, local].all? Heroku::Command.run(current_command, ['--help']) exit(1) end - if local =~ %r(://) - error "LOCAL_SOURCE_DATABASE is not a valid database name" - end - remote_uri = generate_resolver.resolve(remote).url - local_uri = "postgres:///#{local}" + target_uri = resolve_heroku_url(remote) + source_uri = parse_db_url(local) pgdr = PgDumpRestore.new( - local_uri, - remote_uri, + source_uri, + target_uri, self) pgdr.execute end - # pg:pull + # pg:pull # - # pull from REMOTE_SOURCE_DATABASE to LOCAL_TARGET_DATABASE - # LOCAL_TARGET_DATABASE must not already exist. + # pull from REMOTE_SOURCE_DATABASE to TARGET_DATABASE + # TARGET_DATABASE must not already exist. + # + # TARGET_DATABASE will be created locally if it's a database name + # or remotely if it's a fully qualified URL. def pull + requires_preauth remote, local = shift_argument, shift_argument unless [remote, local].all? Heroku::Command.run(current_command, ['--help']) exit(1) end - if local =~ %r(://) - error "LOCAL_TARGET_DATABASE is not a valid database name" - end - remote_uri = generate_resolver.resolve(remote).url - local_uri = "postgres:///#{local}" + source_uri = resolve_heroku_url(remote) + target_uri = parse_db_url(local) pgdr = PgDumpRestore.new( - remote_uri, - local_uri, + source_uri, + target_uri, self) pgdr.execute @@ -339,9 +407,10 @@ def pull # window="" # set weekly UTC maintenance window for DATABASE # # eg: `heroku pg:maintenance window="Sunday 14:30"` def maintenance + requires_preauth mode_with_argument = shift_argument || '' mode, mode_argument = mode_with_argument.split('=') - p [mode, mode_argument] + db = shift_argument no_maintenance = options[:force] if mode.nil? || db.nil? || !(%w[info run window].include? mode) @@ -382,6 +451,7 @@ def maintenance # unfollow a database and upgrade it to the latest PostgreSQL version # def upgrade + requires_preauth unless db = shift_argument error("Usage: heroku pg:upgrade REPLICA\nMust specify REPLICA to upgrade.") end @@ -422,11 +492,25 @@ def upgrade private + def resolve_heroku_url(remote) + generate_resolver.resolve(remote).url + end + def generate_resolver app_name = app rescue nil # will raise if no app, but calling app reads in arguments Resolver.new(app_name, api) end + # Parse string database parameter and return string database URL. + # + # @param db_string [String] The local database name or a full connection URL, e.g. `my_db` or `postgres://user:pass@host:5432/my_db` + # @return [String] A full database connection URL. + def parse_db_url(db_string) + return db_string if db_string =~ %r(://) + + "postgres:///#{db_string}" + end + def display_db(name, db) styled_header(name) @@ -444,6 +528,10 @@ def display_db(name, db) display end + def in_maintenance?(app) + api.get_app_maintenance(app).body['maintenance'] + end + def hpg_client(attachment) Heroku::Client::HerokuPostgresql.new(attachment) end @@ -454,7 +542,8 @@ def hpg_databases_with_info @resolver = generate_resolver dbs = @resolver.all_databases - unique_dbs = dbs.reject { |config, att| 'DATABASE_URL' == config }.map{|config, att| att}.compact + has_promoted = dbs.any? { |_, att| att.primary_attachment? } + unique_dbs = dbs.reject { |var, _| has_promoted && 'DATABASE_URL' == var }.map{|_, att| att}.compact db_infos = {} mutex = Mutex.new @@ -492,17 +581,17 @@ def hpg_info_display(item) end end - def ticking + def ticking(interval) ticks = 0 loop do yield(ticks) ticks +=1 - sleep 1 + sleep interval end end - def wait_for(attach) - ticking do |ticks| + def wait_for(attach, interval) + ticking(interval) do |ticks| status = hpg_client(attach).get_wait_status error status[:message] if status[:error?] break if !status[:waiting?] && ticks.zero? @@ -530,7 +619,9 @@ def find_uri def version return @version if defined? @version - @version = exec_sql("select version();").match(/PostgreSQL (\d+\.\d+\.\d+) on/)[1] + result = exec_sql("select version();").match(/PostgreSQL (\d+\.\d+\.\d+) on/) + fail("Unable to determine Postgres version") unless result + @version = result[1] end def nine_two? @@ -563,8 +654,13 @@ def exec_sql_on_uri(sql,uri) begin ENV["PGPASSWORD"] = uri.password ENV["PGSSLMODE"] = (uri.host == 'localhost' ? 'prefer' : 'require' ) + ENV["PGAPPNAME"] = "#{pgappname} non-interactive" user_part = uri.user ? "-U #{uri.user}" : "" - `psql -c "#{sql}" #{user_part} -h #{uri.host} -p #{uri.port || 5432} #{uri.path[1..-1]}` + output = `#{psql_cmd} -c "#{sql}" #{user_part} -h #{uri.host} -p #{uri.port || 5432} #{uri.path[1..-1]}` + if (! $?.success?) || output.nil? || output.empty? + raise "psql failed. exit status #{$?.to_i}, output: #{output.inspect}" + end + output rescue Errno::ENOENT output_with_bang "The local psql command could not be located" output_with_bang "For help installing psql, see https://devcenter.heroku.com/articles/heroku-postgresql#local-setup" @@ -572,4 +668,53 @@ def exec_sql_on_uri(sql,uri) end end + def pgappname + if running_on_windows? + 'psql (windows)' + else + "psql #{`whoami`.chomp.gsub(/\W/,'')}" + end + end + + def psql_cmd + # some people alais psql, so we need to find the real psql + # but windows doesn't have the command command + running_on_windows? ? 'psql' : 'command psql' + end + + # Finds or creates a non-DATABASE attachment for the DB currently + # attached as DATABASE. + # + # If current DATABASE is attached by other names, return one of them. + # If current DATABASE is only attachment, create a new one and return it. + # If no current DATABASE, return nil. + def find_or_create_non_database_attachment(app) + attachments = get_attachments(:app => app) + + current_attachment = attachments.detect { |att| att['name'] == 'DATABASE' } + current_addon = current_attachment && current_attachment['addon'] + + if current_addon + existing = attachments. + select { |att| att['addon']['id'] == current_addon['id'] }. + detect { |att| att['name'] != 'DATABASE' } + + return existing if existing + + # The current add-on occupying the DATABASE attachment has no + # other attachments. In order to promote this database without + # error, we can create a secondary attachment, just-in-time. + request( + # Note: no attachment name provided; let the API choose one + :body => json_encode({ + "app" => {"name" => app}, + "addon" => {"name" => current_addon['name']}, + "confirm" => app + }), + :expects => 201, + :method => :post, + :path => "/addon-attachments" + ) + end + end end diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb new file mode 100644 index 000000000..5a1db5af8 --- /dev/null +++ b/lib/heroku/command/pg_backups.rb @@ -0,0 +1,619 @@ +require "heroku/client/heroku_postgresql" +require "heroku/client/heroku_postgresql_backups" +require "heroku/command/base" +require "heroku/helpers/heroku_postgresql" + +class Heroku::Command::Pg < Heroku::Command::Base + # pg:copy SOURCE TARGET + # + # Copy all data from source database to target. At least one of + # these must be a Heroku Postgres database. + def copy + source_db = shift_argument + target_db = shift_argument + + validate_arguments! + + source = resolve_db_or_url(source_db) + target = resolve_db_or_url(target_db) + + if source.url == target.url + abort("Cannot copy database to itself") + end + + attachment = target.attachment || source.attachment + + message = "WARNING: Destructive Action" + message << "\nThis command will remove all data from #{target.name}" + message << "\nData from #{source.name} will then be transferred to #{target.name}" + message << "\nThis command will affect the app: #{app}" + + if confirm_command(app, message) + xfer = hpg_client(attachment).pg_copy(source.name, source.url, + target.name, target.url) + poll_transfer('copy', xfer[:uuid]) + end + end + + # pg:backups [subcommand] + # + # Interact with built-in backups. Without a subcommand, it lists all + # available backups. The subcommands available are: + # + # info BACKUP_ID # get information about a specific backup + # capture DATABASE # capture a new backup + # restore [[BACKUP_ID] DATABASE] # restore a backup (default latest) to a database (default DATABASE_URL) + # public-url BACKUP_ID # get secret but publicly accessible URL for BACKUP_ID to download it + # -q, --quiet # Hide expiration message (for use in scripts) + # cancel [BACKUP_ID] # cancel an in-progress backup or restore (default newest) + # delete BACKUP_ID # delete an existing backup + # schedule DATABASE # schedule nightly backups for given database + # --at ':00 ' # at a specific (24h clock) hour in the given timezone + # unschedule SCHEDULE # stop nightly backups on this schedule + # schedules # list backup schedule + def backups + if args.count == 0 + list_backups + else + command = shift_argument + case command + when 'list' then list_backups + when 'info' then backup_status + when 'capture' then capture_backup + when 'restore' then restore_backup + when 'public-url' then public_url + when 'cancel' then cancel_backup + when 'delete' then delete_backup + when 'schedule' then schedule_backups + when 'unschedule' then unschedule_backups + when 'schedules' then list_schedules + else abort "Unknown pg:backups command: #{command}" + end + end + end + + private + + MaybeAttachment = Struct.new(:name, :url, :attachment) + + def url_name(uri) + "Database #{uri.path[1..-1]} on #{uri.host}:#{uri.port || 5432}" + end + + def resolve_db_or_url(name_or_url, default=nil) + if name_or_url =~ %r{postgres://} + url = name_or_url + uri = URI.parse(url) + name = url_name(uri) + MaybeAttachment.new(name, url, nil) + else + attachment = generate_resolver.resolve(name_or_url, default) + name = attachment.config_var.sub(/^HEROKU_POSTGRESQL_/, '').sub(/_URL$/, '') + MaybeAttachment.new(name, attachment.url, attachment) + end + end + + def arbitrary_app_db + generate_resolver.all_databases.values.first + end + + def transfer_name(transfer) + old_pgb_name = transfer.has_key?(:options) && transfer[:options]["pgbackups_name"] + + if old_pgb_name + "o#{old_pgb_name}" + else + transfer_num = transfer[:num] + from_type, to_type = transfer[:from_type], transfer[:to_type] + prefix = if from_type == 'pg_dump' && to_type != 'pg_restore' + transfer.has_key?(:schedule) ? 'a' : 'b' + elsif from_type != 'pg_dump' && to_type == 'pg_restore' + 'r' + elsif from_type == 'pg_dump' && to_type == 'pg_restore' + 'c' + else + 'b' + end + "#{prefix}#{format("%03d", transfer_num)}" + end + end + + def transfer_num(transfer_name) + if /\A[abcr](\d+)\z/.match(transfer_name) + $1.to_i + elsif /\Ao[ab]\d+\z/.match(transfer_name) + xfer = hpg_app_client(app).transfers.find do |t| + transfer_name(t) == transfer_name + end + xfer[:num] unless xfer.nil? + end + end + + def transfer_status(t) + if t[:finished_at] && t[:succeeded] + "Finished #{t[:finished_at]}" + elsif t[:finished_at] && !t[:succeeded] + "Failed #{t[:finished_at]}" + elsif t[:started_at] + "Running (processed #{size_pretty(t[:processed_bytes])})" + else + "Pending" + end + end + + def size_pretty(bytes) + suffixes = [ + ['B', 1], + ['kB', 1_000], + ['MB', 1_000_000], + ['GB', 1_000_000_000], + ['TB', 1_000_000_000_000] # (ohdear) + ] + suffix, multiplier = suffixes.find do |k,v| + normalized = bytes / v.to_f + normalized >= 0 && normalized < 1_000 + end + if suffix.nil? + return bytes + end + normalized = bytes / multiplier.to_f + num_digits = case + when normalized >= 100 then '0' + when normalized >= 10 then '1' + else '2' + end + fmt_str = "%.#{num_digits}f#{suffix}" + format(fmt_str, normalized) + end + + def list_backups + validate_arguments! + transfers = hpg_app_client(app).transfers + + display "=== Backups" + display_backups = transfers.select do |b| + b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' + end.sort_by { |b| b[:created_at] }.reverse.map do |b| + { + "id" => transfer_name(b), + "created_at" => b[:created_at], + "status" => transfer_status(b), + "size" => size_pretty(b[:processed_bytes]), + "database" => b[:from_name] || 'UNKNOWN' + } + end + if display_backups.empty? + display("No backups. Capture one with `heroku pg:backups capture`.") + else + display_table( + display_backups, + %w(id created_at status size database), + ["ID", "Backup Time", "Status", "Size", "Database"] + ) + end + + display "\n=== Restores" + display_restores = transfers.select do |r| + r[:from_type] != 'pg_dump' && r[:to_type] == 'pg_restore' + end.sort_by { |r| r[:created_at] }.reverse.first(10).map do |r| + { + "id" => transfer_name(r), + "created_at" => r[:created_at], + "status" => transfer_status(r), + "size" => size_pretty(r[:processed_bytes]), + "database" => r[:to_name] || 'UNKNOWN' + } + end + if display_restores.empty? + display("No restores found. Use `heroku pg:backups restore` to restore a backup") + else + display_table( + display_restores, + %w(id created_at status size database), + ["ID", "Restore Time", "Status", "Size", "Database"] + ) + end + + display "\n=== Copies" + display_restores = transfers.select do |r| + r[:from_type] == 'pg_dump' && r[:to_type] == 'pg_restore' + end.sort_by { |r| r[:created_at] }.reverse.first(10).map do |r| + { + "id" => transfer_name(r), + "created_at" => r[:created_at], + "status" => transfer_status(r), + "size" => size_pretty(r[:processed_bytes]), + "to_database" => r[:to_name] || 'UNKNOWN', + "from_database" => r[:from_name] || 'UNKNOWN' + } + end + if display_restores.empty? + display("No copies found. Use `heroku pg:copy` to copy a database to another") + else + display_table( + display_restores, + %w(id created_at status size from_database to_database), + ["ID", "Restore Time", "Status", "Size", "From Database", "To Database"] + ) + end + end + + def backup_status + backup_name = shift_argument + validate_arguments! + verbose = true + + client = hpg_app_client(app) + backup = if backup_name.nil? + backups = client.transfers + last_backup = backups.select do |b| + b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' + end.sort_by { |b| b[:created_at] }.last + if last_backup.nil? + error("No backups. Capture one with `heroku pg:backups capture`.") + else + if verbose + client.transfers_get(last_backup[:num], verbose) + else + last_backup + end + end + else + backup_num = transfer_num(backup_name) + if backup_num.nil? + error("No such backup: #{backup_num}") + end + client.transfers_get(backup_num, verbose) + end + status = if backup[:succeeded] + "Completed Successfully" + elsif backup[:canceled_at] + "Canceled" + elsif backup[:finished_at] + "Failed" + elsif backup[:started_at] + "Running" + else + "Pending" + end + type = if backup[:schedule] + "Scheduled" + else + "Manual" + end + + backup_name = transfer_name(backup) + display <<-EOF +=== Backup info: #{backup_name} +Database: #{backup[:from_name]} +EOF + if backup[:started_at] + display <<-EOF +Started: #{backup[:started_at]} +EOF + end + if backup[:finished_at] + display <<-EOF +Finished: #{backup[:finished_at]} +EOF + end + display <<-EOF +Status: #{status} +Type: #{type} +EOF + backup_size = backup[:processed_bytes] + orig_size = backup[:source_bytes] || 0 + if orig_size > 0 + compress_str = "" + unless backup[:finished_at].nil? + compression_pct = if backup_size > 0 + [((orig_size - backup_size).to_f / orig_size * 100) + .round, 0].max + else + 0 + end + compress_str = " (#{compression_pct}% compression)" + end + display <<-EOF +Original DB Size: #{size_pretty(orig_size)} +Backup Size: #{size_pretty(backup_size)}#{compress_str} +EOF + else + display <<-EOF +Backup Size: #{size_pretty(backup_size)} +EOF + end + if verbose + display "=== Backup Logs" + backup[:logs].each do |item| + display "#{item['created_at']}: #{item['message']}" + end + end + end + + def capture_backup + db = shift_argument + attachment = generate_resolver.resolve(db, "DATABASE_URL") + validate_arguments! + + backup = hpg_client(attachment).backups_capture + display <<-EOF +Use Ctrl-C at any time to stop monitoring progress; the backup +will continue running. Use heroku pg:backups info to check progress. +Stop a running backup with heroku pg:backups cancel. + +#{attachment.name} ---backup---> #{transfer_name(backup)} + +EOF + poll_transfer('backup', backup[:uuid]) + end + + def restore_backup + # heroku pg:backups restore [[backup_id] database] + db = nil + restore_from = :latest + + # N.B.: we have to account for the command argument here + if args.count == 2 + db = shift_argument + elsif args.count == 3 + restore_from = shift_argument + db = shift_argument + end + + attachment = generate_resolver.resolve(db, "DATABASE_URL") + validate_arguments! + + restore_url = nil + if restore_from =~ %r{\Ahttps?://} + restore_url = restore_from + else + # assume we're restoring from a backup + backup_name = restore_from + backups = hpg_app_client(app).transfers.select do |b| + b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' + end + backup = if backup_name == :latest + backups.select { |b| b[:succeeded] } + .sort_by { |b| b[:finished_at] }.last + else + backups.find { |b| transfer_name(b) == backup_name } + end + if backups.empty? + abort("No backups. Capture one with `heroku pg:backups capture`.") + elsif backup.nil? + abort("Backup #{backup_name} not found.") + elsif !backup[:succeeded] + abort("Backup #{backup_name} did not complete successfully; cannot restore it.") + end + restore_url = backup[:to_url] + end + + if confirm_command + restore = hpg_client(attachment).backups_restore(restore_url) + display <<-EOF +Use Ctrl-C at any time to stop monitoring progress; the backup +will continue restoring. Use heroku pg:backups to check progress. +Stop a running restore with heroku pg:backups cancel. + +#{transfer_name(restore)} ---restore---> #{attachment.name} + +EOF + poll_transfer('restore', restore[:uuid]) + end + end + + def poll_transfer(action, transfer_id) + # pending, running, complete--poll endpoint to get + backup = nil + ticks = 0 + failed_count = 0 + begin + begin + backup = hpg_app_client(app).transfers_get(transfer_id) + failed_count = 0 + status = if backup[:started_at] + "Running... #{size_pretty(backup[:processed_bytes])}" + else + "Pending... #{spinner(ticks)}" + end + redisplay status + ticks += 1 + rescue RestClient::Exception + backup = {} + failed_count += 1 + if failed_count > 120 + raise + end + end + sleep 3 + end until backup[:finished_at] + if backup[:succeeded] + redisplay "#{action.capitalize} completed\n" + else + # TODO: better errors for + # - db not online (/name or service not known/) + # - bad creds (/psql: FATAL:/???) + redisplay <<-EOF +An error occurred and your backup did not finish. + +Please run `heroku pg:backups info #{transfer_name(backup)}` for details. + +EOF + end + end + + def delete_backup + backup_name = shift_argument + validate_arguments! + + if confirm_command + backup_num = transfer_num(backup_name) + if backup_num.nil? + error("No such backup: #{backup_num}") + end + hpg_app_client(app).transfers_delete(backup_num) + display "Deleted #{backup_name}" + end + end + + def public_url + backup_name = shift_argument + validate_arguments! + + backup_num = nil + client = hpg_app_client(app) + if backup_name + backup_num = transfer_num(backup_name) + if backup_num.nil? + error("No such backup: #{backup_num}") + end + else + last_successful_backup = client.transfers.select do |xfer| + xfer[:succeeded] && xfer[:to_type] == 'gof3r' + end.sort_by { |b| b[:created_at] }.last + if last_successful_backup.nil? + error("No backups. Capture one with `heroku pg:backups capture`.") + else + backup_num = last_successful_backup[:num] + end + end + + url_info = client.transfers_public_url(backup_num) + if $stdout.tty? && !options[:quiet] + display <<-EOF +The following URL will expire at #{url_info[:expires_at]}: + "#{url_info[:url]}" +EOF + else + display url_info[:url] + end + end + + def cancel_backup + backup_name = shift_argument + validate_arguments! + + client = hpg_app_client(app) + + transfer = if backup_name + backup_num = transfer_num(backup_name) + if backup_num.nil? + error("No such backup/restore: #{backup_name}") + else + client.transfers_get(backup_num) + end + else + last_transfer = client.transfers.sort_by { |b| b[:created_at] }.reverse.find { |b| b[:finished_at].nil? } + if last_transfer.nil? + error("No active backups/restores") + else + last_transfer + end + end + + client.transfers_cancel(transfer[:uuid]) + display "Canceled #{transfer_name(transfer)}" + end + + def schedule_backups + db = shift_argument + validate_arguments! + at = options[:at] || '04:00 UTC' + schedule_opts = parse_schedule_time(at) + + resolver = generate_resolver + attachment = resolver.resolve(db, "DATABASE_URL") + + # N.B.: we need to resolve the name to find the right database, + # but we don't want to resolve it to the canonical name, so that, + # e.g., names like FOLLOWER_URL work. To do this, we look up the + # app config vars and re-find one that looks like the user's + # requested name. + db_name, alias_url = resolver.app_config_vars.find { |k,_| k =~ /#{db}/i } + if attachment.url != alias_url + error("Could not find database to schedule for backups. Try using its full name.") + end + + schedule_opts[:schedule_name] = db_name + + hpg_client(attachment).schedule(schedule_opts) + display "Scheduled automatic daily backups at #{at} for #{attachment.name}" + end + + def unschedule_backups + db = shift_argument + validate_arguments! + + if db.nil? + # try to provide a more informative error message, but rescue to + # a generic error message in case things go poorly + begin + attachment = arbitrary_app_db + schedules = hpg_client(attachment).schedules + schedule_names = schedules.map { |s| s[:name] }.join(", ") + abort("Must specify schedule to cancel: existing schedules are #{schedule_names}") + rescue StandardError + abort("Must specify schedule to cancel. Run `heroku help pg:backups` for usage information.") + end + end + + attachment = generate_resolver.resolve(db, "DATABASE_URL") + + schedule = hpg_client(attachment).schedules.find do |s| + # s[:name] is HEROKU_POSTGRESQL_COLOR_URL + s[:name] =~ /#{db}/i + end + + if schedule.nil? + display "No automatic daily backups for #{attachment.name} found" + else + hpg_client(attachment).unschedule(schedule[:uuid]) + display "Stopped automatic daily backups for #{attachment.name}" + end + end + + def list_schedules + validate_arguments! + attachment = arbitrary_app_db + if attachment.nil? + abort("#{app} has no heroku-postgresql databases.") + end + + schedules = hpg_client(attachment).schedules + if schedules.empty? + display "No backup schedules found. Use `heroku pg:backups schedule` to set one up." + else + display "=== Backup Schedules" + schedules.each do |s| + display "#{s[:name]}: daily at #{s[:hour]}:00 (#{s[:timezone]})" + end + end + end + + def hpg_app_client(app_name) + Heroku::Client::HerokuPostgresqlApp.new(app_name) + end + + def parse_schedule_time(time_str) + hour, tz = time_str.match(/([0-2][0-9]):00 (.*)/) && [ $1, $2 ] + if hour.nil? || tz.nil? + abort("Invalid schedule format: expected ':00 '") + end + # do-what-i-mean remapping, since transferatu is (rightfully) picky + remap_tzs = { + 'PST' => 'America/Los_Angeles', + 'PDT' => 'America/Los_Angeles', + 'MST' => 'America/Boise', + 'MDT' => 'America/Boise', + 'CST' => 'America/Chicago', + 'CDT' => 'America/Chicago', + 'EST' => 'America/New_York', + 'EDT' => 'America/New_York' + } + if remap_tzs.has_key? tz.upcase + tz = remap_tzs[tz.upcase] + end + { :hour => hour, :timezone => tz } + end +end diff --git a/lib/heroku/command/pgbackups.rb b/lib/heroku/command/pgbackups.rb index 9754d7ac5..4cd4154bc 100644 --- a/lib/heroku/command/pgbackups.rb +++ b/lib/heroku/command/pgbackups.rb @@ -219,9 +219,13 @@ def transfer validate_arguments! + if from.url == to.url + error("source and target database are the same") + end + opts = {} verify_app = to.app || app - if confirm_command(verify_app, "WARNING: Destructive Action\nTransfering data from #{from.name} to #{to.name}") + if confirm_command(verify_app, "WARNING: Destructive Action\nTransferring data from #{from.name} to #{to.name}") backup = transfer!(from.url, from.name, to.url, to.name, opts) backup = poll_transfer!(backup) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 086f68113..0823159c8 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -13,12 +13,13 @@ class Plugins < Base # # $ heroku plugins # === Installed Plugins - # heroku-accounts + # heroku-production-check@0.2.0 # def index validate_arguments! - plugins = ::Heroku::Plugin.list + plugins = ::Heroku::JSPlugin.plugins.map { |p| "#{p[:name]}@#{p[:version]}" } + plugins.concat(::Heroku::Plugin.list) if plugins.length > 0 styled_header("Installed Plugins") @@ -34,22 +35,18 @@ def index # #Example: # - # $ heroku plugins:install https://github.com/ddollar/heroku-accounts.git - # Installing heroku-accounts... done + # $ heroku plugins:install heroku-production-check + # Installing heroku-production-check... done # def install - plugin = Heroku::Plugin.new(shift_argument) + name = shift_argument validate_arguments! - - action("Installing #{plugin.name}") do - if plugin.install - unless Heroku::Plugin.load_plugin(plugin.name) - plugin.uninstall - exit(1) - end - else - error("Could not install #{plugin.name}. Please check the URL and try again.") - end + if name =~ /\./ + # if it contains a '.' then we are assuming it is a URL + # and we should install it as a ruby plugin + ruby_plugin_install(name) + else + js_plugin_install(name) end end @@ -59,15 +56,18 @@ def install # #Example: # - # $ heroku plugins:uninstall heroku-accounts - # Uninstalling heroku-accounts... done + # $ heroku plugins:uninstall heroku-production-check + # Uninstalling heroku-production-check... done # def uninstall plugin = Heroku::Plugin.new(shift_argument) validate_arguments! - - action("Uninstalling #{plugin.name}") do - plugin.uninstall + if Heroku::Plugin.list.include? plugin.name + action("Uninstalling #{plugin.name}") do + plugin.uninstall + end + elsif Heroku::JSPlugin.setup? + Heroku::JSPlugin.uninstall(plugin.name) end end @@ -78,10 +78,10 @@ def uninstall #Example: # # $ heroku plugins:update - # Updating heroku-accounts... done + # Updating heroku-production-check... done # - # $ heroku plugins:update heroku-accounts - # Updating heroku-accounts... done + # $ heroku plugins:update heroku-production-check + # Updating heroku-production-check... done # def update plugins = if plugin = shift_argument @@ -106,5 +106,37 @@ def update end end + # plugins:link [PATH] + # Links a local plugin into CLI. + # This is useful when developing plugins locally. + # It simply symlinks the specified path into ~/.heroku/node_modules + + #Example: + # $ heroku plugins:link . + # + def link + Heroku::JSPlugin.setup + Heroku::JSPlugin.run('plugins', 'link', ARGV[1..-1]) + end + + private + + def js_plugin_install(name) + Heroku::JSPlugin.install(name, force: true) + end + + def ruby_plugin_install(name) + action("Installing #{name}") do + plugin = Heroku::Plugin.new(name) + if plugin.install + unless Heroku::Plugin.load_plugin(plugin.name) + plugin.uninstall + exit(1) + end + else + error("Could not install #{plugin.name}. Please check the URL and try again.") + end + end + end end end diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 4460a1df0..7130d4e2e 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -3,10 +3,20 @@ # manage dynos (dynos, workers) # class Heroku::Command::Ps < Heroku::Command::Base - PRICES = { - "P" => 0.8, - "PX" => 0.8, - } + PROCESS_TIERS =[ + {"tier"=>"free", "max_scale"=>1, "max_processes"=>2, "cost"=>{"Free"=>0}}, + {"tier"=>"hobby", "max_scale"=>1, "max_processes"=>nil, "cost"=>{"Hobby"=>700}}, + {"tier"=>"production", "max_scale"=>100, "max_processes"=>nil, "cost"=>{"Standard-1X"=>2500, "Standard-2X"=>5000, "Performance"=>50000}}, + {"tier"=>"traditional", "max_scale"=>100, "max_processes"=>nil, "cost"=>{"1X"=>3600, "2X"=>7200, "PX"=>57600}} + ] + + costs = PROCESS_TIERS.collect do |tier| + tier["cost"].collect do |name, cents_per_month| + [name, (cents_per_month / 100)] + end + end + COSTS = Hash[*costs.flatten] + # ps:dynos [QTY] # @@ -97,7 +107,32 @@ def workers # def index validate_arguments! - resp = api.request( + quota_resp = api.request( + :expects => [200, 404], + :method => :post, + :path => "/apps/#{app}/actions/get-quota", + :headers => { + "Accept" => "application/vnd.heroku+json; version=3.app-quotas", + "Content-Type" => "application/json" + } + ) + + if quota_resp.status = 200 + quota = quota_resp.body + now = Time.now.getutc + quota_message = if quota["allow_until"] + "Free quota left:" + elsif quota["deny_until"] + "Free quota exhausted. Unidle available in:" + end + if quota_message + quota_timestamp = (quota["allow_until"] ? Time.parse(quota["allow_until"]).getutc : Time.parse(quota["deny_until"]).getutc) + time_left = time_remaining(Time.now.getutc, quota_timestamp) + display("#{quota_message} #{time_left}") + end + end + + processes_resp = api.request( :expects => 200, :method => :get, :path => "/apps/#{app}/dynos", @@ -106,7 +141,7 @@ def index "Content-Type" => "application/json" } ) - processes = resp.body + processes = processes_resp.body processes_by_command = Hash.new {|hash,key| hash[key] = []} processes.each do |process| @@ -194,7 +229,7 @@ def scale change_map = {} changes = args.map do |arg| - if change = arg.scan(/^([a-zA-Z0-9_]+)([=+-]\d+)(?::(\w+))?$/).first + if change = arg.scan(/^([a-zA-Z0-9_]+)([=+-]\d+)(?::([\w-]+))?$/).first formation, quantity, size = change quantity.gsub!("=", "") # only allow + and - on quantity change_map[formation] = [quantity, size] @@ -203,7 +238,7 @@ def scale end.compact if changes.empty? - error("Usage: heroku ps:scale DYNO1=AMOUNT1[:SIZE] [DYNO2=AMOUNT2 ...]\nMust specify DYNO and AMOUNT to scale.") + error("Usage: heroku ps:scale DYNO1=AMOUNT1[:SIZE] [DYNO2=AMOUNT2 ...]\nMust specify DYNO and AMOUNT to scale.\nDYNO must be alphanumeric.") end action("Scaling dynos") do @@ -262,23 +297,132 @@ def stop alias_command "stop", "ps:stop" - # ps:resize DYNO1=1X|2X|PX [DYNO2=1X|2X|PX ...] + # ps:type [TYPE | DYNO=TYPE [DYNO=TYPE ...]] # - # resize dynos to the given size + # manage dyno types # - # Example: + # called with no arguments shows the current dyno type # - # $ heroku ps:resize web=PX worker=2X - # Resizing and restarting the specified dynos... done - # web dynos now PX ($0.80/dyno-hour) - # worker dynos now 2X ($0.10/dyno-hour) + # called with one argument sets the type + # where type is one of traditional|free|hobby|standard-1x|standard-2x|performance # - def resize + # called with 1..n DYNO=TYPE arguments sets the type per dyno + # this is only available when the app is on production and performance + # + def type + if args.any?{|arg| arg =~ /=/} + _original_resize + return + end + + app + process_tier = shift_argument + process_tier.downcase! if process_tier + validate_arguments! + + if %w[standard-1x standard-2x performance].include?(process_tier) + special_case_change_tier_and_resize(process_tier) + return + end + + # get or update app.process_tier + app_resp = process_tier.nil? ? edge_app_info : change_dyno_type(process_tier) + + # get, calculate and display app process type costs + formation_resp = edge_app_formation + + display_dyno_type_and_costs(app_resp, formation_resp) + end + + alias_method :resize, :type + alias_command "resize", "ps:type" + + private + + def change_dyno_type(process_tier) + print "Changing dyno type... " + + app_resp = patch_tier(process_tier) + + if app_resp.status != 200 + puts "failed" + error app_resp.body["message"] + " Please use `heroku ps:scale` to change process size and scale." + end + + puts "done." + + return app_resp + end + + def patch_tier(process_tier) + api.request( + :method => :patch, + :path => "/apps/#{app}", + :body => json_encode("process_tier" => process_tier), + :headers => { + "Accept" => "application/vnd.heroku+json; version=edge", + "Content-Type" => "application/json" + } + ) + end + + def display_dyno_type_and_costs(app_resp, formation_resp) + tier_info = PROCESS_TIERS.detect { |t| t["tier"] == app_resp.body["process_tier"] } + + formation = formation_resp.body.reject {|ps| ps['quantity'] < 1} + + annotated = formation.sort_by{|d| d['type']}.map do |dyno| + cost = tier_info["cost"][dyno["size"]] * dyno["quantity"] / 100 + { + 'dyno' => dyno['type'], + 'type' => dyno['size'].rjust(4), + 'qty' => dyno['quantity'].to_s.rjust(3), + 'cost/mo' => cost.to_s.rjust(7) + } + end + + # in case of an app not yet released + annotated = [tier_info] if annotated.empty? + + display_table(annotated, annotated.first.keys, annotated.first.keys) + end + + def edge_app_info + api.request( + :expects => 200, + :method => :get, + :path => "/apps/#{app}", + :headers => { + "Accept" => "application/vnd.heroku+json; version=edge", + "Content-Type" => "application/json" + } + ) + end + + def edge_app_formation + api.request( + :expects => 200, + :method => :get, + :path => "/apps/#{app}/formation", + :headers => { + "Accept" => "application/vnd.heroku+json; version=3", + "Content-Type" => "application/json" + } + ) + end + + def special_case_change_tier_and_resize(type) + patch_tier("production") + override_args = edge_app_formation.body.map { |ps| "#{ps['type']}=#{type}" } + _original_resize(override_args) + end + + def _original_resize(override_args=nil) app change_map = {} - changes = args.map do |arg| - if arg =~ /^([a-zA-Z0-9_]+)=(\w+)$/ + changes = (override_args || args).map do |arg| + if arg =~ /^([a-zA-Z0-9_]+)=([\w-]+)$/ change_map[$1] = $2 { "process" => $1, "size" => $2 } end @@ -286,8 +430,8 @@ def resize if changes.empty? message = [ - "Usage: heroku ps:resize DYNO1=1X|2X|PX [DYNO2=1X|2X|PX ...]", - "Must specify DYNO and SIZE to resize." + "Usage: heroku dyno:type DYNO1=1X|2X|PX [DYNO2=1X|2X|PX ...]", + "Must specify DYNO and TYPE to resize." ] error(message.join("\n")) end @@ -308,14 +452,12 @@ def resize resp.body.select {|p| change_map.key?(p['type']) }.each do |p| size = p["size"] - price = if size.to_i > 0 - sprintf("%.2f", 0.05 * size.to_i) - else - sprintf("%.2f", PRICES[size]) - end - display "#{p["type"]} dynos now #{size} ($#{price}/dyno-hour)" + display "#{p["type"]} dynos now #{size} ($#{COSTS[size]}/month)" end end +end - alias_command "resize", "ps:resize" +%w[type restart scale stop].each do |cmd| + Heroku::Command::Base.alias_command "dyno:#{cmd}", "ps:#{cmd}" end + diff --git a/lib/heroku/command/redis.rb b/lib/heroku/command/redis.rb new file mode 100644 index 000000000..390ba74e5 --- /dev/null +++ b/lib/heroku/command/redis.rb @@ -0,0 +1,19 @@ +require "heroku/command/base" + +module Heroku::Command + + # list redis databases for an app + # + class Redis < Base + + # redis [DATABASE] + # + # Get information about redis database + # + # + def index + Heroku::JSPlugin.install('heroku-redis') + Heroku::JSPlugin.run('redis:info', nil, ARGV[1..-1]) + end + end +end diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index ae4b2f43a..fffa19d40 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -1,4 +1,20 @@ -require "readline" +begin + require "readline" +rescue LoadError + module Readline + def self.readline(prompt) + print prompt + $stdout.flush + gets + end + + module HISTORY + def self.push(cmd) + # dummy + end + end + end +end require "heroku/command/base" require "heroku/helpers/log_displayer" @@ -11,6 +27,7 @@ class Heroku::Command::Run < Heroku::Command::Base # run an attached dyno # # -s, --size SIZE # specify dyno size + # --exit-code # return exit code from process # #Example: # @@ -18,9 +35,18 @@ class Heroku::Command::Run < Heroku::Command::Base # Running `bash` attached to terminal... up, run.1 # ~ $ # + # $ heroku run -s hobby -- myscript.sh -a arg1 -s arg2 + # Running `myscript.sh -a arg1 -s arg2` attached to terminal... up, run.1 + # def index + if ARGV.include?('--') || ARGV.include?('--exit-code') + Heroku::JSPlugin.install('heroku-run') + Heroku::JSPlugin.run('run', nil, ARGV[1..-1]) + return + end command = args.join(" ") error("Usage: heroku run COMMAND") if command.empty? + warn_if_using_jruby run_attached(command) end @@ -56,7 +82,7 @@ def detached log_displayer = ::Heroku::Helpers::LogDisplayer.new(heroku, app, opts) log_displayer.display_logs else - display("Use `heroku logs -p #{process_data['process']}` to view the output.") + display("Use `heroku logs -p #{process_data['process']} -a #{app_name}` to view the output.") end end @@ -81,7 +107,7 @@ def rake alias_command "rake", "run:rake" - # run:console [COMMAND] + # HIDDEN: run:console [COMMAND] # # open a remote console session # @@ -131,11 +157,11 @@ def rendezvous_session(rendezvous_url, &on_connect) rendezvous.on_connect(&on_connect) rendezvous.start rescue Timeout::Error, Errno::ETIMEDOUT - error "\nTimeout awaiting process" + error "\nTimeout awaiting dyno, see https://devcenter.heroku.com/articles/one-off-dynos#timeout-awaiting-process" rescue OpenSSL::SSL::SSLError - error "Authentication error" + error "\nSSL error connecting to dyno." rescue Errno::ECONNREFUSED, Errno::ECONNRESET - error "\nError connecting to process" + error "\nError connecting to dyno, see https://devcenter.heroku.com/articles/one-off-dynos#timeout-awaiting-process" rescue Interrupt ensure set_buffer(true) @@ -174,7 +200,7 @@ def console_history_read(app) end history.each { |cmd| Readline::HISTORY.push(cmd) } rescue Errno::ENOENT - rescue Exception => ex + rescue => ex display "Error reading your console history: #{ex.message}" if confirm("Would you like to clear it? (y/N):") FileUtils.rm(console_history_file(app)) rescue nil @@ -185,5 +211,4 @@ def console_history_add(app, cmd) Readline::HISTORY.push(cmd) File.open(console_history_file(app), "a") { |f| f.puts cmd + "\n" } end - end diff --git a/lib/heroku/command/ssl.rb b/lib/heroku/command/ssl.rb deleted file mode 100644 index 1441aaa91..000000000 --- a/lib/heroku/command/ssl.rb +++ /dev/null @@ -1,43 +0,0 @@ -require "heroku/command/base" - -module Heroku::Command - - # DEPRECATED: see `heroku certs` instead - # - # manage ssl certificates for an app - # - class Ssl < Base - - # ssl - # - # list legacy certificates for an app - # - def index - api.get_domains(app).body.each do |domain| - if cert = domain['cert'] - display "#{domain['domain']} has a SSL certificate registered to #{cert['subject']} which expires on #{format_date(cert['expires_at'])}" - else - display "#{domain['domain']} has no certificate" - end - end - end - - # ssl:add PEM KEY - # - # DEPRECATED: see `heroku certs:add` instead - # - def add - $stderr.puts " ! `heroku ssl:add` has been deprecated. Please use the SSL Endpoint add-on and the `heroku certs` commands instead." - $stderr.puts " ! SSL Endpoint documentation is available at: https://devcenter.heroku.com/articles/ssl-endpoint" - end - - # ssl:clear - # - # remove legacy ssl certificates from an app - # - def clear - heroku.clear_ssl(app) - display "Cleared certificates for #{app}" - end - end -end diff --git a/lib/heroku/command/stack.rb b/lib/heroku/command/stack.rb index 3415eb5cc..7036590d0 100644 --- a/lib/heroku/command/stack.rb +++ b/lib/heroku/command/stack.rb @@ -13,9 +13,8 @@ class Stack < Base # # $ heroku stack # === example Available Stacks - # bamboo-mri-1.9.2 - # bamboo-ree-1.8.7 - # * cedar + # cedar-10 + # * cedar-14 # def index validate_arguments! @@ -24,7 +23,7 @@ def index styled_header("#{app} Available Stacks") stacks = stacks_data.map do |stack| - row = [stack['current'] ? '*' : ' ', stack['name']] + row = [stack['current'] ? '*' : ' ', Codex.out(stack['name'])] row << '(beta)' if stack['beta'] row << '(deprecated)' if stack['deprecated'] row << '(prepared, will migrate on next git push)' if stack['requested'] @@ -38,15 +37,36 @@ def index # set new app stack # def set - unless stack = shift_argument + unless stack = Codex.in(shift_argument) error("Usage: heroku stack:set STACK.\nMust specify target stack.") end api.put_stack(app, stack) - display "Stack set. Next release on #{app} will use #{stack}." - display "Run `git push heroku master` to create a new release on #{stack}." + display "Stack set. Next release on #{app} will use #{Codex.out(stack)}." + display "Run `git push heroku master` to create a new release on #{Codex.out(stack)}." end alias_command "stack:migrate", "stack:set" + + module Codex + def self.in(stack) + IN[stack] || stack + end + + def self.out(stack) + OUT[stack] || stack + end + + # Legacy translations for cedar => cedar-10 + # only here for UX purposes to avoid confusion + # when we say `Sunsetting cedar`. + IN = { + "cedar-10" => "cedar" + } + + OUT = { + "cedar" => "cedar-10" + } + end end end diff --git a/lib/heroku/command/status.rb b/lib/heroku/command/status.rb index 0b8f93538..993d6fa5f 100644 --- a/lib/heroku/command/status.rb +++ b/lib/heroku/command/status.rb @@ -16,36 +16,7 @@ class Heroku::Command::Status < Heroku::Command::Base # Production: No known issues at this time. # def index - validate_arguments! - - heroku_status_host = ENV['HEROKU_STATUS_HOST'] || "status.heroku.com" - require('excon') - status = json_decode(Excon.get("https://#{heroku_status_host}/api/v3/current-status.json", :nonblock => false).body) - - styled_header("Heroku Status") - - status['status'].each do |key, value| - if value == 'green' - status['status'][key] = 'No known issues at this time.' - end - end - styled_hash(status['status']) - - unless status['issues'].empty? - display - status['issues'].each do |issue| - duration = time_ago(issue['created_at']).gsub(' ago', '+') - styled_header("#{issue['title']} #{duration}") - changes = issue['updates'].map do |issue| - [ - time_ago(issue['created_at']), - issue['update_type'], - issue['contents'] - ] - end - styled_array(changes, :sort => false) - end - end + Heroku::JSPlugin.install('heroku-status') + Heroku::JSPlugin.run('status', nil, ARGV[1..-1]) end - end diff --git a/lib/heroku/command/two_factor.rb b/lib/heroku/command/two_factor.rb index c2c1a6efd..c9c94f56d 100644 --- a/lib/heroku/command/two_factor.rb +++ b/lib/heroku/command/two_factor.rb @@ -1,10 +1,12 @@ require "heroku/command/base" module Heroku::Command + # manage two-factor authentication settings + # class TwoFactor < BaseWithApp - # 2fa + # twofactor # - # Display whether two-factor is enabled or not + # Display whether two-factor authentication is enabled or not # def index account = api.request( @@ -14,23 +16,23 @@ def index :path => "/account").body if account["two_factor_authentication"] - display "Two-factor auth is enabled." + display "Two-factor authentication is enabled." else - display "Two-factor is not enabled." + display "Two-factor authentication is not enabled." end end alias_command "2fa", "twofactor" - # 2fa:disable + # twofactor:disable # - # Disable 2fa on your account + # Disable two-factor authentication for your account # def disable print "Password (typing will be hidden): " password = Heroku::Auth.ask_for_password - update = MultiJson.encode( + update = MultiJson.dump( :two_factor_authentication => false, :password => password) @@ -48,9 +50,9 @@ def disable alias_command "2fa:disable", "twofactor:disable" - # 2fa:generate-recovery-codes + # twofactor:generate-recovery-codes # - # Generates (and replaces) recovery codes + # Generates and replaces recovery codes # def generate_recovery_codes code = Heroku::Auth.ask_for_second_factor diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index 67b977a15..51bb3e1bb 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -12,11 +12,14 @@ class Heroku::Command::Update < Heroku::Command::Base # Example: # # $ heroku update - # Updating from v1.2.3... done, updated to v2.3.4 + # Updating... done, v1.2.3 updated to v2.3.4 # def index validate_arguments! - update_from_url("https://toolbelt.heroku.com/download/zip") + update_from_url(false) + if Heroku::JSPlugin.setup? + Heroku::JSPlugin.update + end end # update:beta @@ -24,24 +27,17 @@ def index # update to the latest beta client # # $ heroku update - # Updating from v1.2.3... done, updated to v2.3.4.pre + # Updating... done, v1.2.3 updated to v2.3.4.pre # def beta validate_arguments! - update_from_url("https://toolbelt.heroku.com/download/beta-zip") + update_from_url(true) end -private + private - def update_from_url(url) + def update_from_url(prerelease) Heroku::Updater.check_disabled! - action("Updating from #{Heroku::VERSION}") do - if new_version = Heroku::Updater.update(url) - status("updated to #{new_version}") - else - status("nothing to update") - end - end + Heroku::Updater.update(prerelease) end - end diff --git a/lib/heroku/command/version.rb b/lib/heroku/command/version.rb index e87e320c8..84a5800c6 100644 --- a/lib/heroku/command/version.rb +++ b/lib/heroku/command/version.rb @@ -18,6 +18,8 @@ def index validate_arguments! display(Heroku.user_agent) - end + display(Heroku::JSPlugin.version) if Heroku::JSPlugin.setup? + Heroku::Command::Plugins.new.index + end end diff --git a/lib/heroku/distribution.rb b/lib/heroku/distribution.rb deleted file mode 100644 index 109ab188e..000000000 --- a/lib/heroku/distribution.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Heroku - module Distribution - def self.files - Dir[File.expand_path("../../../{bin,data,lib}/**/*", __FILE__)].select do |file| - File.file?(file) - end - end - end -end diff --git a/lib/heroku/git.rb b/lib/heroku/git.rb new file mode 100644 index 000000000..aee49e6ab --- /dev/null +++ b/lib/heroku/git.rb @@ -0,0 +1,69 @@ +require "heroku/helpers" + +module Heroku::Git + extend Heroku::Helpers + + def self.check_git_version + return unless running_on_windows? || running_on_a_mac? + if git_is_insecure(git_version) + warn_about_insecure_git + end + end + + def self.git_is_insecure(version) + v = Version.parse(version) + if v < Version.parse('1.8.5.6') + return true + end + if v >= Version.parse('1.9') && v < Version.parse('1.9.5') + return true + end + if v >= Version.parse('2.0') && v < Version.parse('2.0.5') + return true + end + if v >= Version.parse('2.1') && v < Version.parse('2.1.4') + return true + end + if v >= Version.parse('2.2') && v < Version.parse('2.2.1') + return true + end + return false + end + + def self.warn_about_insecure_git + warn "Your version of git is #{git_version}. Which has serious security vulnerabilities." + warn "More information here: https://blog.heroku.com/archives/2014/12/23/update_your_git_clients_on_windows_and_os_x" + end + + private + + def self.git_version + version = /git version ([\d\.]+)/.match(`git --version`) + error("Git appears to be installed incorrectly\nEnsure that `git --version` outputs the version correctly.") unless version + version[1] + rescue Errno::ENOENT + error("Git must be installed to use the Heroku Toolbelt.\nSee instructions here: http://git-scm.com") + end + + class Version + include Comparable + + attr_accessor :major, :minor, :patch, :special + + def initialize(major, minor=0, patch=0, special=0) + @major, @minor, @patch, @special = major, minor, patch, special + end + + def self.parse(s) + digits = s.split('.').map { |i| i.to_i } + Version.new(*digits) + end + + def <=>(other) + return major <=> other.major unless (major <=> other.major) == 0 + return minor <=> other.minor unless (minor <=> other.minor) == 0 + return patch <=> other.patch unless (patch <=> other.patch) == 0 + return special <=> other.special + end + end +end diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 2e618d8f9..fbf222412 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -1,4 +1,4 @@ -require "vendor/heroku/okjson" +# encoding: utf-8 module Heroku module Helpers @@ -6,7 +6,12 @@ module Helpers extend self def home_directory - running_on_windows? ? ENV['USERPROFILE'].gsub("\\","/") : ENV['HOME'] + if running_on_windows? && RUBY_VERSION == '1.9.3' + # https://bugs.ruby-lang.org/issues/10126 + Dir.home.force_encoding('cp775') + else + Dir.home + end end def running_on_windows? @@ -34,6 +39,22 @@ def deprecate(message) display "WARNING: #{message}" end + def debug(*args) + $stderr.puts(*args) if debugging? + end + + def stderr_puts(*args) + $stderr.puts(*args) + end + + def stderr_print(*args) + $stderr.print(*args) + end + + def debugging? + ENV['HEROKU_DEBUG'] + end + def confirm(message="Are you sure you wish to continue? (y/n)") display("#{message} ", false) ['y', 'yes'].include?(ask.downcase) @@ -118,7 +139,17 @@ def time_ago(since) message end + def time_remaining(from, to) + secs = (to - from).to_i + mins = secs / 60 + hours = mins / 60 + return "#{hours}h #{mins % 60}m" if hours > 0 + return "#{mins}m #{secs % 60}s" if mins > 0 + return "#{secs}s" if secs >= 0 + end + def truncate(text, length) + return "" if text.nil? if text.size > length text[0, length - 2] + '..' else @@ -142,11 +173,14 @@ def quantify(string, num) "%d %s" % [ num, num.to_i == 1 ? string : "#{string}s" ] end + def has_git_remote?(remote) + git('remote').split("\n").include?(remote) && $?.success? + end + def create_git_remote(remote, url) - return if git('remote').split("\n").include?(remote) - return unless File.exists?(".git") + return if has_git_remote? remote git "remote add #{remote} #{url}" - display "Git remote #{remote} added" + display "Git remote #{remote} added" if $?.success? end def longest(items) @@ -178,14 +212,12 @@ def display_row(row, lengths) end def json_encode(object) - Heroku::OkJson.encode(object) - rescue Heroku::OkJson::Error - nil + JSON.generate(object) end def json_decode(json) - Heroku::OkJson.decode(json) - rescue Heroku::OkJson::Error + JSON.parse(json) + rescue JSON::ParserError nil end @@ -249,12 +281,14 @@ def output_with_bang(message="", new_line=true) display(format_with_bang(message), new_line) end - def error(message) + def error(message, report=false) if Heroku::Helpers.error_with_failure display("failed") Heroku::Helpers.error_with_failure = false end $stderr.puts(format_with_bang(message)) + rollbar_id = Rollbar.error(message) if report + $stderr.puts("Error ID: #{rollbar_id}") if rollbar_id exit(1) end @@ -362,20 +396,13 @@ def styled_array(array, options={}) display end - def format_error(error, message='Heroku client internal error.') + def format_error(error, message='Heroku client internal error.', rollbar_id=nil) formatted_error = [] formatted_error << " ! #{message}" formatted_error << ' ! Search for help at: https://help.heroku.com' formatted_error << ' ! Or report a bug at: https://github.com/heroku/heroku/issues/new' formatted_error << '' formatted_error << " Error: #{error.message} (#{error.class})" - formatted_error << " Backtrace: #{error.backtrace.first}" - error.backtrace[1..-1].each do |line| - formatted_error << " #{line}" - end - if error.backtrace.length > 1 - formatted_error << '' - end command = ARGV.map do |arg| if arg.include?(' ') arg = %{"#{arg}"} @@ -406,6 +433,9 @@ def format_error(error, message='Heroku client internal error.') end end formatted_error << " Version: #{Heroku.user_agent}" + formatted_error << " Error ID: #{rollbar_id}" if rollbar_id + formatted_error << "\n" + formatted_error << " More information in #{error_log_path}" formatted_error << "\n" formatted_error.join("\n") end @@ -415,7 +445,22 @@ def styled_error(error, message='Heroku client internal error.') display("failed") Heroku::Helpers.error_with_failure = false end - $stderr.puts(format_error(error, message)) + rollbar_id = Rollbar.error(error) + $stderr.puts(format_error(error, message, rollbar_id)) + error_log(message, error.message, error.backtrace.join("\n")) + rescue => e + $stderr.puts e, e.backtrace, error, error.backtrace + end + + def error_log(*obj) + FileUtils.mkdir_p(File.dirname(error_log_path)) + File.open(error_log_path, 'a') do |file| + file.write(obj.join("\n") + "\n") + end + end + + def error_log_path + File.join(home_directory, '.heroku', 'error.log') end def styled_header(header) @@ -520,5 +565,12 @@ def app_owner email org?(email) ? email.gsub(/^(.*)@#{org_host}$/,'\1') : email end + def has_http_git_entry_in_netrc + Auth.netrc && Auth.netrc[Auth.http_git_host] + end + + def warn_if_using_jruby + stderr_puts "WARNING: jruby is known to cause issues when used with the toolbelt." if RUBY_PLATFORM == "java" + end end end diff --git a/lib/heroku/helpers/addons/api.rb b/lib/heroku/helpers/addons/api.rb new file mode 100644 index 000000000..4daa35805 --- /dev/null +++ b/lib/heroku/helpers/addons/api.rb @@ -0,0 +1,98 @@ +module Heroku::Helpers + module Addons + module API + VERSION="3".freeze + + def request(options = {}) + defaults = { + :expects => 200, + :headers => {}, + :method => :get + } + options = defaults.merge(options) + options[:headers]["Accept"] ||= "application/vnd.heroku+json; version=#{VERSION}" + api.request(options).body + end + + def request_list(options = {}) + options = options.dup + options[:expects] = [200, 206, *options[:expects]].uniq + + request(options) + end + + def get_attachments(options = {}) + request_list(:path => attachments_path(options)) + end + + def get_attachment!(identifier, options = {}) + request(:path => "#{attachments_path(options)}/#{identifier}") + end + + def get_attachment(identifier, options = {}) + get_attachment!(identifier, options) + rescue Heroku::API::Errors::NotFound + end + + def get_addons(options = {}) + request_list( + :headers => { 'Accept-Expansion' => 'plan' }, + :path => addons_path(options) + ) + end + + def get_addon!(identifier, options = {}) + request( + :headers => { 'Accept-Expansion' => 'plan' }, + :path => "#{addons_path(options)}/#{identifier}" + ) + end + + def get_addon(identifier, options = {}) + get_addon!(identifier, options) + rescue Heroku::API::Errors::NotFound + end + + def get_service!(service) + request(:path => "/addon-services/#{service}") + end + + def get_service(service) + get_service! + rescue Heroku::API::Errors::NotFound + end + + def get_services + request_list(:path => "/addon-services") + end + + def get_plans(options = {}) + path = options[:service] ? + "/addon-services/#{options[:service]}/plans" : + "/plans" + + request_list(:path => path) + end + + private + + def addons_path(options) + if app = options[:app] + "/apps/#{app}/addons" + else + "/addons" + end + end + + def attachments_path(options) + if resource = options[:resource] + "/addons/#{resource}/addon-attachments" + elsif app = options[:app] + "/apps/#{app}/addon-attachments" + else + "/addon-attachments" + end + end + end + end +end diff --git a/lib/heroku/helpers/addons/display.rb b/lib/heroku/helpers/addons/display.rb new file mode 100644 index 000000000..c67e7fb23 --- /dev/null +++ b/lib/heroku/helpers/addons/display.rb @@ -0,0 +1,134 @@ +require "heroku/helpers/addons/api" + +module Heroku::Helpers + module Addons + module Display + include Heroku::Helpers::Addons::API + + # Shows details about and attachments for a specified resource. For example: + # + # $ heroku addons --resource practicing-nobly-1495 + # === Resource Info + # Name: practicing-nobly-1495 + # Plan: heroku-postgresql:premium-yanari + # Billing App: addons-reports + # Price: $200.00/month + # + # === Attachments + # App Name + # -------------- ------------------------ + # addons ADDONS_REPORTS + # addons-reports DATABASE + # addons-reports HEROKU_POSTGRESQL_SILVER + def show_for_resource(identifier) + styled_header("Resource Info") + + resource = resolve_addon!(identifier) + + styled_hash({ + 'Name' => resource['name'], + 'Plan' => resource['plan']['name'], + 'Billing App' => resource['app']['name'], + 'Price' => format_price(resource['plan']['price']) + }, ['Name', 'Plan', 'Billing App', 'Price']) + + display("") # separate sections + + styled_header("Attachments") + display_attachments(get_attachments(:resource => resource['id']), ['App', 'Name']) + end + + # Shows all add-ons owned by and attachments attached to the provided app. For example: + # + # === Add-on Resources for bjeanes + # Plan Name Price + # ----------------------- ---------------------- ----- + # heroku-postgresql:dev budding-busily-2230 free + # memcachier-staging:test sighing-ably-6278 free + # memcachier-staging:test rolling-carefully-8506 free + # newrelic:wayne unwinding-kindly-4330 free + # pgbackups:plus pgbackups-8071074 free + # + # === Add-on Attachments for bjeanes + # Name Add-on Billing App + # ------------------------ ---------------------- ----------- + # DATABASE budding-busily-2230 bjeanes + # HEROKU_POSTGRESQL_VIOLET budding-busily-2230 bjeanes + # MEMCACHE sighing-ably-6278 bjeanes + # MEMCACHIER_STAGING rolling-carefully-8506 bjeanes + # NEWRELIC unwinding-kindly-4330 bjeanes + # PGBACKUPS pgbackups-8071074 bjeanes + def show_for_app(app) + styled_header("Resources for #{app}") + + addons = get_addons(:app => app). + # the /apps/:id/addons endpoint can return more than just those owned + # by the app, so filter: + select { |addon| addon['app']['name'] == app } + + display_addons(addons, %w[Plan Name Price]) + + display('') # separate sections + + styled_header("Attachments for #{app}") + display_attachments(get_attachments(:app => app), ['Name', 'Add-on', 'Billing App']) + end + + # Shows a table of all add-ons on the account. For example: + # + # === Add-on Resources + # Plan Name Billing App Price + # ----------------------- --------------------------- -------------- ------------ + # bugsnag:sagittaron bugsnag-9174150 addons $9.00/month + # deployhooks:hipchat deployhooks-hipchat-9852225 addons-staging free + # heroku-postgresql:crane advising-fairly-3183 ion-bo $50.00/month + # newrelic:wayne unwinding-kindly-4330 bjeanes free + def show_all + styled_header('Resources') + display_addons(get_addons, ['Plan', 'Name', 'Billed to', 'Price']) + end + + def display_attachments(attachments, fields) + if attachments.empty? + display('There are no attachments.') + else + table = attachments.map do |attachment| + { + 'Name' => attachment['name'], + 'Add-on' => attachment['addon']['name'], + 'Billing App' => attachment['addon']['app']['name'], + 'App' => attachment['app']['name'] + } + end.sort_by { |addon| fields.map { |f| addon[f] } } + + display_table(table, fields, fields) + end + end + + def display_addons(addons, fields) + if addons.empty? + display('There are no add-ons.') + else + table = addons.map do |addon| + { + 'Plan' => addon['plan']['name'], + 'Name' => addon['name'], + 'Billed to' => addon['app']['name'], + 'Price' => format_price(addon['plan']['price']) + } + end.sort_by { |addon| fields.map { |f| addon[f] } } + + display_table(table, fields, fields) + end + end + + def format_price(price) + if price['cents'] == 0 + 'free' + else + '$%.2f/%s' % [(price['cents'] / 100.0), price['unit']] + end + end + end + end +end diff --git a/lib/heroku/helpers/addons/resolve.rb b/lib/heroku/helpers/addons/resolve.rb new file mode 100644 index 000000000..673f6977a --- /dev/null +++ b/lib/heroku/helpers/addons/resolve.rb @@ -0,0 +1,138 @@ +require "heroku/helpers/addons/api" + +module Heroku::Helpers + module Addons + module Resolve + include Heroku::Helpers::Addons::API + + UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + ATTACHMENT = /^(?:([a-z][a-z0-9-]+)::)?([A-Z][A-Z0-9_]+)$/ + RESOURCE = /^@?([a-z][a-z0-9-]+)$/ + SERVICE_PLAN = /^(?:([a-z0-9_-]+):)?([a-z0-9_-]+)$/ # service / service:plan + + class AddonDoesNotExistError < Heroku::API::Errors::Error + end + + # Finds attachments that match provided identifier. + # + # Always returns an Array of 0 or more results. + def resolve_attachment(identifier, &filter) + results = case identifier + when UUID + [get_attachment(identifier)].compact + when ATTACHMENT + app = $1 || self.app # "app::..." or current app + name = $2 + + attachment = begin + get_attachment(name, :app => app) + rescue Heroku::API::Errors::NotFound + end + + return [attachment] if attachment + + get_attachments(:app => app).select { |att| att["name"][name] } + else + [] + end + + filter ? results.select(&filter) : results + end + + # Finds a single attachment unambiguously given an identifier. + # + # Returns an attachment hash or exits with an error. + def resolve_attachment!(identifier, &filter) + results = resolve_attachment(identifier, &filter) + + case results.count + when 1 + results[0] + when 0 + error("Can not find attachment with #{identifier.inspect}") + else + app = results.first['app']['name'] + error("Multiple attachments on #{app} match #{identifier.inspect}.\n" + + "Did you mean one of:\n\n" + + results.map { |att| "- #{att['name']}" }.join("\n")) + end + end + + # Finds add-ons that match provided identifier. + # + # Supports: + # * add-on resource UUID + # * add-on resource name (@my-db / my-db) + # * attachment name (other-app::ATTACHMENT / ATTACHMENT on current app) + # * service name + # * service:plan name + # + # Returns an array in every case except for when using a service name for an + # non-existent add-on. In that case, the error message is returned. + # + def resolve_addon(identifier, &filter) + results = case identifier + when UUID + return [get_addon(identifier)].compact + when ATTACHMENT + # identifier -> Array[Attachment] -> uniq Array[Addon] + matches = resolve_attachment(identifier) + matches. + map { |att| att['addon']['id'] }. + uniq. + map { |addon_id| get_addon(addon_id) } + else # try both resource and service identifiers, because they look similar + if identifier =~ RESOURCE + name = $1 + + addon = begin + get_addon(name) + rescue Heroku::API::Errors::Forbidden + # treat permission error as no match because there might exist a + # resource on someone else's app that has a name which + # corresponds to a service name that we wish to check below (e.g. + # "memcachier") + end + + return [addon] if addon + end + + if identifier =~ SERVICE_PLAN + service_name, plan_name = *[$1, $2].compact + full_plan_name = [service_name, plan_name].join(':') if plan_name + + addons = get_addons(:app => app).select do |addon| + addon['addon_service']['name'] == service_name && # match service + [nil, addon['plan']['name']].include?(full_plan_name) && # match plan, IFF specified + addon['app']['name'] == app # /apps/:id/addons returns un-owned add-ons + end + + return addons + end + + [] + end + + filter ? results.select(&filter) : results + end + + # Finds a single add-on unambiguously given an identifier. + # + # Returns an add-on hash or exits with an error. + def resolve_addon!(identifier, &filter) + results = resolve_addon(identifier, &filter) + + case results.count + when 1 + results[0] + when 0 + error("Can not find add-on with #{identifier.inspect}") + else + error("Multiple add-ons match #{identifier.inspect}.\n" + + "Use the name of add-on resource:\n\n" + + results.map { |a| "- #{a['name']} (#{a['plan']['name']})" }.join("\n")) + end + end + end + end +end diff --git a/lib/heroku/helpers/heroku_postgresql.rb b/lib/heroku/helpers/heroku_postgresql.rb index bd1f21866..40cd1ed63 100644 --- a/lib/heroku/helpers/heroku_postgresql.rb +++ b/lib/heroku/helpers/heroku_postgresql.rb @@ -80,6 +80,11 @@ def hpg_addon_name end end + def app_config_vars + protect_missing_app + @app_config_vars ||= api.get_config_vars(app_name).body + end + private def protect_missing_app @@ -89,11 +94,6 @@ def protect_missing_app end end - def app_config_vars - protect_missing_app - @app_config_vars ||= api.get_config_vars(app_name).body - end - def app_attachments protect_missing_app @app_attachments ||= api.get_attachments(app_name).body.map { |raw| Attachment.new(raw) } @@ -108,7 +108,8 @@ def hpg_databases } @hpg_databases = Hash[ pairs ] - if find_database_url_real_attachment + # TODO: don't bother doing this if DATABASE_URL is already present in hash! + if !@hpg_databases.key?('DATABASE_URL') && find_database_url_real_attachment @hpg_databases['DATABASE_URL'] = find_database_url_real_attachment end @@ -197,7 +198,7 @@ def hpg_translate_db_opts_to_urls(addon, config) else attachment = resolver.resolve(val) if attachment.starter_plan? - error("#{opt.tr 'f', 'F'} is only available on production databases.") + error("#{opt.capitalize} is only available on production databases.") end argument_url = attachment.url end diff --git a/lib/heroku/helpers/log_displayer.rb b/lib/heroku/helpers/log_displayer.rb index 50ba95e13..5cd41234a 100644 --- a/lib/heroku/helpers/log_displayer.rb +++ b/lib/heroku/helpers/log_displayer.rb @@ -5,10 +5,10 @@ class LogDisplayer include Heroku::Helpers - attr_reader :heroku, :app, :opts + attr_reader :heroku, :app, :opts, :force_colors - def initialize(heroku, app, opts) - @heroku, @app, @opts = heroku, app, opts + def initialize(heroku, app, opts, force_colors = false) + @heroku, @app, @opts, @force_colors = heroku, app, opts, force_colors end def display_logs @@ -17,19 +17,11 @@ def display_logs @token = nil heroku.read_logs(app, opts) do |chunk| - unless chunk.empty? - if STDOUT.isatty && ENV.has_key?("TERM") - display(colorize(chunk)) - else - display(chunk) - end - end + display(display_colors? ? colorize(chunk) : chunk) unless chunk.empty? end rescue Errno::EPIPE rescue Interrupt => interrupt - if STDOUT.isatty && ENV.has_key?("TERM") - display("\e[0m") - end + display("\e[0m") if display_colors? raise(interrupt) end @@ -66,5 +58,10 @@ def parse_log(log) [1, 2, 4].map { |i| parsed[i] } end + private + + def display_colors? + force_colors || (STDOUT.isatty && ENV.has_key?("TERM")) + end end end diff --git a/lib/heroku/http_instrumentor.rb b/lib/heroku/http_instrumentor.rb new file mode 100644 index 000000000..60e1d4fd6 --- /dev/null +++ b/lib/heroku/http_instrumentor.rb @@ -0,0 +1,46 @@ +class HTTPInstrumentor + class << self + def filter_parameter(parameter) + @filter_parameters ||= [] + @filter_parameters << parameter + end + + def instrument(name, params={}, &block) + headers = params[:headers] + case name + when "excon.error" + $stderr.puts params[:error].message + when "excon.request" + $stderr.print "HTTP #{params[:method].upcase} #{params[:scheme]}://#{params[:host]}#{params[:path]} " + $stderr.print "[auth] " if headers['Authorization'] && headers['Authorization'] != 'Basic Og==' + $stderr.print "[2fa] " if headers['Heroku-Two-Factor-Code'] + $stderr.puts filter(params[:query]) + when "excon.response" + $stderr.puts "#{params[:status]} #{params[:reason_phrase]}" + $stderr.puts "request-id: #{headers['Request-id']}" if headers['Request-Id'] + if headers['Content-Encoding'] == 'gzip' + $stderr.puts filter(ungzip(params[:body])) + else + $stderr.puts filter(params[:body]) + end + else + $stderr.puts name + end + yield if block_given? + end + + private + + def ungzip(string) + Zlib::GzipReader.new(StringIO.new(string)).read() + end + + def filter(obj) + string = obj.to_s + (@filter_parameters || []).each do |parameter| + string.gsub! parameter, '[FILTERED]' + end + string + end + end +end diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb new file mode 100644 index 000000000..269965fce --- /dev/null +++ b/lib/heroku/jsplugin.rb @@ -0,0 +1,187 @@ +require 'rbconfig' + +class Heroku::JSPlugin + extend Heroku::Helpers + + def self.setup? + File.exists? bin + end + + def self.try_takeover(command, args) + command = command.split(':') + if command.length == 1 + command = commands.find { |t| t["topic"] == command[0] && t["command"] == nil } + else + command = commands.find { |t| t["topic"] == command[0] && t["command"] == command[1] } + end + return if !command || command["hidden"] + run(command['topic'], command['command'], ARGV[1..-1]) + end + + def self.load! + return unless setup? + this = self + topics.each do |topic| + Heroku::Command.register_namespace( + :name => topic['name'], + :description => " #{topic['description']}" + ) unless topic['hidden'] || Heroku::Command.namespaces.include?(topic['name']) + end + commands.each do |plugin| + help = "\n\n #{plugin['fullHelp'].split("\n").join("\n ")}" + klass = Class.new do + def initialize(args, opts) + @args = args + @opts = opts + end + end + klass.send(:define_method, :run) do + this.run(plugin['topic'], plugin['command'], ARGV[1..-1]) + end + Heroku::Command.register_command( + :command => plugin['command'] ? "#{plugin['topic']}:#{plugin['command']}" : plugin['topic'], + :namespace => plugin['topic'], + :klass => klass, + :method => :run, + :banner => plugin['usage'], + :summary => " #{plugin['description']}", + :help => help, + :hidden => plugin['hidden'], + ) + end + end + + def self.plugins + return [] unless setup? + @plugins ||= `"#{bin}" plugins`.lines.map do |line| + name, version = line.split + { :name => name, :version => version } + end + end + + def self.is_plugin_installed?(name) + plugins.any? { |p| p[:name] == name } + end + + def self.topics + commands_info['topics'] + rescue + $stderr.puts "error loading plugin topics" + return [] + end + + def self.commands + commands_info['commands'] + rescue + $stderr.puts "error loading plugin commands" + # Remove v4 if it is causing issues (for now) + File.delete(bin) rescue nil + return [] + end + + def self.commands_info + copy_ca_cert rescue nil # TODO: remove this once most of the users have the cacert setup + @commands_info ||= json_decode(`"#{bin}" commands --json`) + end + + def self.install(name, opts={}) + self.setup + system "\"#{bin}\" plugins:install #{name}" if opts[:force] || !self.is_plugin_installed?(name) + end + + def self.uninstall(name) + system "\"#{bin}\" plugins:uninstall #{name}" + end + + def self.update + system "\"#{bin}\" update" + end + + def self.version + `"#{bin}" version` + end + + def self.bin + if os == 'windows' + File.join(Heroku::Helpers.home_directory, ".heroku", "heroku-cli.exe") + else + File.join(Heroku::Helpers.home_directory, ".heroku", "heroku-cli") + end + end + + def self.setup + return if File.exist? bin + $stderr.print "Installing Heroku Toolbelt v4..." + FileUtils.mkdir_p File.dirname(bin) + copy_ca_cert + opts = excon_opts.merge(:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]) + resp = Excon.get(url, opts) + open(bin, "wb") do |file| + file.write(resp.body) + end + File.chmod(0755, bin) + if Digest::SHA1.file(bin).hexdigest != manifest['builds'][os][arch]['sha1'] + File.delete bin + raise 'SHA mismatch for heroku-cli' + end + $stderr.puts " done" + end + + def self.copy_ca_cert + to = File.join(Heroku::Helpers.home_directory, ".heroku", "cacert.pem") + return if File.exists?(to) + from = File.expand_path("../../../data/cacert.pem", __FILE__) + FileUtils.copy(from, to) + end + + def self.run(topic, command, args) + cmd = command ? "#{topic}:#{command}" : topic + debug("running #{cmd} on v4") + exec self.bin, cmd, *args + end + + def self.arch + case RbConfig::CONFIG['host_cpu'] + when /x86_64/ + "amd64" + when /arm/ + "arm" + else + "386" + end + end + + def self.os + case RbConfig::CONFIG['host_os'] + when /darwin|mac os/ + "darwin" + when /linux/ + "linux" + when /mswin|msys|mingw|cygwin|bccwin|wince|emc/ + "windows" + when /openbsd/ + "openbsd" + when /freebsd/ + "freebsd" + else + raise "unsupported on #{RbConfig::CONFIG['host_os']}" + end + end + + def self.manifest + @manifest ||= JSON.parse(Excon.get("https://d1gvo455cekpjp.cloudfront.net/master/manifest.json", excon_opts).body) + end + + def self.excon_opts + if os == 'windows' || ENV['HEROKU_SSL_VERIFY'] == 'disable' + # S3 SSL downloads do not work from ruby in Windows + {:ssl_verify_peer => false} + else + {} + end + end + + def self.url + manifest['builds'][os][arch]['url'] + ".gz" + end +end diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb new file mode 100644 index 000000000..f6a8f70cb --- /dev/null +++ b/lib/heroku/open_ssl.rb @@ -0,0 +1,93 @@ +require "heroku/helpers" +require "tempfile" + +module Heroku + module OpenSSL + def self.openssl(*args) + if args.empty? + ENV["OPENSSL"] || "openssl" + else + system(openssl, *args) + end + end + + def self.openssl=(val) + @checked = false + ENV["OPENSSL"] = val + end + + class CertificateRequest + attr_accessor :domain, :subject, :key_size, :self_signed + + def initialize() + @key_size = 2048 + @self_signed = false + super + end + + def generate + if self_signed + generate_self_signed_certificate + else + generate_csr + end + end + + class Result + attr_accessor :request, :key_file, :csr_file, :crt_file + + def initialize(request, key_file, csr_file, crt_file) + @request = request.dup + @key_file, @csr_file, @crt_file = key_file, csr_file, crt_file + end + end + + private + def generate_csr + keyfile = "#{domain}.key" + csrfile = "#{domain}.csr" + + openssl_req_new(keyfile, csrfile) or raise GenericError, "Key and CSR generation failed: #{$?}" + + return Result.new(self, keyfile, csrfile, nil) + end + + def generate_self_signed_certificate + keyfile = "#{domain}.key" + crtfile = "#{domain}.crt" + + openssl_req_new(keyfile, crtfile, "-x509") or raise GenericError, "Key and self-signed certificate generation failed: #{$?}" + + return Result.new(self, keyfile, nil, crtfile) + end + + def openssl_req_new(keyfile, outfile, *args) + Heroku::OpenSSL.ensure_openssl_installed! + Heroku::OpenSSL.openssl("req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", outfile, "-subj", subject, *args) + end + end + + class GenericError < StandardError; end + + class NotInstalledError < GenericError + include Heroku::Helpers + + def installation_hint + if running_on_a_mac? + "With Homebrew installed, run the following command:\n$ brew install openssl" + elsif running_on_windows? + "Download and install OpenSSL from ." + else + # Probably some kind of Linux or other Unix. Who knows what package manager they're using? + "Make sure your package manager's 'openssl' package is installed." + end + end + end + + def self.ensure_openssl_installed! + return if @checked + openssl("version") or raise NotInstalledError + @checked = true + end + end +end diff --git a/lib/heroku/plugin.rb b/lib/heroku/plugin.rb index ae6291bd9..ad1a7214d 100644 --- a/lib/heroku/plugin.rb +++ b/lib/heroku/plugin.rb @@ -8,27 +8,31 @@ class Plugin class ErrorUpdatingSymlinkPlugin < StandardError; end DEPRECATED_PLUGINS = %w( + heroku-addon-attachments heroku-cedar heroku-certs heroku-credentials heroku-dyno-size + heroku-dyno-types + heroku-fork heroku-kill heroku-labs heroku-logging heroku-netrc + heroku-orgs heroku-pgdumps heroku-postgresql + heroku-push heroku-releases heroku-shared-postgresql heroku-sql-console heroku-status heroku-stop heroku-suggest + heroku-symbol heroku-two-factor pgbackups-automate pgcmd - heroku-fork - heroku-orgs ) attr_reader :name, :uri @@ -58,6 +62,13 @@ def self.load_plugin(plugin) load "#{folder}/init.rb" if File.exists? "#{folder}/init.rb" rescue ScriptError, StandardError => error styled_error(error, "Unable to load plugin #{plugin}.") + action("Updating #{plugin}") do + begin + Heroku::Plugin.new(plugin).update + rescue => e + $stderr.puts(format_with_bang(e.to_s)) + end + end false end end @@ -122,10 +133,10 @@ def update unless git('config --get branch.master.remote').empty? message = git("pull") unless $?.success? - error("Unable to update #{name}.\n" + message) + raise "Unable to update #{name}.\n" + message end else - error(<<-ERROR) + raise <<-ERROR #{name} is a legacy plugin installation. Enable updating by reinstalling with `heroku plugins:install`. ERROR diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb new file mode 100644 index 000000000..ef26c9e44 --- /dev/null +++ b/lib/heroku/rollbar.rb @@ -0,0 +1,67 @@ +module Rollbar + extend Heroku::Helpers + + def self.error(e) + return if ENV['HEROKU_DISABLE_ERROR_REPORTING'] + payload = json_encode(build_payload(e)) + response = Excon.post('https://api.rollbar.com/api/1/item/', :body => payload) + response = json_decode(response.body) + raise response.to_s if response["err"] != 0 + response["result"]["uuid"] + rescue + $stderr.puts(e.message, e.backtrace.join("\n")) + nil + end + + private + + def self.build_payload(e) + if e.is_a? Exception + build_trace_payload(e) + else + build_message_payload(e.to_s) + end + end + + def self.build_trace_payload(e) + payload = base_payload + payload[:data][:body] = {:trace => trace_from_exception(e)} + payload + end + + def self.build_message_payload(message) + payload = base_payload + payload[:data][:body] = {:message => {:body => message}} + payload + end + + def self.base_payload + { + :access_token => '488f0c3af3d6450cb5b5827c8099dbff', + :data => { + :platform => 'client', + :environment => 'production', + :code_version => Heroku::VERSION, + :client => { :platform => RUBY_PLATFORM, :ruby => RUBY_VERSION }, + :request => { :command => ARGV[0] } + } + } + end + + def self.trace_from_exception(e) + { + :frames => frames_from_exception(e), + :exception => { + :class => e.class.to_s, + :message => e.message + } + } + end + + def self.frames_from_exception(e) + e.backtrace.map do |line| + filename, lineno, method = line.scan(/(.+):(\d+):in `(.*)'/)[0] + { :filename => filename, :lineno => lineno.to_i, :method => method } + end + end +end diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 87540aa8c..1a6546a0e 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -4,6 +4,7 @@ module Heroku module Updater + extend Heroku::Helpers def self.error(message) raise Heroku::Command::CommandFailed.new(message) @@ -21,6 +22,20 @@ def self.updated_client_path File.join(Heroku::Helpers.home_directory, ".heroku", "client") end + def self.latest_version + http_get('https://assets.heroku.com/heroku-client/VERSION').chomp + end + + def self.official_zip_hash + http_get('https://toolbelt.heroku.com/update/hash').chomp + end + + def self.http_get(url) + require 'excon' + require 'heroku/excon' + Excon.get_with_redirect(url, :nonblock => false).body + end + def self.latest_local_version installed_version = client_version_from_path(installed_client_path) updated_version = client_version_from_path(updated_client_path) @@ -31,6 +46,14 @@ def self.latest_local_version end end + def self.needs_update? + compare_versions(latest_version, latest_local_version) > 0 + end + + def self.needs_minor_update? + latest_version[0..3] != latest_local_version[0..3] + end + def self.client_version_from_path(path) version_file = File.join(path, "lib/heroku/version.rb") if File.exists?(version_file) @@ -51,7 +74,8 @@ def self.check_disabled! end end - def self.wait_for_lock(path, wait_for=5, check_every=0.5) + def self.wait_for_lock(wait_for=5, check_every=0.5) + path = updating_lock_path start = Time.now.to_i while File.exists?(path) sleep check_every @@ -59,69 +83,82 @@ def self.wait_for_lock(path, wait_for=5, check_every=0.5) Heroku::Helpers.error "Unable to acquire update lock" end end - begin - FileUtils.touch path - ret = yield - ensure - FileUtils.rm_f path - end - ret + FileUtils.mkdir_p File.dirname(path) + FileUtils.touch path + yield + ensure + FileUtils.rm_f path end - def self.autoupdate? - true + def self.autoupdate + # if we've updated in the last hour, don't try again + if File.exists?(last_autoupdate_path) + return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 60*60 + end + FileUtils.mkdir_p File.dirname(last_autoupdate_path) + FileUtils.touch last_autoupdate_path + return warn_if_out_of_date if disable + update end - def self.update(url, autoupdate=false) - wait_for_lock(updating_lock_path, 5) do - require "excon" - require "heroku" - require "heroku/excon" - require "tmpdir" - require "zip/zip" + def self.warn_if_out_of_date + $stderr.puts "WARNING: Toolbelt v#{latest_version} update available." if needs_minor_update? + end - latest_version = Excon.get_with_redirect("http://assets.heroku.com/heroku-client/VERSION", :nonblock => false).body.chomp + def self.update(prerelease=false) + return unless prerelease || needs_update? - if compare_versions(latest_version, latest_local_version) > 0 - Dir.mktmpdir do |download_dir| - File.open("#{download_dir}/heroku.zip", "wb") do |file| - file.print Excon.get_with_redirect(url, :nonblock => false).body - end + stderr_print 'updating Heroku CLI...' + wait_for_lock do + require "tmpdir" + require "zip" + + Dir.mktmpdir do |download_dir| + zip_filename = "#{download_dir}/heroku.zip" + if prerelease + url = "https://toolbelt.heroku.com/download/beta-zip" + else + url = "https://toolbelt.heroku.com/download/zip" + end - hash = Digest::SHA256.file("#{download_dir}/heroku.zip").hexdigest - official_hash = Excon.get_with_redirect("https://toolbelt.heroku.com/update/hash", :nonblock => false).body.chomp + download_file(url, zip_filename) + unless prerelease + hash = Digest::SHA256.file(zip_filename).hexdigest + error "Update hash signature mismatch" unless hash == official_zip_hash + end - error "Update hash signature mismatch" unless hash == official_hash + extract_zip(zip_filename, download_dir) + FileUtils.rm_f zip_filename - Zip::ZipFile.open("#{download_dir}/heroku.zip") do |zip| - zip.each do |entry| - target = File.join(download_dir, entry.to_s) - FileUtils.mkdir_p File.dirname(target) - zip.extract(entry, target) { true } - end - end + version = client_version_from_path(download_dir) - FileUtils.rm "#{download_dir}/heroku.zip" + # do not replace beta version if it is old + return if compare_versions(version, latest_local_version) < 0 - old_version = latest_local_version - new_version = client_version_from_path(download_dir) + FileUtils.rm_rf updated_client_path + FileUtils.mkdir_p File.dirname(updated_client_path) + FileUtils.cp_r download_dir, updated_client_path - if compare_versions(new_version, old_version) < 0 && !autoupdate - Heroku::Helpers.error("Installed version (#{old_version}) is newer than the latest available update (#{new_version})") - end + stderr_puts "done. Updated to #{version}" + version + end + end + end - FileUtils.rm_rf updated_client_path - FileUtils.mkdir_p File.dirname(updated_client_path) - FileUtils.cp_r download_dir, updated_client_path + def self.download_file(from_url, to_filename) + File.open(to_filename, "wb") do |file| + file.print http_get(from_url) + end + end - new_version - end - else - false # already up to date + def self.extract_zip(filename, dir) + Zip::File.open(filename) do |zip| + zip.each do |entry| + target = File.join(dir, entry.to_s) + FileUtils.mkdir_p File.dirname(target) + entry.extract(target) { true } end end - ensure - FileUtils.rm_f(updating_lock_path) end def self.compare_versions(first_version, second_version) @@ -140,32 +177,14 @@ def self.inject_libpath end load('heroku/updater.rb') # reload updated updater end - - background_update! end def self.last_autoupdate_path File.join(Heroku::Helpers.home_directory, ".heroku", "autoupdate.last") end - def self.background_update! - # if we've updated in the last 300 seconds, dont try again - if File.exists?(last_autoupdate_path) - return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 300 - end - log_path = File.join(Heroku::Helpers.home_directory, '.heroku', 'autoupdate.log') - FileUtils.mkdir_p File.dirname(log_path) - heroku_binary = File.expand_path($0) - pid = if defined?(RUBY_VERSION) and RUBY_VERSION =~ /^1\.8\.\d+/ - fork do - exec("\"#{heroku_binary}\" update &> #{log_path} 2>&1") - end - else - spawn("\"#{heroku_binary}\" update", {:err => log_path, :out => log_path}) - end - Process.detach(pid) - FileUtils.mkdir_p File.dirname(last_autoupdate_path) - FileUtils.touch last_autoupdate_path + def self.warn_if_updating + warn "WARNING: Toolbelt is currently updating" if File.exists?(updating_lock_path) end end end diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 43b86f551..77b1f61be 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.9.4" + VERSION = "3.37.6" end diff --git a/resources/deb/heroku-toolbelt/apt-ftparchive.conf b/resources/deb/heroku-toolbelt/apt-ftparchive.conf new file mode 100644 index 000000000..f8b6264a7 --- /dev/null +++ b/resources/deb/heroku-toolbelt/apt-ftparchive.conf @@ -0,0 +1,4 @@ +APT::FTPArchive::Release { + Origin "Heroku, Inc."; + Suite "stable"; +} diff --git a/resources/deb/heroku-toolbelt/control b/resources/deb/heroku-toolbelt/control new file mode 100644 index 000000000..726cef4ae --- /dev/null +++ b/resources/deb/heroku-toolbelt/control @@ -0,0 +1,9 @@ +Package: heroku-toolbelt +Version: <%= version %> +Section: main +Priority: standard +Architecture: all +Depends: git-core, foreman, heroku (= <%= version %>) +Installed-Size: +Maintainer: Heroku +Description: A metapackage for working with the Heroku platform. diff --git a/resources/deb/heroku/control b/resources/deb/heroku/control new file mode 100644 index 000000000..c7f9a96df --- /dev/null +++ b/resources/deb/heroku/control @@ -0,0 +1,8 @@ +Package: heroku +Version: <%= version %> +Section: main +Priority: standard +Architecture: all +Depends: ruby2.1|ruby2.0|libopenssl-ruby1.9.1, ruby2.1|ruby2.0|libreadline-ruby1.9.1, ruby2.1|ruby2.0|ruby1.9.1, libssl0.9.8 (>= 0.9.8k) | libssl1.0.0 +Maintainer: Heroku +Description: Client library and CLI to deploy apps on Heroku. diff --git a/dist/resources/deb/heroku b/resources/deb/heroku/heroku similarity index 100% rename from dist/resources/deb/heroku rename to resources/deb/heroku/heroku diff --git a/dist/resources/deb/postinst b/resources/deb/heroku/postinst similarity index 100% rename from dist/resources/deb/postinst rename to resources/deb/heroku/postinst diff --git a/resources/exe/foreman b/resources/exe/foreman new file mode 100755 index 000000000..2fee762fe --- /dev/null +++ b/resources/exe/foreman @@ -0,0 +1,9 @@ +#!/bin/sh +# find embedded ruby relative to script +bindir=`cd -P "${0%/*}/../ruby-1.9.3/bin" 2>/dev/null; pwd` +exec "$bindir/ruby" -x "$0" "$@" + +#!/usr/bin/env ruby +# encoding: UTF-8 +require "foreman/cli" +Foreman::CLI.start diff --git a/resources/exe/foreman.bat b/resources/exe/foreman.bat new file mode 100644 index 000000000..f15c848c6 --- /dev/null +++ b/resources/exe/foreman.bat @@ -0,0 +1,11 @@ +:: Don't use ECHO OFF to avoid possible change of ECHO +:: Use SETLOCAL so variables set in the script are not persisted +@SETLOCAL + +:: Add bundled ruby version to the PATH, use HerokuPath as starting point +@SET HEROKU_RUBY="%HerokuPath%\ruby-1.9.3\bin" +@SET PATH=%HEROKU_RUBY%;%PATH% + +:: Invoke 'foreman' (the calling script) as argument to ruby. +:: Also forward all the arguments provided to it. +@ruby.exe "%~dpn0" %* diff --git a/resources/exe/heroku b/resources/exe/heroku new file mode 100755 index 000000000..7a7fd4ce5 --- /dev/null +++ b/resources/exe/heroku @@ -0,0 +1,29 @@ +#!/bin/sh +# find embedded ruby relative to script +bindir=`cd -P "${0%/*}/../ruby-1.9.3/bin" 2>/dev/null; pwd` +exec "$bindir/ruby" -x "$0" "$@" + +#!/usr/bin/env ruby +# encoding: UTF-8 + +# resolve bin path, ignoring symlinks +require "pathname" +bin_file = Pathname.new(__FILE__).realpath + +# add locally vendored gems to libpath +gem_dir = File.expand_path("../../vendor/gems", bin_file) +Dir["#{gem_dir}/**/lib"].each do |libdir| + $:.unshift libdir +end + +# add self to libpath +$:.unshift File.expand_path("../../lib", bin_file) + +# inject any code in ~/.heroku/client over top +require "heroku/updater" +Heroku::Updater.inject_libpath + +# start up the CLI +require "heroku/cli" +Heroku.user_agent = "heroku/toolbelt/#{Heroku::VERSION} (#{RUBY_PLATFORM}) ruby/#{RUBY_VERSION}" +Heroku::CLI.start(*ARGV) diff --git a/resources/exe/heroku-codesign-cert.encrypted.pvk b/resources/exe/heroku-codesign-cert.encrypted.pvk new file mode 100644 index 000000000..d8e6ca573 Binary files /dev/null and b/resources/exe/heroku-codesign-cert.encrypted.pvk differ diff --git a/resources/exe/heroku-codesign-cert.spc b/resources/exe/heroku-codesign-cert.spc new file mode 100644 index 000000000..a5c57f3d5 Binary files /dev/null and b/resources/exe/heroku-codesign-cert.spc differ diff --git a/resources/exe/heroku.bat b/resources/exe/heroku.bat new file mode 100644 index 000000000..f4f8bacc8 --- /dev/null +++ b/resources/exe/heroku.bat @@ -0,0 +1,11 @@ +:: Don't use ECHO OFF to avoid possible change of ECHO +:: Use SETLOCAL so variables set in the script are not persisted +@SETLOCAL + +:: Add bundled ruby version to the PATH, use HerokuPath as starting point +@SET HEROKU_RUBY="%HerokuPath%\ruby-1.9.3\bin" +@SET PATH=%HEROKU_RUBY%;%PATH%;%ProgramFiles(x86)%\Git\bin + +:: Invoke 'heroku' (the calling script) as argument to ruby. +:: Also forward all the arguments provided to it. +@ruby.exe "%~dpn0" %* diff --git a/resources/exe/heroku.iss b/resources/exe/heroku.iss new file mode 100644 index 000000000..0c2af35bc --- /dev/null +++ b/resources/exe/heroku.iss @@ -0,0 +1,76 @@ +[Setup] +AppName=Heroku Toolbelt +AppVersion=<%= version %> +AppVerName=Heroku Toolbelt <%= version %> +AppPublisher=Heroku, Inc. +AppPublisherURL=http://www.heroku.com/ +DefaultDirName={pf}\Heroku +DefaultGroupName=Heroku +Compression=lzma2 +SolidCompression=yes +OutputBaseFilename=<%= File.basename(exe_task.name, ".exe") %> +OutputDir=.. +ChangesEnvironment=yes +UsePreviousSetupType=no +AlwaysShowComponentsList=no +SignTool=mono-signcode + +; For Ruby expansion ~ 32MB (installed) - 12MB (installer) +ExtraDiskSpaceRequired=20971520 + +[Types] +Name: client; Description: "Full Installation"; +Name: custom; Description: "Custom Installation"; flags: iscustom + +[Components] +Name: "toolbelt"; Description: "Heroku Toolbelt"; Types: "client custom" +Name: "toolbelt/client"; Description: "Heroku Client"; Types: "client custom"; Flags: fixed +Name: "toolbelt/foreman"; Description: "Foreman"; Types: "client custom" +Name: "toolbelt/git"; Description: "Git and SSH"; Types: "client custom"; Check: "not IsProgramInstalled('git.exe')" +Name: "toolbelt/git"; Description: "Git and SSH"; Check: "IsProgramInstalled('git.exe')" + +[Files] +Source: "heroku\*.*"; DestDir: "{app}"; Flags: recursesubdirs; Components: "toolbelt/client" +Source: "installers\rubyinstaller.exe"; DestDir: "{tmp}"; Components: "toolbelt/client" +Source: "installers\git.exe"; DestDir: "{tmp}"; Components: "toolbelt/git" + +[Registry] +Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "HerokuPath"; \ + ValueData: "{app}" +Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "Path"; \ + ValueData: "{olddata};{app}\bin"; Check: NeedsAddPath(ExpandConstant('{app}\bin')) +Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "Path"; \ + ValueData: "{olddata};{pf}\git\cmd"; Check: NeedsAddPath(ExpandConstant('{pf}\git\cmd')) + +[Run] +Filename: "{tmp}\rubyinstaller.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-1.9.3"""; \ + Flags: shellexec waituntilterminated; StatusMsg: "Installing Ruby"; Components: "toolbelt/client" +Filename: "{app}\ruby-1.9.3\bin\gem.bat"; Parameters: "install foreman --no-rdoc --no-ri"; \ + Flags: runhidden shellexec waituntilterminated; StatusMsg: "Installing Foreman"; Components: "toolbelt/foreman" +Filename: "{tmp}\git.exe"; Parameters: "/silent /nocancel /noicons"; \ + Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git" + +[Code] + +function NeedsAddPath(Param: string): boolean; +var + OrigPath: string; +begin + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'Path', OrigPath) + then begin + Result := True; + exit; + end; + // look for the path with leading and trailing semicolon + // Pos() returns 0 if not found + Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0; +end; + +function IsProgramInstalled(Name: string): boolean; +var + ResultCode: integer; +begin + Result := Exec(Name, 'version', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; diff --git a/resources/exe/ssh-keygen.bat b/resources/exe/ssh-keygen.bat new file mode 100644 index 000000000..cb16d2a30 --- /dev/null +++ b/resources/exe/ssh-keygen.bat @@ -0,0 +1,3 @@ +@SETLOCAL +@SET HOME=%USERPROFILE% +@"%HerokuPath%\..\Git\bin\ssh-keygen.exe" %* diff --git a/dist/resources/pkg/Distribution.erb b/resources/pkg/Distribution.erb similarity index 67% rename from dist/resources/pkg/Distribution.erb rename to resources/pkg/Distribution.erb index 795215b31..43638a952 100644 --- a/dist/resources/pkg/Distribution.erb +++ b/resources/pkg/Distribution.erb @@ -1,15 +1,20 @@ - Heroku Client - + Heroku Toolbelt + + + + + + #foreman.pkg #heroku-client.pkg #ruby.pkg diff --git a/dist/resources/pkg/PackageInfo.erb b/resources/pkg/PackageInfo.erb similarity index 100% rename from dist/resources/pkg/PackageInfo.erb rename to resources/pkg/PackageInfo.erb diff --git a/dist/resources/pkg/heroku b/resources/pkg/heroku old mode 100644 new mode 100755 similarity index 100% rename from dist/resources/pkg/heroku rename to resources/pkg/heroku diff --git a/dist/resources/pkg/postinstall b/resources/pkg/postinstall similarity index 100% rename from dist/resources/pkg/postinstall rename to resources/pkg/postinstall diff --git a/dist/resources/tgz/heroku b/resources/tgz/heroku similarity index 100% rename from dist/resources/tgz/heroku rename to resources/tgz/heroku diff --git a/spec/fixtures/heroku-client-3.9.7.zip b/spec/fixtures/heroku-client-3.9.7.zip new file mode 100644 index 000000000..c109fdcf8 Binary files /dev/null and b/spec/fixtures/heroku-client-3.9.7.zip differ diff --git a/spec/helper/pg_dump_restore_spec.rb b/spec/helper/pg_dump_restore_spec.rb index f99020783..761c51063 100644 --- a/spec/helper/pg_dump_restore_spec.rb +++ b/spec/helper/pg_dump_restore_spec.rb @@ -7,35 +7,35 @@ end it 'requires uris for from and to arguments' do - expect { PgDumpRestore.new(nil , @localdb, mock) }.to raise_error - expect { PgDumpRestore.new(@remotedb, nil , mock) }.to raise_error - expect { PgDumpRestore.new(@remotedb, @localdb, mock) }.to_not raise_error + expect { PgDumpRestore.new(nil , @localdb, double) }.to raise_error + expect { PgDumpRestore.new(@remotedb, nil , double) }.to raise_error + expect { PgDumpRestore.new(@remotedb, @localdb, double) }.to_not raise_error end it 'uses PGPORT from ENV to set local port' do ENV['PGPORT'] = '15432' - expect(PgDumpRestore.new(@remotedb, @localdb, mock).instance_variable_get('@target').port).to eq 15432 + expect(PgDumpRestore.new(@remotedb, @localdb, double).instance_variable_get('@target').port).to eq 15432 end it 'on pulls, prepare requires the local database to not exist' do - mock_command = mock - mock_command.should_receive(:error).once + mock_command = double + expect(mock_command).to receive(:error).once pgdr = PgDumpRestore.new(@remotedb, @localdb, mock_command) - pgdr.should_receive(:`).once.and_return(`false`) + expect(pgdr).to receive(:`).once.and_return(`false`) pgdr.prepare end it 'on pushes, prepare requires the remote database to be empty' do - mock_command = mock - mock_command.should_receive(:error).once + mock_command = double + expect(mock_command).to receive(:error).once pgdr = PgDumpRestore.new(@localdb, @remotedb, mock_command) - mock_command.should_receive(:exec_sql_on_uri).once.and_return("something that isn't a true") + expect(mock_command).to receive(:exec_sql_on_uri).once.and_return("something that isn't a true") pgdr.prepare end it 'executes a proper dump/restore command' do - pgdr = PgDumpRestore.new(@remotedb, @localdb, mock) + pgdr = PgDumpRestore.new(@remotedb, @localdb, double) expect(pgdr.dump_restore_cmd).to match(/ pg_dump .* remotehost .* @@ -49,18 +49,18 @@ describe 'verification' do it 'errors when the extensions do not match' do - mock_command = mock - mock_command.should_receive(:error).once + mock_command = double + expect(mock_command).to receive(:error).once pgdr = PgDumpRestore.new(@localdb, @remotedb, mock_command) - mock_command.should_receive(:exec_sql_on_uri).twice.and_return("these", "don't match") + expect(mock_command).to receive(:exec_sql_on_uri).twice.and_return("these", "don't match") pgdr.verify end it 'is fine when the extensions match' do - mock_command = mock - mock_command.should_not_receive(:error) + mock_command = double + expect(mock_command).not_to receive(:error) pgdr = PgDumpRestore.new(@localdb, @remotedb, mock_command) - mock_command.should_receive(:exec_sql_on_uri).twice.and_return("these match", "these match") + expect(mock_command).to receive(:exec_sql_on_uri).twice.and_return("these match", "these match") pgdr.verify end end diff --git a/spec/heroku/auth_spec.rb b/spec/heroku/auth_spec.rb index ee9a04da9..01d1f2277 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -10,16 +10,16 @@ module Heroku ENV['HEROKU_API_KEY'] = nil @cli = Heroku::Auth - @cli.stub!(:check) - @cli.stub!(:display) - @cli.stub!(:running_on_a_mac?).and_return(false) + allow(@cli).to receive(:check) + allow(@cli).to receive(:display) + allow(@cli).to receive(:running_on_a_mac?).and_return(false) @cli.credentials = nil FakeFS.activate! - FakeFS::File.stub!(:stat).and_return(double('stat', :mode => "0600".to_i(8))) - FakeFS::FileUtils.stub!(:chmod) - FakeFS::File.stub!(:readlines) do |path| + allow(FakeFS::File).to receive(:stat).and_return(double('stat', :mode => "0600".to_i(8))) + allow(FakeFS::FileUtils).to receive(:chmod) + allow(FakeFS::File).to receive(:readlines) do |path| File.read(path).split("\n").map {|line| "#{line}\n"} end @@ -27,7 +27,7 @@ module Heroku File.open(@cli.netrc_path, "w") do |file| file.puts("machine api.heroku.com\n login user\n password pass\n") - file.puts("machine code.heroku.com\n login user\n password pass\n") + file.puts("machine git.heroku.com\n login user\n password pass\n") end end @@ -48,16 +48,18 @@ module Heroku it "should translate to netrc and cleanup" do # preconditions - File.exist?(@cli.legacy_credentials_path).should == true - File.exist?(@cli.netrc_path).should == false + expect(File.exist?(@cli.legacy_credentials_path)).to eq(true) + expect(File.exist?(@cli.netrc_path)).to eq(false) # transition - @cli.get_credentials.should == ['legacy_user', 'legacy_pass'] + expect(@cli.get_credentials.login).to eq('legacy_user') + expect(@cli.get_credentials.password).to eq('legacy_pass') # postconditions - File.exist?(@cli.legacy_credentials_path).should == false - File.exist?(@cli.netrc_path).should == true - Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].should == ['legacy_user', 'legacy_pass'] + expect(File.exist?(@cli.legacy_credentials_path)).to eq(false) + expect(File.exist?(@cli.netrc_path)).to eq(true) + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].login).to eq('legacy_user') + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].password).to eq('legacy_pass') end end @@ -67,34 +69,34 @@ module Heroku end it "gets credentials from environment variables in preference to credentials file" do - @cli.read_credentials.should == ['', ENV['HEROKU_API_KEY']] + expect(@cli.read_credentials).to eq(['', ENV['HEROKU_API_KEY']]) end it "returns a blank username" do - @cli.user.should be_empty + expect(@cli.user).to be_empty end it "returns the api key as the password" do - @cli.password.should == ENV['HEROKU_API_KEY'] + expect(@cli.password).to eq(ENV['HEROKU_API_KEY']) end it "does not overwrite credentials file with environment variable credentials" do - @cli.should_not_receive(:write_credentials) + expect(@cli).not_to receive(:write_credentials) @cli.read_credentials end context "reauthenticating" do before do - @cli.stub!(:ask_for_credentials).and_return(['new_user', 'new_password']) - @cli.stub!(:check) - @cli.should_receive(:check_for_associated_ssh_key) + allow(@cli).to receive(:ask_for_credentials).and_return(['new_user', 'new_password']) + allow(@cli).to receive(:check) @cli.reauthorize end it "updates saved credentials" do - Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].should == ['new_user', 'new_password'] + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].login).to eq('new_user') + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].password).to eq('new_password') end it "returns environment variable credentials" do - @cli.read_credentials.should == ['', ENV['HEROKU_API_KEY']] + expect(@cli.read_credentials).to eq(['', ENV['HEROKU_API_KEY']]) end end @@ -103,56 +105,54 @@ module Heroku @cli.logout end it "should delete saved credentials" do - File.exists?(@cli.legacy_credentials_path).should be_false - Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].should be_nil + expect(File.exists?(@cli.legacy_credentials_path)).to be_falsey + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"]).to be_nil end end end describe "#base_host" do it "returns the host without the first part" do - @cli.base_host("http://foo.bar.com").should == "bar.com" + expect(@cli.base_host("http://foo.bar.com")).to eq("bar.com") end it "works with localhost" do - @cli.base_host("http://localhost:3000").should == "localhost" + expect(@cli.base_host("http://localhost:3000")).to eq("localhost") end end it "asks for credentials when the file doesn't exist" do @cli.delete_credentials - @cli.should_receive(:ask_for_credentials).and_return(["u", "p"]) - @cli.should_receive(:check_for_associated_ssh_key) - @cli.user.should == 'u' - @cli.password.should == 'p' + expect(@cli).to receive(:ask_for_credentials).and_return(["u", "p"]) + expect(@cli.user).to eq('u') + expect(@cli.password).to eq('p') end it "writes credentials and uploads authkey when credentials are saved" do - @cli.stub!(:credentials) - @cli.stub!(:check) - @cli.stub!(:ask_for_credentials).and_return("username", "apikey") - @cli.should_receive(:write_credentials) - @cli.should_receive(:check_for_associated_ssh_key) + allow(@cli).to receive(:credentials) + allow(@cli).to receive(:check) + allow(@cli).to receive(:ask_for_credentials).and_return(["username", "apikey"]) + expect(@cli).to receive(:write_credentials) @cli.ask_for_and_save_credentials end it "save_credentials deletes the credentials when the upload authkey is unauthorized" do - @cli.stub!(:write_credentials) - @cli.stub!(:retry_login?).and_return(false) - @cli.stub!(:ask_for_credentials).and_return("username", "apikey") - @cli.stub!(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } - @cli.should_receive(:delete_credentials) - lambda { @cli.ask_for_and_save_credentials }.should raise_error(SystemExit) + allow(@cli).to receive(:write_credentials) + allow(@cli).to receive(:retry_login?).and_return(false) + allow(@cli).to receive(:ask_for_credentials).and_return(["username", "apikey"]) + allow(@cli).to receive(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } + expect(@cli).to receive(:delete_credentials) + expect { @cli.ask_for_and_save_credentials }.to raise_error(SystemExit) end it "asks for login again when not authorized, for three times" do - @cli.stub!(:read_credentials) - @cli.stub!(:write_credentials) - @cli.stub!(:delete_credentials) - @cli.stub!(:ask_for_credentials).and_return("username", "apikey") - @cli.stub!(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } - @cli.should_receive(:ask_for_credentials).exactly(3).times - lambda { @cli.ask_for_and_save_credentials }.should raise_error(SystemExit) + allow(@cli).to receive(:read_credentials) + allow(@cli).to receive(:write_credentials) + allow(@cli).to receive(:delete_credentials) + allow(@cli).to receive(:ask_for_credentials).and_return(["username", "apikey"]) + allow(@cli).to receive(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } + expect(@cli).to receive(:ask_for_credentials).exactly(3).times + expect { @cli.ask_for_and_save_credentials }.to raise_error(SystemExit) end it "deletes the credentials file" do @@ -160,16 +160,16 @@ module Heroku File.open(@cli.legacy_credentials_path, "w") do |file| file.puts "legacy_user\nlegacy_pass" end - FileUtils.should_receive(:rm_f).with(@cli.legacy_credentials_path) + expect(FileUtils).to receive(:rm_f).with(@cli.legacy_credentials_path) @cli.delete_credentials end it "writes the login information to the credentials file for the 'heroku login' command" do - @cli.stub!(:ask_for_credentials).and_return(['one', 'two']) - @cli.stub!(:check) - @cli.should_receive(:check_for_associated_ssh_key) + allow(@cli).to receive(:ask_for_credentials).and_return(['one', 'two']) + allow(@cli).to receive(:check) @cli.reauthorize - Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].should == (['one', 'two']) + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].login).to eq('one') + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].password).to eq('two') end it "migrates long api keys to short api keys" do @@ -177,79 +177,11 @@ module Heroku api_key = "7e262de8cac430d8a250793ce8d5b334ae56b4ff15767385121145198a2b4d2e195905ef8bf7cfc5" @cli.netrc["api.#{@cli.host}"] = ["user", api_key] - @cli.get_credentials.should == ["user", api_key[0,40]] - %w{api code}.each do |section| - Netrc.read(@cli.netrc_path)["#{section}.#{@cli.host}"].should == ["user", api_key[0,40]] - end - end - - describe "automatic key uploading" do - before(:each) do - FileUtils.mkdir_p("#{@cli.home_directory}/.ssh") - @cli.stub!(:ask_for_credentials).and_return("username", "apikey") - end - - describe "an account with existing keys" do - before :each do - @api = mock(Object) - @response = mock(Object) - @response.should_receive(:body).and_return(['existingkeys']) - @api.should_receive(:get_keys).and_return(@response) - @cli.should_receive(:api).and_return(@api) - end - - it "should not do anything if the account already has keys" do - @cli.should_not_receive(:associate_key) - @cli.check_for_associated_ssh_key - end - end - - describe "an account with no keys" do - before :each do - @api = mock(Object) - @response = mock(Object) - @response.should_receive(:body).and_return([]) - @api.should_receive(:get_keys).and_return(@response) - @cli.should_receive(:api).and_return(@api) - end - - describe "with zero public keys" do - it "should ask to generate a key" do - @cli.should_receive(:ask).and_return("y") - @cli.should_receive(:generate_ssh_key).with("id_rsa") - @cli.should_receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa.pub") - @cli.check_for_associated_ssh_key - end - end - - describe "with one public key" do - before(:each) { FileUtils.touch("#{@cli.home_directory}/.ssh/id_rsa.pub") } - after(:each) { FileUtils.rm("#{@cli.home_directory}/.ssh/id_rsa.pub") } - - it "should upload the key" do - @cli.should_receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa.pub") - @cli.check_for_associated_ssh_key - end - end - - describe "with many public keys" do - before(:each) do - FileUtils.touch("#{@cli.home_directory}/.ssh/id_rsa.pub") - FileUtils.touch("#{@cli.home_directory}/.ssh/id_rsa2.pub") - end - - after(:each) do - FileUtils.rm("#{@cli.home_directory}/.ssh/id_rsa.pub") - FileUtils.rm("#{@cli.home_directory}/.ssh/id_rsa2.pub") - end - - it "should ask which key to upload" do - File.open("#{@cli.home_directory}/.ssh/id_rsa.pub", "w") { |f| f.puts } - @cli.should_receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa2.pub") - @cli.should_receive(:ask).and_return("2") - @cli.check_for_associated_ssh_key - end - end + expect(@cli.get_credentials.login).to eq("user") + expect(@cli.get_credentials.password).to eq(api_key[0,40]) + Auth.subdomains.each do |section| + expect(Netrc.read(@cli.netrc_path)["#{section}.#{@cli.host}"].login).to eq("user") + expect(Netrc.read(@cli.netrc_path)["#{section}.#{@cli.host}"].password).to eq(api_key[0,40]) end end end diff --git a/spec/heroku/client/heroku_postgresql_spec.rb b/spec/heroku/client/heroku_postgresql_spec.rb index 840e24a79..94b575c5c 100644 --- a/spec/heroku/client/heroku_postgresql_spec.rb +++ b/spec/heroku/client/heroku_postgresql_spec.rb @@ -6,7 +6,8 @@ include Heroku::Helpers before do - Heroku::Auth.stub :user => 'user@example.com', :password => 'apitoken' + allow(Heroku::Auth).to receive(:user).and_return('user@example.com') + allow(Heroku::Auth).to receive(:password).and_return('apitoken') end let(:attachment) { double('attachment', :resource_name => 'something-something-42', :starter_plan? => false) } @@ -14,7 +15,7 @@ describe 'api choosing' do it "sends an ingress request to the client for production plans" do - attachment.stub! :starter_plan? => false + expect(attachment).to receive(:starter_plan?).and_return(false) host = 'postgres-api.heroku.com' url = "https://user@example.com:apitoken@#{host}/client/v11/databases/#{attachment.resource_name}/ingress" @@ -25,11 +26,11 @@ client.ingress - a_request(:put, url).should have_been_made.once + expect(a_request(:put, url)).to have_been_made.once end it "sends an ingress request to the client for production plans" do - attachment.stub! :starter_plan? => true + allow(attachment).to receive_messages :starter_plan? => true host = 'postgres-starter-api.heroku.com' url = "https://user@example.com:apitoken@#{host}/client/v11/databases/#{attachment.resource_name}/ingress" @@ -40,7 +41,7 @@ client.ingress - a_request(:put, url).should have_been_made.once + expect(a_request(:put, url)).to have_been_made.once end end @@ -50,21 +51,21 @@ it 'works without the extended option' do stub_request(:get, url).to_return :body => '{}' client.get_database - a_request(:get, url).should have_been_made.once + expect(a_request(:get, url)).to have_been_made.once end it 'works with the extended option' do url2 = url + '?extended=true' stub_request(:get, url2).to_return :body => '{}' client.get_database(true) - a_request(:get, url2).should have_been_made.once + expect(a_request(:get, url2)).to have_been_made.once end it "retries on error, then raises" do stub_request(:get, url).to_return(:body => "error", :status => 500) - client.stub(:sleep) - lambda { client.get_database }.should raise_error RestClient::InternalServerError - a_request(:get, url).should have_been_made.times(4) + allow(client).to receive(:sleep) + expect { client.get_database }.to raise_error RestClient::InternalServerError + expect(a_request(:get, url)).to have_been_made.times(4) end end diff --git a/spec/heroku/client/pgbackups_spec.rb b/spec/heroku/client/pgbackups_spec.rb index b6b7cdfb2..e4fa794cf 100644 --- a/spec/heroku/client/pgbackups_spec.rb +++ b/spec/heroku/client/pgbackups_spec.rb @@ -14,16 +14,16 @@ let(:version) { Heroku::Client.version } it 'still has a heroku gem version' do - version.should be - version.split(/\./).first.to_i.should >= 2 + expect(version).to be + expect(version.split(/\./).first.to_i).to be >= 2 end it 'includes the heroku gem version' do stub_request(:get, transfer_path) client.get_transfers - a_request(:get, transfer_path).with( + expect(a_request(:get, transfer_path).with( :headers => {'X-Heroku-Gem-Version' => version} - ).should have_been_made.once + )).to have_been_made.once end end @@ -36,7 +36,7 @@ client.create_transfer("postgres://from", "postgres://to", "FROMNAME", "TO_NAME") - a_request(:post, transfer_path).should have_been_made.once + expect(a_request(:post, transfer_path)).to have_been_made.once end end diff --git a/spec/heroku/client/rendezvous_spec.rb b/spec/heroku/client/rendezvous_spec.rb index f450ec98a..1cc73f7b8 100644 --- a/spec/heroku/client/rendezvous_spec.rb +++ b/spec/heroku/client/rendezvous_spec.rb @@ -12,51 +12,51 @@ end context "fixup" do it "null" do - @rendezvous.send(:fixup, nil).should be_nil + expect(@rendezvous.send(:fixup, nil)).to be_nil end it "an empty string" do - @rendezvous.send(:fixup, "").should eq "" + expect(@rendezvous.send(:fixup, "")).to eq "" end it "hash" do - @rendezvous.send(:fixup, { :x => :y }).should eq({ :x => :y }) + expect(@rendezvous.send(:fixup, { :x => :y })).to eq({ :x => :y }) end it "default English UTF-8 data" do - @rendezvous.send(:fixup, "heroku").should eq "heroku" + expect(@rendezvous.send(:fixup, "heroku")).to eq "heroku" end it "default Japanese UTF-8 encoded data" do - @rendezvous.send(:fixup, "愛しています").should eq "愛しています" + expect(@rendezvous.send(:fixup, "愛しています")).to eq "愛しています" end if RUBY_VERSION >= "1.9" it "ISO-8859-1 force-encoded data" do - @rendezvous.send(:fixup, "Хероку".force_encoding("ISO-8859-1")).should eq "Хероку".force_encoding("UTF-8") + expect(@rendezvous.send(:fixup, "Хероку".force_encoding("ISO-8859-1"))).to eq "Хероку".force_encoding("UTF-8") end end end context "with mock ssl" do before :each do mock_openssl - @ssl_socket_mock.should_receive(:puts).with("secret") - @ssl_socket_mock.should_receive(:readline).and_return(nil) + expect(@ssl_socket_mock).to receive(:puts).with("secret") + expect(@ssl_socket_mock).to receive(:readline).and_return(nil) end it "should connect to host:post" do - TCPSocket.should_receive(:open).with("heroku.local", 1234).and_return(@tcp_socket_mock) - IO.stub(:select).and_return(nil) - @ssl_socket_mock.stub(:write) - @ssl_socket_mock.stub(:flush) { raise Timeout::Error } - lambda { @rendezvous.start }.should raise_error(Timeout::Error) + expect(TCPSocket).to receive(:open).with("heroku.local", 1234).and_return(@tcp_socket_mock) + allow(IO).to receive(:select).and_return(nil) + allow(@ssl_socket_mock).to receive(:write) + allow(@ssl_socket_mock).to receive(:flush) { raise Timeout::Error } + expect { @rendezvous.start }.to raise_error(Timeout::Error) end it "should callback on_connect" do @rendezvous.on_connect do raise "on_connect" end - TCPSocket.should_receive(:open).and_return(@tcp_socket_mock) - lambda { @rendezvous.start }.should raise_error("on_connect") + expect(TCPSocket).to receive(:open).and_return(@tcp_socket_mock) + expect { @rendezvous.start }.to raise_error("on_connect") end it "should fixup received data" do - TCPSocket.should_receive(:open).and_return(@tcp_socket_mock) - @ssl_socket_mock.should_receive(:readpartial).and_return("The quick brown fox jumps over the lazy dog") - @rendezvous.stub(:fixup) { |data| raise "received: #{data}" } - lambda { @rendezvous.start }.should raise_error("received: The quick brown fox jumps over the lazy dog") + expect(TCPSocket).to receive(:open).and_return(@tcp_socket_mock) + expect(@ssl_socket_mock).to receive(:readpartial).and_return("The quick brown fox jumps over the lazy dog") + allow(@rendezvous).to receive(:fixup) { |data| raise "received: #{data}" } + expect { @rendezvous.start }.to raise_error("received: The quick brown fox jumps over the lazy dog") end end end diff --git a/spec/heroku/client/ssl_endpoint_spec.rb b/spec/heroku/client/ssl_endpoint_spec.rb index d58f1e1cb..a78abf49d 100644 --- a/spec/heroku/client/ssl_endpoint_spec.rb +++ b/spec/heroku/client/ssl_endpoint_spec.rb @@ -10,22 +10,22 @@ stub_request(:post, "https://api.heroku.com/apps/example/ssl-endpoints"). with(:body => { :accept => "json", :pem => "pem content", :key => "key content" }). to_return(:body => %{ {"cname": "tokyo-1050" } }) - @client.ssl_endpoint_add("example", "pem content", "key content").should == { "cname" => "tokyo-1050" } + expect(@client.ssl_endpoint_add("example", "pem content", "key content")).to eq({ "cname" => "tokyo-1050" }) end it "gets info on an ssl endpoint" do stub_request(:get, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050"). to_return(:body => %{ {"cname": "tokyo-1050" } }) - @client.ssl_endpoint_info("example", "tokyo-1050").should == { "cname" => "tokyo-1050" } + expect(@client.ssl_endpoint_info("example", "tokyo-1050")).to eq({ "cname" => "tokyo-1050" }) end it "lists ssl endpoints for an app" do stub_request(:get, "https://api.heroku.com/apps/example/ssl-endpoints"). to_return(:body => %{ [{"cname": "tokyo-1050" }, {"cname": "tokyo-1051" }] }) - @client.ssl_endpoint_list("example").should == [ + expect(@client.ssl_endpoint_list("example")).to eq([ { "cname" => "tokyo-1050" }, { "cname" => "tokyo-1051" }, - ] + ]) end it "removes an ssl endpoint" do @@ -36,13 +36,13 @@ it "rolls back an ssl endpoint" do stub_request(:post, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050/rollback"). to_return(:body => %{ {"cname": "tokyo-1050" } }) - @client.ssl_endpoint_rollback("example", "tokyo-1050").should == { "cname" => "tokyo-1050" } + expect(@client.ssl_endpoint_rollback("example", "tokyo-1050")).to eq({ "cname" => "tokyo-1050" }) end it "updates an ssl endpoint" do stub_request(:put, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050"). with(:body => { :accept => "json", :pem => "pem content", :key => "key content" }). to_return(:body => %{ {"cname": "tokyo-1050" } }) - @client.ssl_endpoint_update("example", "tokyo-1050", "pem content", "key content").should == { "cname" => "tokyo-1050" } + expect(@client.ssl_endpoint_update("example", "tokyo-1050", "pem content", "key content")).to eq({ "cname" => "tokyo-1050" }) end end diff --git a/spec/heroku/client_spec.rb b/spec/heroku/client_spec.rb index 56c2cceac..d9a8826a9 100644 --- a/spec/heroku/client_spec.rb +++ b/spec/heroku/client_spec.rb @@ -8,15 +8,15 @@ before do @client = Heroku::Client.new(nil, nil) - @resource = mock('heroku rest resource') - @client.stub!(:extract_warning) + @resource = double('heroku rest resource') + allow(@client).to receive(:extract_warning) end it "Client.auth -> get user details" do user_info = { "api_key" => "abc" } stub_request(:post, "https://foo:bar@api.heroku.com/login").to_return(:body => json_encode(user_info)) capture_stderr do # capture deprecation message - Heroku::Client.auth("foo", "bar").should == user_info + expect(Heroku::Client.auth("foo", "bar")).to eq(user_info) end end @@ -29,10 +29,10 @@ EOXML capture_stderr do # capture deprecation message - @client.list.should == [ + expect(@client.list).to eq([ ["example", "test@heroku.com"], ["example2", "test@heroku.com"] - ] + ]) end end @@ -49,10 +49,10 @@ EOXML - @client.stub!(:list_collaborators).and_return([:jon, :mike]) - @client.stub!(:installed_addons).and_return([:addon1]) + allow(@client).to receive(:list_collaborators).and_return([:jon, :mike]) + allow(@client).to receive(:installed_addons).and_return([:addon1]) capture_stderr do # capture deprecation message - @client.info('example').should == { :blessed => 'true', :created_at => '2008-07-08T17:21:50-07:00', :id => '49134', :name => 'example', :production => 'true', :share_public => 'true', :domain_name => nil, :collaborators => [:jon, :mike], :addons => [:addon1] } + expect(@client.info('example')).to eq({ :blessed => 'true', :created_at => '2008-07-08T17:21:50-07:00', :id => '49134', :name => 'example', :production => 'true', :share_public => 'true', :domain_name => nil, :collaborators => [:jon, :mike], :addons => [:addon1] }) end end @@ -62,7 +62,7 @@ untitled-123 EOXML capture_stderr do # capture deprecation message - @client.create_request.should == "untitled-123" + expect(@client.create_request).to eq("untitled-123") end end @@ -72,17 +72,17 @@ newapp EOXML capture_stderr do # capture deprecation message - @client.create_request("newapp").should == "newapp" + expect(@client.create_request("newapp")).to eq("newapp") end end it "create_complete?(name) -> checks if a create request is complete" do - @response = mock('response') - @response.should_receive(:code).and_return(202) - @client.should_receive(:resource).and_return(@resource) - @resource.should_receive(:put).with({}, @client.heroku_headers).and_return(@response) + @response = double('response') + expect(@response).to receive(:code).and_return(202) + expect(@client).to receive(:resource).and_return(@resource) + expect(@resource).to receive(:put).with({}, @client.heroku_headers).and_return(@response) capture_stderr do # capture deprecation message - @client.create_complete?('example').should be_false + expect(@client.create_complete?('example')).to be_falsey end end @@ -119,7 +119,7 @@ stub_api_request(:delete, "/apps/example/consoles/consolename") @client.console('example') do |c| - c.run("1+1").should == '=> 2' + expect(c.run("1+1")).to eq('=> 2') end end @@ -127,7 +127,7 @@ stub_request(:post, %r{.*/apps/example/console}).to_return({ :body => "ERRMSG", :status => 502 }) - lambda { @client.console('example') }.should raise_error(Heroku::Client::AppCrashed, /Your application may have crashed/) + expect { @client.console('example') }.to raise_error(Heroku::Client::AppCrashed, /Your application may have crashed/) end it "restart(app_name) -> restarts the app servers" do @@ -145,7 +145,7 @@ end it "can read old style logs" do - @client.should_receive(:puts).with("oldlogs") + expect(@client).to receive(:puts).with("oldlogs") @client.read_logs("example") end end @@ -158,7 +158,7 @@ it "can read new style logs" do @client.read_logs("example") do |logs| - logs.should == "newlogs" + expect(logs).to eq("newlogs") end end end @@ -167,7 +167,7 @@ it "logs(app_name) -> returns recent output of the app logs" do stub_api_request(:get, "/apps/example/logs").to_return(:body => "log") capture_stderr do # capture deprecation message - @client.logs('example').should == 'log' + expect(@client.logs('example')).to eq('log') end end @@ -179,7 +179,7 @@ EOXML capture_stderr do # capture deprecation message - @client.dynos('example').should == 5 + expect(@client.dynos('example')).to eq(5) end end @@ -191,7 +191,7 @@ EOXML capture_stderr do # capture deprecation message - @client.workers('example').should == 5 + expect(@client.workers('example')).to eq(5) end end @@ -204,21 +204,21 @@ it "rake catches 502s and shows the app crashlog" do e = RestClient::RequestFailed.new - e.stub!(:http_code).and_return(502) - e.stub!(:http_body).and_return('the crashlog') - @client.should_receive(:post).and_raise(e) + allow(e).to receive(:http_code).and_return(502) + allow(e).to receive(:http_body).and_return('the crashlog') + expect(@client).to receive(:post).and_raise(e) capture_stderr do # capture deprecation message - lambda { @client.rake('example', '') }.should raise_error(Heroku::Client::AppCrashed) + expect { @client.rake('example', '') }.to raise_error(Heroku::Client::AppCrashed) end end it "rake passes other status codes (i.e., 500) as standard restclient exceptions" do e = RestClient::RequestFailed.new - e.stub!(:http_code).and_return(500) - e.stub!(:http_body).and_return('not a crashlog') - @client.should_receive(:post).and_raise(e) + allow(e).to receive(:http_code).and_return(500) + allow(e).to receive(:http_body).and_return('not a crashlog') + expect(@client).to receive(:post).and_raise(e) capture_stderr do # capture deprecation message - lambda { @client.rake('example', '') }.should raise_error(RestClient::RequestFailed) + expect { @client.rake('example', '') }.to raise_error(RestClient::RequestFailed) end end @@ -226,7 +226,7 @@ it "scales a process and returns the new count" do stub_api_request(:post, "/apps/example/ps/scale").with(:body => { :type => "web", :qty => "5" }).to_return(:body => "5") capture_stderr do # capture deprecation message - @client.ps_scale("example", :type => "web", :qty => "5").should == 5 + expect(@client.ps_scale("example", :type => "web", :qty => "5")).to eq(5) end end end @@ -241,10 +241,10 @@ EOXML capture_stderr do # capture deprecation message - @client.list_collaborators('example').should == [ + expect(@client.list_collaborators('example')).to eq([ { :email => 'joe@example.com' }, { :email => 'jon@example.com' } - ] + ]) end end @@ -273,7 +273,7 @@ EOXML capture_stderr do # capture deprecation message - @client.list_domains('example').should == [{:domain => 'example1.com'}, {:domain => 'example2.com'}] + expect(@client.list_domains('example')).to eq([{:domain => 'example1.com'}, {:domain => 'example2.com'}]) end end @@ -292,11 +292,11 @@ end it "remove_domain(app_name, domain) -> makes sure a domain is set" do - lambda do + expect do capture_stderr do # capture deprecation message @client.remove_domain('example', '') end - end.should raise_error(ArgumentError) + end.to raise_error(ArgumentError) end it "remove_domains(app_name) -> removes all domain names from app" do @@ -309,8 +309,8 @@ it "add_ssl(app_name, pem, key) -> adds a ssl cert to the domain" do stub_api_request(:post, "/apps/example/ssl").with do |request| body = CGI::parse(request.body) - body["key"].first.should == "thekey" - body["pem"].first.should == "thepem" + expect(body["key"].first).to eq("thekey") + expect(body["pem"].first).to eq("thepem") end.to_return(:body => "{}") @client.add_ssl('example', 'thepem', 'thekey') end @@ -332,7 +332,7 @@ EOXML capture_stderr do # capture deprecation message - @client.keys.should == [ "ssh-dss thekey== joe@workstation" ] + expect(@client.keys).to eq([ "ssh-dss thekey== joe@workstation" ]) end end @@ -376,7 +376,7 @@ it "config_vars(app_name) -> json hash of config vars for the app" do stub_api_request(:get, "/apps/example/config_vars").to_return(:body => '{"A":"one", "B":"two"}') capture_stderr do # capture deprecation message - @client.config_vars('example').should == { 'A' => 'one', 'B' => 'two'} + expect(@client.config_vars('example')).to eq({ 'A' => 'one', 'B' => 'two'}) end end @@ -404,7 +404,7 @@ it "can handle config vars with special characters" do stub_api_request(:delete, "/apps/example/config_vars/foo%5Bbar%5D") capture_stderr do # capture deprecation message - lambda { @client.remove_config_var('example', 'foo[bar]') }.should_not raise_error + expect { @client.remove_config_var('example', 'foo[bar]') }.not_to raise_error end end end @@ -412,73 +412,73 @@ describe "addons" do it "addons -> array with addons available for installation" do stub_api_request(:get, "/addons").to_return(:body => '[{"name":"addon1"}, {"name":"addon2"}]') - @client.addons.should == [{'name' => 'addon1'}, {'name' => 'addon2'}] + expect(@client.addons).to eq([{'name' => 'addon1'}, {'name' => 'addon2'}]) end it "installed_addons(app_name) -> array of installed addons" do stub_api_request(:get, "/apps/example/addons").to_return(:body => '[{"name":"addon1"}]') - @client.installed_addons('example').should == [{'name' => 'addon1'}] + expect(@client.installed_addons('example')).to eq([{'name' => 'addon1'}]) end it "install_addon(app_name, addon_name)" do stub_api_request(:post, "/apps/example/addons/addon1") - @client.install_addon('example', 'addon1').should be_nil + expect(@client.install_addon('example', 'addon1')).to be_nil end it "upgrade_addon(app_name, addon_name)" do stub_api_request(:put, "/apps/example/addons/addon1") - @client.upgrade_addon('example', 'addon1').should be_nil + expect(@client.upgrade_addon('example', 'addon1')).to be_nil end it "downgrade_addon(app_name, addon_name)" do stub_api_request(:put, "/apps/example/addons/addon1") - @client.downgrade_addon('example', 'addon1').should be_nil + expect(@client.downgrade_addon('example', 'addon1')).to be_nil end it "uninstall_addon(app_name, addon_name)" do stub_api_request(:delete, "/apps/example/addons/addon1?"). to_return(:body => json_encode({"message" => nil, "price" => "free", "status" => "uninstalled"})) - @client.uninstall_addon('example', 'addon1').should be_true + expect(@client.uninstall_addon('example', 'addon1')).to be_truthy end it "uninstall_addon(app_name, addon_name) with confirmation" do stub_api_request(:delete, "/apps/example/addons/addon1?confirm=example"). to_return(:body => json_encode({"message" => nil, "price" => "free", "status" => "uninstalled"})) - @client.uninstall_addon('example', 'addon1', :confirm => "example").should be_true + expect(@client.uninstall_addon('example', 'addon1', :confirm => "example")).to be_truthy end it "install_addon(app_name, addon_name) with response" do stub_request(:post, "https://api.heroku.com/apps/example/addons/addon1"). to_return(:body => json_encode({'price' => 'free', 'message' => "Don't Panic"})) - @client.install_addon('example', 'addon1'). - should == { 'price' => 'free', 'message' => "Don't Panic" } + expect(@client.install_addon('example', 'addon1')). + to eq({ 'price' => 'free', 'message' => "Don't Panic" }) end it "upgrade_addon(app_name, addon_name) with response" do stub_request(:put, "https://api.heroku.com/apps/example/addons/addon1"). to_return(:body => json_encode('price' => 'free', 'message' => "Don't Panic")) - @client.upgrade_addon('example', 'addon1'). - should == { 'price' => 'free', 'message' => "Don't Panic" } + expect(@client.upgrade_addon('example', 'addon1')). + to eq({ 'price' => 'free', 'message' => "Don't Panic" }) end it "downgrade_addon(app_name, addon_name) with response" do stub_request(:put, "https://api.heroku.com/apps/example/addons/addon1"). to_return(:body => json_encode('price' => 'free', 'message' => "Don't Panic")) - @client.downgrade_addon('example', 'addon1'). - should == { 'price' => 'free', 'message' => "Don't Panic" } + expect(@client.downgrade_addon('example', 'addon1')). + to eq({ 'price' => 'free', 'message' => "Don't Panic" }) end it "uninstall_addon(app_name, addon_name) with response" do stub_api_request(:delete, "/apps/example/addons/addon1?"). to_return(:body => json_encode('price'=> 'free', 'message'=> "Don't Panic")) - @client.uninstall_addon('example', 'addon1'). - should == { 'price' => 'free', 'message' => "Don't Panic" } + expect(@client.uninstall_addon('example', 'addon1')). + to eq({ 'price' => 'free', 'message' => "Don't Panic" }) end end @@ -488,45 +488,45 @@ end it "creates a RestClient resource for making calls" do - @client.stub!(:host).and_return('heroku.com') - @client.stub!(:user).and_return('joe@example.com') - @client.stub!(:password).and_return('secret') + allow(@client).to receive(:host).and_return('heroku.com') + allow(@client).to receive(:user).and_return('joe@example.com') + allow(@client).to receive(:password).and_return('secret') res = @client.resource('/xyz') - res.url.should == 'https://api.heroku.com/xyz' - res.user.should == 'joe@example.com' - res.password.should == 'secret' + expect(res.url).to eq('https://api.heroku.com/xyz') + expect(res.user).to eq('joe@example.com') + expect(res.password).to eq('secret') end it "appends the api. prefix to the host" do @client.host = "heroku.com" - @client.resource('/xyz').url.should == 'https://api.heroku.com/xyz' + expect(@client.resource('/xyz').url).to eq('https://api.heroku.com/xyz') end it "doesn't add the api. prefix to full hosts" do @client.host = 'http://resource' res = @client.resource('/xyz') - res.url.should == 'http://resource/xyz' + expect(res.url).to eq('http://resource/xyz') end it "runs a callback when the API sets a warning header" do - response = mock('rest client response', :headers => { :x_heroku_warning => 'Warning' }) - @client.should_receive(:resource).and_return(@resource) - @resource.should_receive(:get).and_return(response) + response = double('rest client response', :headers => { :x_heroku_warning => 'Warning' }) + expect(@client).to receive(:resource).and_return(@resource) + expect(@resource).to receive(:get).and_return(response) @client.on_warning { |msg| @callback = msg } @client.get('test') - @callback.should == 'Warning' + expect(@callback).to eq('Warning') end it "doesn't run the callback twice for the same warning" do - response = mock('rest client response', :headers => { :x_heroku_warning => 'Warning' }) - @client.stub!(:resource).and_return(@resource) - @resource.stub!(:get).and_return(response) + response = double('rest client response', :headers => { :x_heroku_warning => 'Warning' }) + allow(@client).to receive(:resource).and_return(@resource) + allow(@resource).to receive(:get).and_return(response) @client.on_warning { |msg| @callback_called ||= 0; @callback_called += 1 } @client.get('test1') @client.get('test2') - @callback_called.should == 1 + expect(@callback_called).to eq(1) end end @@ -534,14 +534,14 @@ it "list_stacks(app_name) -> json hash of available stacks" do stub_api_request(:get, "/apps/example/stack?include_deprecated=false").to_return(:body => '{"stack":"one"}') capture_stderr do # capture deprecation message - @client.list_stacks("example").should == { 'stack' => 'one' } + expect(@client.list_stacks("example")).to eq({ 'stack' => 'one' }) end end it "list_stacks(app_name, include_deprecated=true) passes the deprecated option" do stub_api_request(:get, "/apps/example/stack?include_deprecated=true").to_return(:body => '{"stack":"one"}') capture_stderr do # capture deprecation message - @client.list_stacks("example", :include_deprecated => true).should == { 'stack' => 'one' } + expect(@client.list_stacks("example", :include_deprecated => true)).to eq({ 'stack' => 'one' }) end end end diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index 56dddb35f..21cfcd784 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -3,13 +3,15 @@ module Heroku::Command describe Addons do + include Support::Addons + let(:addon) { build_addon(name: "my_addon", app: { name: "example" }) } + before do @addons = prepare_command(Addons) stub_core.release("example", "current").returns( "name" => "v99" ) end - describe "index" do - + describe "#index" do before(:each) do stub_core api.post_app("name" => "example", "stack" => "cedar") @@ -20,223 +22,298 @@ module Heroku::Command end it "should display no addons when none are configured" do + Excon.stub(method: :get, path: %r(/apps/example/addons)) do + { body: "[]", status: 200 } + end + + Excon.stub(method: :get, path: %r(/apps/example/addon-attachments)) do + { body: "[]", status: 200 } + end + stderr, stdout = execute("addons") - stderr.should == "" - stdout.should == <<-STDOUT -example has no add-ons. + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== Resources for example +There are no add-ons. + +=== Attachments for example +There are no attachments. STDOUT + + Excon.stubs.shift(2) end it "should list addons and attachments" do - Excon.stub( - { - :expects => 200, - :method => :get, - :path => %r{^/apps/example/addons$} - }, - { - :body => Heroku::OkJson.encode([ - { 'configured' => false, 'name' => 'deployhooks:email' }, - { 'attachment_name' => 'HEROKU_POSTGRESQL_RED', 'configured' => true, 'name' => 'heroku-postgresql:ronin' }, - { 'configured' => true, 'name' => 'deployhooks:http' } - ]), - :status => 200, - } - ) - stderr, stdout = execute("addons") - stderr.should == "" - stdout.should == <<-STDOUT -=== example Configured Add-ons -deployhooks:http -heroku-postgresql:ronin HEROKU_POSTGRESQL_RED + Excon.stub(method: :get, path: %r(/apps/example/addons)) do + hooks = build_addon( + name: "swimming-nicely-42", + plan: { name: "deployhooks:http", price: { cents: 0, unit: "month" }}, + app: { name: "example" }) + + hpg = build_addon( + name: "jumping-slowly-76", + plan: { name: "heroku-postgresql:ronin", price: { cents: 20000, unit: "month" }}, + app: { name: "example" }) + + { body: MultiJson.encode([hooks, hpg]), status: 200 } + end -=== example Add-ons to Configure -deployhooks:email https://addons-sso.heroku.com/apps/example/addons/deployhooks:email + Excon.stub(method: :get, path: %(/apps/example/addon-attachments)) do + hpg = build_attachment( + name: "HEROKU_POSTGRESQL_CYAN", + addon: { name: "heroku-postgresql-12345", app: { name: "example" }}, + app: { name: "example" }) + { body: MultiJson.encode([hpg]), status: 200 } + end + + stderr, stdout = execute("addons") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== Resources for example +Plan Name Price +----------------------- ------------------ ------------- +deployhooks:http swimming-nicely-42 free +heroku-postgresql:ronin jumping-slowly-76 $200.00/month + +=== Attachments for example +Name Add-on Billing App +---------------------- ----------------------- ----------- +HEROKU_POSTGRESQL_CYAN heroku-postgresql-12345 example STDOUT - Excon.stubs.shift + Excon.stubs.shift(2) end end describe "list" do - - it "sends region option to the server" do - stub_request(:get, %r{/addons\?region=eu$}). - to_return(:body => Heroku::OkJson.encode([])) - execute("addons:list --region=eu") + before do + Excon.stub(method: :get, path: %r(/addon-services)) do + services = [ + { "name" => "cloudcounter:basic", "state" => "alpha" }, + { "name" => "cloudcounter:pro", "state" => "public" }, + { "name" => "cloudcounter:gold", "state" => "public" }, + { "name" => "cloudcounter:old", "state" => "disabled" }, + { "name" => "cloudcounter:platinum", "state" => "beta" } + ] + + { body: MultiJson.encode(services), status: 200 } + end end - it "lists available addons" do - stub_core.addons.returns([ - { "name" => "cloudcounter:basic", "state" => "alpha" }, - { "name" => "cloudcounter:pro", "state" => "public" }, - { "name" => "cloudcounter:gold", "state" => "public" }, - { "name" => "cloudcounter:old", "state" => "disabled" }, - { "name" => "cloudcounter:platinum", "state" => "beta" } - ]) - stderr, stdout = execute("addons:list") - stderr.should == "" - stdout.should == <<-STDOUT -=== alpha -cloudcounter:basic - -=== available -cloudcounter:gold, pro + after do + Excon.stubs.shift + end -=== beta -cloudcounter:platinum + # TODO: plugin code doesn't support this. Do we need it? + xit "sends region option to the server" do + stub_request(:get, %r{/addon-services\?region=eu$}). + to_return(:body => MultiJson.dump([])) + execute("addons:list --region=eu") + end -=== disabled -cloudcounter:old + describe "when using the deprecated `addons:list` command" do + it "displays a deprecation warning" do + stderr, stdout = execute("addons:list") + expect(stderr).to eq("") + expect(stdout).to include "WARNING: `heroku addons:list` has been deprecated. Please use `heroku addons:services` instead." + end + end -STDOUT + describe "when using correct `addons:services` command" do + it "displays all services" do + stderr, stdout = execute("addons:services") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Slug Name State +--------------------- ---- -------- +cloudcounter:basic alpha +cloudcounter:pro public +cloudcounter:gold public +cloudcounter:old disabled +cloudcounter:platinum beta + +See plans with `heroku addons:plans SERVICE` + STDOUT + end end end describe 'v1-style command line params' do - it "understands foo=baz" do - @addons.stub!(:args).and_return(%w(my_addon foo=baz)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) - @addons.add + before do + Excon.stub(method: :post, path: %r(/apps/example/addons)) do + { body: MultiJson.encode(addon), status: 201 } + end end - it "gives a deprecation notice with an example" do - stub_request(:post, %r{apps/example/addons/my_addon$}). - with(:body => {:config => {:foo => 'bar', :extra => "XXX"}}). - to_return(:body => Heroku::OkJson.encode({ 'price' => 'free' })) - Excon.stub( - { - :expects => 200, - :method => :get, - :path => %r{^/apps/example/releases/current} - }, - { - :body => Heroku::OkJson.encode({ 'name' => 'v99' }), - :status => 200, - } - ) - stderr, stdout = execute("addons:add my_addon --foo=bar extra=XXX") - stderr.should == "" - stdout.should == <<-STDOUT -Warning: non-unix style params have been deprecated, use --extra=XXX instead -Adding my_addon on example... done, v99 (free) -Use `heroku addons:docs my_addon` to view documentation. -STDOUT + after do Excon.stubs.shift end + + it "understands foo=baz" do + allow(@addons).to receive(:args).and_return(%w(my_addon foo=baz)) + + allow(@addons.api).to receive(:request) { |params| + expect(params[:body]).to include '"foo":"baz"' + }.and_return(double(body: stringify(addon))) + + @addons.create + end + + describe "addons:add" do + before do + Excon.stub(method: :get, path: %r{^/apps/example/releases/current}) do + { body: MultiJson.dump({ 'name' => 'v99' }), status: 200 } + end + + Excon.stub(method: :post, path: %r{apps/example/addons/my_addon$}) do + { body: MultiJson.encode(price: "free"), status: 200 } + end + end + + after do + Excon.stubs.shift(2) + end + + it "shows a deprecation warning about addon:add vs addons:create" do + stderr, stdout = execute("addons:add my_addon --foo=bar extra=XXX") + expect(stderr).to eq("") + expect(stdout).to include "WARNING: `heroku addons:add` has been deprecated. Please use `heroku addons:create` instead." + end + + it "shows a deprecation warning about non-unix params" do + stderr, stdout = execute("addons:add my_addon --foo=bar extra=XXX") + expect(stderr).to eq("") + expect(stdout).to include "Warning: non-unix style params have been deprecated, use --extra=XXX instead" + end + end end describe 'unix-style command line params' do it "understands --foo=baz" do - @addons.stub!(:args).and_return(%w(my_addon --foo=baz)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) - @addons.add + allow(@addons).to receive(:args).and_return(%w(my_addon --foo=baz)) + + allow(@addons).to receive(:request) { |args| + expect(args[:body]).to include '"foo":"baz"' + }.and_return(stringify(addon)) + + @addons.create end it "understands --foo baz" do - @addons.stub!(:args).and_return(%w(my_addon --foo baz)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) - @addons.add + allow(@addons).to receive(:args).and_return(%w(my_addon --foo baz)) + + expect(@addons).to receive(:request) { |args| + expect(args[:body]).to include '"foo":"baz"' + }.and_return(stringify(addon)) + + @addons.create end it "treats lone switches as true" do - @addons.stub!(:args).and_return(%w(my_addon --foo)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => true }) - @addons.add + allow(@addons).to receive(:args).and_return(%w(my_addon --foo)) + + expect(@addons).to receive(:request) { |args| + expect(args[:body]).to include '"foo":true' + }.and_return(stringify(addon)) + + @addons.create end it "converts 'true' to boolean" do - @addons.stub!(:args).and_return(%w(my_addon --foo=true)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => true }) - @addons.add + allow(@addons).to receive(:args).and_return(%w(my_addon --foo=true)) + + expect(@addons).to receive(:request) { |args| + expect(args[:body]).to include '"foo":true' + }.and_return(stringify(addon)) + + @addons.create end it "works with many config vars" do - @addons.stub!(:args).and_return(%w(my_addon --foo baz --bar yes --baz=foo --bab --bob=true)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz', 'bar' => 'yes', 'baz' => 'foo', 'bab' => true, 'bob' => true }) - @addons.add - end + allow(@addons).to receive(:args).and_return(%w(my_addon --foo baz --bar yes --baz=foo --bab --bob=true)) - it "sends the variables to the server" do - stub_request(:post, %r{apps/example/addons/my_addon$}). - with(:body => {:config => { 'foo' => 'baz', 'bar' => 'yes', 'baz' => 'foo', 'bab' => 'true', 'bob' => 'true' }}) - stderr, stdout = execute("addons:add my_addon --foo baz --bar yes --baz=foo --bab --bob=true") - stderr.should == "" + expect(@addons).to receive(:request) { |args| + expect(args[:body]).to include({ foo: 'baz', bar: 'yes', baz: 'foo', bab: true, bob: true }.to_json) + }.and_return(stringify(addon)) + + @addons.create end it "raises an error for spurious arguments" do - @addons.stub!(:args).and_return(%w(my_addon spurious)) - lambda { @addons.add }.should raise_error(CommandFailed) + allow(@addons).to receive(:args).and_return(%w(my_addon spurious)) + expect { @addons.create }.to raise_error(CommandFailed) end end describe "mixed options" do it "understands foo=bar and --baz=bar on the same line" do - @addons.stub!(:args).and_return(%w(my_addon foo=baz --baz=bar bob=true --bar)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz', 'baz' => 'bar', 'bar' => true, 'bob' => true }) - @addons.add + allow(@addons).to receive(:args).and_return(%w(my_addon foo=baz --baz=bar bob=true --bar)) + + expect(@addons).to receive(:request) { |args| + expect(args[:body]).to include '"foo":"baz"' + expect(args[:body]).to include '"baz":"bar"' + expect(args[:body]).to include '"bar":true' + expect(args[:body]).to include '"bob":true' + }.and_return(stringify(addon)) + + @addons.create end it "sends the variables to the server" do - stub_request(:post, %r{apps/example/addons/my_addon$}). - with(:body => {:config => { 'foo' => 'baz', 'baz' => 'bar', 'bar' => 'true', 'bob' => 'true' }}) + Excon.stub(method: :post, path: %r{/apps/example/addons$}) do + { body: MultiJson.encode(addon), status: 201 } + end + stderr, stdout = execute("addons:add my_addon foo=baz --baz=bar bob=true --bar") - stderr.should == "" - stdout.should include("Warning: non-unix style params have been deprecated, use --foo=baz --bob=true instead") + expect(stderr).to eq("") + expect(stdout).to include("Warning: non-unix style params have been deprecated, use --foo=baz --bob=true instead") + + Excon.stubs.shift end end describe "fork, follow, and rollback switches" do it "should only resolve for heroku-postgresql addon" do %w{fork follow rollback}.each do |switch| - @addons.stub!(:args).and_return("addon --#{switch} HEROKU_POSTGRESQL_RED".split) - @addons.heroku.should_receive(:install_addon). - with('example', 'addon', {switch => 'HEROKU_POSTGRESQL_RED'}) - @addons.add - end - end + allow(@addons).to receive(:args).and_return("addon --#{switch} HEROKU_POSTGRESQL_RED".split) - it "should translate --fork, --follow, and --rollback" do - %w{fork follow rollback}.each do |switch| - Heroku::Helpers::HerokuPostgresql::Resolver.any_instance.stub(:app_config_vars).and_return({}) - Heroku::Helpers::HerokuPostgresql::Resolver.any_instance.stub(:app_attachments).and_return([Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, - 'name' => 'HEROKU_POSTGRESQL_RED', - 'config_var' => 'HEROKU_POSTGRESQL_RED_URL', - 'resource' => {'name' => 'loudly-yelling-1232', - 'value' => 'postgres://red_url', - 'type' => 'heroku-postgresql:ronin' }}) - ]) - @addons.stub!(:args).and_return("heroku-postgresql --#{switch} HEROKU_POSTGRESQL_RED".split) - @addons.heroku.should_receive(:install_addon).with('example', 'heroku-postgresql:ronin', {switch => 'postgres://red_url'}) - @addons.add + allow(@addons).to receive(:request) { |args| + expect(args[:body]).to include %("#{switch}":"HEROKU_POSTGRESQL_RED") + }.and_return(stringify(addon)) + + @addons.create end end it "should NOT translate --fork and --follow if passed in a full postgres url even if there are no databases" do %w{fork follow}.each do |switch| - @addons.stub!(:app_config_vars).and_return({}) - @addons.stub!(:app_attachments).and_return([]) - @addons.stub!(:args).and_return("heroku-postgresql:ronin --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) - @addons.heroku.should_receive(:install_addon).with('example', 'heroku-postgresql:ronin', {switch => 'postgres://foo:yeah@awesome.com:234/bestdb'}) - @addons.add + allow(@addons).to receive(:app_config_vars).and_return({}) + allow(@addons).to receive(:app_attachments).and_return([]) + allow(@addons).to receive(:args).and_return("heroku-postgresql:ronin --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) + + allow(@addons).to receive(:request) { |args| + expect(args[:body]).to include %("#{switch}":"postgres://foo:yeah@awesome.com:234/bestdb") + }.and_return(stringify(addon)) + + @addons.create end end - it "should fail if fork / follow across applications and no plan is specified" do + # TODO: ? + xit "should fail if fork / follow across applications and no plan is specified" do %w{fork follow}.each do |switch| - @addons.stub!(:app_config_vars).and_return({}) - @addons.stub!(:app_attachments).and_return([]) - @addons.stub!(:args).and_return("heroku-postgresql --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) - lambda { @addons.add }.should raise_error(CommandFailed) + allow(@addons).to receive(:app_config_vars).and_return({}) + allow(@addons).to receive(:app_attachments).and_return([]) + allow(@addons).to receive(:args).and_return("heroku-postgresql --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) + expect { @addons.create }.to raise_error(CommandFailed) end end end describe 'adding' do before do - @addons.stub!(:args).and_return(%w(my_addon)) + allow(@addons).to receive(:args).and_return(%w(my_addon)) Excon.stub( { :expects => 200, @@ -244,77 +321,145 @@ module Heroku::Command :path => %r{^/apps/example/releases/current} }, { - :body => Heroku::OkJson.encode({ 'name' => 'v99' }), + :body => MultiJson.dump({ 'name' => 'v99' }), :status => 200, } ) end + after do Excon.stubs.shift end it "requires an addon name" do - @addons.stub!(:args).and_return([]) - lambda { @addons.add }.should raise_error(CommandFailed) + allow(@addons).to receive(:args).and_return([]) + expect { @addons.create }.to raise_error(CommandFailed) end it "adds an addon" do - @addons.stub!(:args).and_return(%w(my_addon)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', {}) - @addons.add + allow(@addons).to receive(:args).and_return(%w(my_addon)) + + allow(@addons).to receive(:request) { |args| + expect(args[:path]).to eq "/apps/example/addons" + expect(args[:body]).to include '"name":"my_addon"' + }.and_return(stringify(addon)) + + @addons.create + end + + it "expands hgp:s0 to heroku-postgresql:standard-0" do + allow(@addons).to receive(:args).and_return(%w(hpg:s0)) + + allow(@addons).to receive(:request) { |args| + expect(args[:path]).to eq "/apps/example/addons" + expect(args[:body]).to include '"name":"heroku-postgresql:standard-0"' + }.and_return(stringify(addon)) + + @addons.create end it "adds an addon with a price" do - stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "free" }) - stderr, stdout = execute("addons:add my_addon") - stderr.should == "" - stdout.should =~ /\(free\)/ + Excon.stub(method: :post, path: %r(/apps/example/addons)) do + addon = build_addon( + name: "my_addon", + addon_service: { name: "my_addon" }, + app: { name: "example" }) + + { body: MultiJson.encode(addon), status: 201 } + end + + stderr, stdout = execute("addons:create my_addon") + expect(stderr).to eq("") + expect(stdout).to match /Creating my_addon... done/ + + Excon.stubs.shift end it "adds an addon with a price and message" do - stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "free", "message" => "foo" }) - stderr, stdout = execute("addons:add my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT -Adding my_addon on example... done, v99 (free) -foo + Excon.stub(method: :post, path: %r(/apps/example/addons)) do + addon = build_addon( + name: "my_addon", + addon_service: { name: "my_addon" }, + app: { name: "example" } + ).merge(provision_message: "OMG A MESSAGE", plan: { price: { 'cents' => 1000, 'unit' => 'month' }}) + + { body: MultiJson.encode(addon), status: 201 } + end + + stderr, stdout = execute("addons:create my_addon") + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT +Creating my_addon... done, ($10.00/month) +Adding my_addon to example... done +OMG A MESSAGE Use `heroku addons:docs my_addon` to view documentation. OUTPUT + + Excon.stubs.shift end it "excludes addon plan from docs message" do - stub_core.install_addon("example", "my_addon:test", {}).returns({ "price" => "free", "message" => "foo" }) - stderr, stdout = execute("addons:add my_addon:test") - stderr.should == "" - stdout.should == <<-OUTPUT -Adding my_addon:test on example... done, v99 (free) -foo + Excon.stub(method: :post, path: %r(/apps/example/addons)) do + addon = build_addon( + name: "my_addon", + addon_service: { name: "my_addon" }, + app: { name: "example" }) + + { body: MultiJson.encode(addon), status: 201 } + end + + stderr, stdout = execute("addons:create my_addon:test") + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT +Creating my_addon... done, (free) +Adding my_addon to example... done Use `heroku addons:docs my_addon` to view documentation. OUTPUT + + Excon.stubs.shift end it "adds an addon with a price and multiline message" do + Excon.stub(method: :post, path: %r(/apps/example/addons)) do + addon = build_addon( + name: "my_addon", + addon_service: { name: "my_addon" }, + app: { name: "example" } + ).merge(provision_message: "foo\nbar") + + { body: MultiJson.encode(addon), status: 201 } + end + stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "$200/mo", "message" => "foo\nbar" }) - stderr, stdout = execute("addons:add my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT -Adding my_addon on example... done, v99 ($200/mo) + stderr, stdout = execute("addons:create my_addon") + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT +Creating my_addon... done, (free) +Adding my_addon to example... done foo bar Use `heroku addons:docs my_addon` to view documentation. OUTPUT + + Excon.stubs.shift end it "displays an error with unexpected options" do - Heroku::Command.should_receive(:error).with("Unexpected arguments: bar") + expect(Heroku::Command).to receive(:error).with("Unexpected arguments: bar", false) run("addons:add redistogo -a foo bar") end end describe 'upgrading' do + let(:addon) do + build_addon(name: "my_addon", + app: { name: "example" }, + plan: { name: "my_addon" }) + end + before do - @addons.stub!(:args).and_return(%w(my_addon)) + allow(@addons).to receive(:args).and_return(%w(my_addon)) Excon.stub( { :expects => 200, @@ -322,131 +467,188 @@ module Heroku::Command :path => %r{^/apps/example/releases/current} }, { - :body => Heroku::OkJson.encode({ 'name' => 'v99' }), + :body => MultiJson.dump({ 'name' => 'v99' }), :status => 200, } ) end + after do Excon.stubs.shift end it "requires an addon name" do - @addons.stub!(:args).and_return([]) - lambda { @addons.upgrade }.should raise_error(CommandFailed) + allow(@addons).to receive(:args).and_return([]) + expect { @addons.upgrade }.to raise_error(CommandFailed) end it "upgrades an addon" do - @addons.stub!(:args).and_return(%w(my_addon)) - @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', {}) + allow(@addons).to receive(:resolve_addon!).and_return(stringify(addon)) + allow(@addons).to receive(:args).and_return(%w(my_addon)) + + expect(@addons.api).to receive(:request) { |args| + expect(args[:method]).to eq :patch + expect(args[:path]).to eq "/apps/example/addons/my_addon" + }.and_return(OpenStruct.new(body: stringify(addon))) + @addons.upgrade end - it "upgrade an addon with config vars" do - @addons.stub!(:args).and_return(%w(my_addon --foo=baz)) - @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', { 'foo' => 'baz' }) + # TODO: need this? + xit "upgrade an addon with config vars" do + allow(@addons).to receive(:resolve_addon!).and_return(stringify(addon)) + allow(@addons).to receive(:args).and_return(%w(my_addon --foo=baz)) + expect(@addons.heroku).to receive(:upgrade_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.upgrade end - it "adds an addon with a price" do - stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free" }) - stderr, stdout = execute("addons:upgrade my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT -Upgrading to my_addon on example... done, v99 (free) -Use `heroku addons:docs my_addon` to view documentation. -OUTPUT - end + it "upgrades an addon with a price" do + my_addon = build_addon( + name: "my_addon", + plan: { name: "my_plan" }, + addon_service: { name: "my_service" }, + app: { name: "example" }, + price: { cents: 0, unit: "month" }) - it "adds an addon with a price and message" do - stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free", "message" => "Don't Panic" }) - stderr, stdout = execute("addons:upgrade my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT -Upgrading to my_addon on example... done, v99 (free) -Don't Panic -Use `heroku addons:docs my_addon` to view documentation. + Excon.stub(method: :get, path: %r(/apps/example/addons)) do + { body: MultiJson.encode([my_addon]), status: 200 } + end + + Excon.stub(method: :patch, path: %r(/apps/example/addons/my_addon)) do + { body: MultiJson.encode(my_addon), status: 200 } + end + + stderr, stdout = execute("addons:upgrade my_service") + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT +WARNING: No add-on name specified (see `heroku help addons:upgrade`) +Finding add-on from service my_service on app example... done +Found my_addon (my_plan) on example. +Changing my_addon plan to my_service... done, (free) OUTPUT + + Excon.stubs.shift(2) end end describe 'downgrading' do + let(:addon) do + build_addon( + name: "my_addon", + addon_service: { name: "my_service" }, + plan: { name: "my_plan" }, + app: { name: "example" }) + end + before do - @addons.stub!(:args).and_return(%w(my_addon)) Excon.stub( - { - :expects => 200, - :method => :get, - :path => %r{^/apps/example/releases/current} - }, - { - :body => Heroku::OkJson.encode({ 'name' => 'v99' }), - :status => 200, - } + { :expects => 200, :method => :get, :path => %r{^/apps/example/releases/current} }, + { :body => MultiJson.dump({ 'name' => 'v99' }), :status => 200, } ) end + after do Excon.stubs.shift end it "requires an addon name" do - @addons.stub!(:args).and_return([]) - lambda { @addons.downgrade }.should raise_error(CommandFailed) + allow(@addons).to receive(:args).and_return([]) + expect { @addons.downgrade }.to raise_error(CommandFailed) end it "downgrades an addon" do - @addons.stub!(:args).and_return(%w(my_addon)) - @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', {}) + allow(@addons).to receive(:args).and_return(%w(my_service low_plan)) + + allow(@addons.api).to receive(:request) { |args| + expect(args[:method]).to eq :patch + expect(args[:path]).to eq "/apps/example/addons/my_service" + }.and_return(OpenStruct.new(body: stringify(addon))) + @addons.downgrade end it "downgrade an addon with config vars" do - @addons.stub!(:args).and_return(%w(my_addon --foo=baz)) - @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', { 'foo' => 'baz' }) + allow(@addons).to receive(:args).and_return(%w(my_service --foo=baz)) + + allow(@addons.api).to receive(:request) { |args| + expect(args[:method]).to eq :patch + expect(args[:path]).to eq "/apps/example/addons/my_service" + }.and_return(OpenStruct.new(body: stringify(addon))) + @addons.downgrade end - it "downgrades an addon with a price" do - stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free" }) - stderr, stdout = execute("addons:downgrade my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT -Downgrading to my_addon on example... done, v99 (free) -Use `heroku addons:docs my_addon` to view documentation. -OUTPUT - end + describe "console output" do + before do + my_addon = build_addon( + name: "my_addon", + plan: { name: "my_plan" }, + addon_service: { name: "my_service" }, + app: { name: "example" }) - it "downgrades an addon with a price and message" do - stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free", "message" => "Don't Panic" }) - stderr, stdout = execute("addons:downgrade my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT -Downgrading to my_addon on example... done, v99 (free) -Don't Panic -Use `heroku addons:docs my_addon` to view documentation. + Excon.stub(method: :get, path: %r(/apps/example/addons)) do + { body: MultiJson.encode([my_addon]), status: 200 } + end + + Excon.stub(method: :patch, path: %r(/apps/example/addons/my_service)) do + { body: MultiJson.encode(my_addon), status: 200 } + end + end + + after do + Excon.stubs.shift(2) + end + + it "downgrades an addon with a price" do + stderr, stdout = execute("addons:downgrade my_service low_plan") + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT +Changing my_service plan to low_plan... done, (free) OUTPUT + end end end - it "does not remove addons with no confirm" do - @addons.stub!(:args).and_return(%w( addon1 )) - @addons.should_receive(:confirm_command).once.and_return(false) - @addons.heroku.should_not_receive(:uninstall_addon) - @addons.remove + it "does not destroy addons with no confirm" do + allow(@addons).to receive(:args).and_return(%w( addon1 )) + allow(@addons).to receive(:resolve_addon!).and_return({"app" => { "name" => "example" }}) + expect(@addons).to receive(:confirm_command).once.and_return(false) + expect(@addons.api).not_to receive(:request).with(hash_including(method: :delete)) + @addons.destroy end - it "removes addons after prompting for confirmation" do - @addons.stub!(:args).and_return(%w( addon1 )) - @addons.should_receive(:confirm_command).once.and_return(true) - @addons.heroku.should_receive(:uninstall_addon).with('example', 'addon1', :confirm => "example") - @addons.remove + it "destroys addons after prompting for confirmation" do + allow(@addons).to receive(:args).and_return(%w( addon1 )) + expect(@addons).to receive(:confirm_command).once.and_return(true) + allow(@addons).to receive(:get_attachments).and_return([]) + allow(@addons).to receive(:resolve_addon!).and_return({ + "id" => "abc123", + "config_vars" => [], + "app" => { "id" => "123", "name" => "example" } + }) + + allow(@addons.api).to receive(:request) { |args| + expect(args[:path]).to eq "/apps/123/addons/abc123" + }.and_return(OpenStruct.new(body: stringify(addon))) + + @addons.destroy end - it "removes addons with confirm option" do - Heroku::Command.stub!(:current_options).and_return(:confirm => "example") - @addons.stub!(:args).and_return(%w( addon1 )) - @addons.heroku.should_receive(:uninstall_addon).with('example', 'addon1', :confirm => "example") - @addons.remove + it "destroys addons with confirm option" do + allow(Heroku::Command).to receive(:current_options).and_return(:confirm => "example") + allow(@addons).to receive(:args).and_return(%w( addon1 )) + allow(@addons).to receive(:get_attachments).and_return([]) + allow(@addons).to receive(:resolve_addon!).and_return({ + "id" => "abc123", + "config_vars" => [], + "app" => { "id" => "123", "name" => "example" } + }) + + allow(@addons.api).to receive(:request) { |args| + expect(args[:path]).to eq "/apps/123/addons/abc123" + }.and_return(OpenStruct.new(body: stringify(addon))) + + @addons.destroy end describe "opening add-on docs" do @@ -454,6 +656,8 @@ module Heroku::Command before(:each) do stub_core api.post_app("name" => "example", "stack" => "cedar") + require "launchy" + allow(Launchy).to receive(:open) end after(:each) do @@ -462,76 +666,57 @@ module Heroku::Command it "displays usage when no argument is specified" do stderr, stdout = execute('addons:docs') - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku addons:docs ADDON ! Must specify ADDON to open docs for. STDERR - stdout.should == '' + expect(stdout).to eq('') end it "opens the addon if only one matches" do require("launchy") - Launchy.should_receive(:open).with("https://devcenter.heroku.com/articles/redistogo").and_return(Thread.new {}) + expect(Launchy).to receive(:open).with("https://devcenter.heroku.com/articles/redistogo").and_return(Thread.new {}) stderr, stdout = execute('addons:docs redistogo:nano') - stderr.should == '' - stdout.should == <<-STDOUT -Opening redistogo:nano docs... done + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT +Opening redistogo docs... done STDOUT end - it "complains about ambiguity" do - Excon.stub( - { - :expects => 200, - :method => :get, - :path => %r{^/addons$} - }, - { - :body => Heroku::OkJson.encode([ - { 'name' => 'qux:foo' }, - { 'name' => 'quux:bar' } - ]), - :status => 200, - } - ) - stderr, stdout = execute('addons:docs qu') - stderr.should == <<-STDERR - ! Ambiguous addon name: qu - ! Perhaps you meant `qux:foo` or `quux:bar`. -STDERR - stdout.should == '' - Excon.stubs.shift - end + it "complains when many_per_app" do + addon1 = stringify(addon.merge(name: "my_addon1", addon_service: { name: "my_service" })) + addon2 = stringify(addon.merge(name: "my_addon2", addon_service: { name: "my_service_2" })) + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([addon1, addon2]) - it "complains if no such addon exists" do - stderr, stdout = execute('addons:docs unknown') - stderr.should == <<-STDERR - ! `unknown` is not a heroku add-on. - ! See `heroku addons:list` for all available addons. + stderr, stdout = execute('addons:docs my_service') + expect(stdout).to eq('') + expect(stderr).to eq <<-STDERR + ! Multiple add-ons match "my_service". + ! Use the name of one of the add-on resources: + ! + ! - my_addon1 (my_service) + ! - my_addon2 (my_service_2) STDERR - stdout.should == '' end - it "suggests alternatives if addon has typo" do - stderr, stdout = execute('addons:docs redisgoto') - stderr.should == <<-STDERR - ! `redisgoto` is not a heroku add-on. - ! Perhaps you meant `redistogo`. - ! See `heroku addons:list` for all available addons. -STDERR - stdout.should == '' - end + it "optimistically opens the page if nothing matches" do + Excon.stub(method: :get, path: %r(/addons/unknown)) do + { status: 404 } + end - it "complains if addon is not installed" do - stderr, stdout = execute('addons:open deployhooks:http') - stderr.should == <<-STDOUT - ! Addon not installed: deployhooks:http -STDOUT - stdout.should == '' + Excon.stub(method: :get, path: %r(/apps/example/addons)) do + { body: "[]", status: 200 } + end + + expect(Launchy).to receive(:open).with("https://devcenter.heroku.com/articles/unknown").and_return(Thread.new {}) + stderr, stdout = execute('addons:docs unknown') + expect(stdout).to eq "Opening unknown docs... done\n" + + Excon.stubs.shift(2) end end - describe "opening an addon" do + describe "opening an addon" do before(:each) do stub_core api.post_app("name" => "example", "stack" => "cedar") @@ -543,74 +728,61 @@ module Heroku::Command it "displays usage when no argument is specified" do stderr, stdout = execute('addons:open') - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku addons:open ADDON ! Must specify ADDON to open. STDERR - stdout.should == '' + expect(stdout).to eq('') end it "opens the addon if only one matches" do - api.post_addon('example', 'redistogo:nano') + addon.merge!(addon_service: { name: "redistogo:nano" }) + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([stringify(addon)]) require("launchy") - Launchy.should_receive(:open).with("https://addons-sso.heroku.com/apps/example/addons/redistogo:nano").and_return(Thread.new {}) + expect(Launchy).to receive(:open).with("https://addons-sso.heroku.com/apps/example/addons/#{addon[:id]}").and_return(Thread.new {}) stderr, stdout = execute('addons:open redistogo:nano') - stderr.should == '' - stdout.should == <<-STDOUT -Opening redistogo:nano for example... done + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT +Opening redistogo:nano (my_addon) for example... done STDOUT end it "complains about ambiguity" do - Excon.stub( - { - :expects => 200, - :method => :get, - :path => %r{^/apps/example/addons$} - }, - { - :body => Heroku::OkJson.encode([ - { 'name' => 'deployhooks:email' }, - { 'name' => 'deployhooks:http' } - ]), - :status => 200, - } - ) + addon.merge!(addon_service: { name: "deployhooks:email" }) + email = stringify(addon.merge(name: "my_email", plan: { name: "email" })) + http = stringify(addon.merge(name: "my_http", plan: { name: "http" })) + + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([email, http]) + stderr, stdout = execute('addons:open deployhooks') - stderr.should == <<-STDERR - ! Ambiguous addon name: deployhooks - ! Perhaps you meant `deployhooks:email` or `deployhooks:http`. + expect(stderr).to eq <<-STDERR + ! Multiple add-ons match "deployhooks". + ! Use the name of add-on resource: + ! + ! - my_email (email) + ! - my_http (http) STDERR - stdout.should == '' - Excon.stubs.shift + expect(stdout).to eq('') end it "complains if no such addon exists" do + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([]) stderr, stdout = execute('addons:open unknown') - stderr.should == <<-STDERR - ! `unknown` is not a heroku add-on. - ! See `heroku addons:list` for all available addons. -STDERR - stdout.should == '' - end - - it "suggests alternatives if addon has typo" do - stderr, stdout = execute('addons:open redisgoto') - stderr.should == <<-STDERR - ! `redisgoto` is not a heroku add-on. - ! Perhaps you meant `redistogo`. - ! See `heroku addons:list` for all available addons. + expect(stderr).to eq <<-STDERR + ! Can not find add-on with "unknown" STDERR - stdout.should == '' + expect(stdout).to eq('') end it "complains if addon is not installed" do + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([]) stderr, stdout = execute('addons:open deployhooks:http') - stderr.should == <<-STDOUT - ! Addon not installed: deployhooks:http + expect(stderr).to eq <<-STDOUT + ! Can not find add-on with "deployhooks:http" STDOUT - stdout.should == '' + expect(stdout).to eq('') end end + end end diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index ac8f5689e..8253012df 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -19,38 +19,38 @@ module Heroku::Command api.delete_app("example") end - it "displays impicit app info" do + it "displays implicit app info" do stderr, stdout = execute("apps:info") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example -Git URL: git@heroku.com:example.git +Git URL: https://git.heroku.com/example.git Owner Email: email@example.com -Stack: cedar +Stack: cedar-10 Web URL: http://example.herokuapp.com/ STDOUT end it "gets explicit app from --app" do stderr, stdout = execute("apps:info --app example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example -Git URL: git@heroku.com:example.git +Git URL: https://git.heroku.com/example.git Owner Email: email@example.com -Stack: cedar +Stack: cedar-10 Web URL: http://example.herokuapp.com/ STDOUT end it "shows shell app info when --shell option is used" do stderr, stdout = execute("apps:info --shell") - stderr.should == "" - stdout.should match Regexp.new(<<-STDOUT) + expect(stderr).to eq("") + expect(stdout).to match Regexp.new(<<-STDOUT) create_status=complete created_at=\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4} dynos=0 -git_url=git@heroku.com:example.git +git_url=https://git.heroku.com/example.git id=\\d{1,5} name=example owner_email=email@example.com @@ -73,10 +73,10 @@ module Heroku::Command with_blank_git_repository do stderr, stdout = execute("apps:create") name = api.get_apps.body.first["name"] - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating #{name}... done, stack is bamboo-mri-1.9.2 -http://#{name}.herokuapp.com/ | git@heroku.com:#{name}.git +http://#{name}.herokuapp.com/ | https://git.heroku.com/#{name}.git Git remote heroku added STDOUT end @@ -86,10 +86,10 @@ module Heroku::Command it "with a name" do with_blank_git_repository do stderr, stdout = execute("apps:create example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git Git remote heroku added STDOUT end @@ -99,10 +99,10 @@ module Heroku::Command it "with -a name" do with_blank_git_repository do stderr, stdout = execute("apps:create -a example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git Git remote heroku added STDOUT end @@ -112,10 +112,10 @@ module Heroku::Command it "with --no-remote" do with_blank_git_repository do stderr, stdout = execute("apps:create example --no-remote") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git STDOUT end api.delete_app("example") @@ -123,13 +123,12 @@ module Heroku::Command it "with addons" do with_blank_git_repository do - stderr, stdout = execute("apps:create addonapp --addon custom_domains:basic,releases:basic") - stderr.should == "" - stdout.should == <<-STDOUT + stderr, stdout = execute("apps:create addonapp --addon pgbackups:auto-month") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating addonapp... done, stack is bamboo-mri-1.9.2 -Adding custom_domains:basic to addonapp... done -Adding releases:basic to addonapp... done -http://addonapp.herokuapp.com/ | git@heroku.com:addonapp.git +Adding pgbackups:auto-month to addonapp... done +http://addonapp.herokuapp.com/ | https://git.heroku.com/addonapp.git Git remote heroku added STDOUT end @@ -137,13 +136,14 @@ module Heroku::Command end it "with a buildpack" do + Excon.stub({:method => :put, :path => "/apps/buildpackapp/buildpack-installations"}, {:status => 200}) with_blank_git_repository do stderr, stdout = execute("apps:create buildpackapp --buildpack http://example.org/buildpack.git") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating buildpackapp... done, stack is bamboo-mri-1.9.2 -BUILDPACK_URL=http://example.org/buildpack.git -http://buildpackapp.herokuapp.com/ | git@heroku.com:buildpackapp.git +Buildpack set. Next release on buildpackapp will use http://example.org/buildpack.git. +http://buildpackapp.herokuapp.com/ | https://git.heroku.com/buildpackapp.git Git remote heroku added STDOUT end @@ -153,10 +153,10 @@ module Heroku::Command it "with an alternate remote name" do with_blank_git_repository do stderr, stdout = execute("apps:create alternate-remote --remote alternate") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating alternate-remote... done, stack is bamboo-mri-1.9.2 -http://alternate-remote.herokuapp.com/ | git@heroku.com:alternate-remote.git +http://alternate-remote.herokuapp.com/ | https://git.heroku.com/alternate-remote.git Git remote alternate added STDOUT end @@ -178,8 +178,8 @@ module Heroku::Command it "succeeds" do stub_core.list.returns([["example", "user"]]) stderr, stdout = execute("apps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === My Apps example @@ -190,21 +190,11 @@ module Heroku::Command context("index with orgs") do context("when you are a member of the org") do - before(:each) do - Excon.stub({ :method => :get, :path => '/v1/user/info' }, { :status => 200, :body => Heroku::OkJson.encode({ - "user" => {"default_organization" => "test-org"} - })}) - end - - after(:each) do - Excon.stub({ :method => :get, :path => '/v1/user/info' }, { :status => 404 }) - end - it "displays a message when the org has no apps" do - Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }, { :status => 200, :body => Heroku::OkJson.encode([]) }) - stderr, stdout = execute("apps") - stderr.should == "" - stdout.should == <<-STDOUT + Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }, { :status => 200, :body => MultiJson.dump([]) }) + stderr, stdout = execute("apps -o test-org") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT There are no apps in organization test-org. STDOUT @@ -214,7 +204,7 @@ module Heroku::Command before(:each) do Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }, { - :body => Heroku::OkJson.encode([ + :body => MultiJson.dump([ {"name" => "org-app-1", "joined" => true}, {"name" => "org-app-2"} ]), @@ -224,9 +214,9 @@ module Heroku::Command end it "lists joined apps in an organization" do - stderr, stdout = execute("apps") - stderr.should == "" - stdout.should == <<-STDOUT + stderr, stdout = execute("apps -o test-org") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === Apps joined in organization test-org org-app-1 @@ -234,9 +224,9 @@ module Heroku::Command end it "list all apps in an organization with the --all flag" do - stderr, stdout = execute("apps --all") - stderr.should == "" - stdout.should == <<-STDOUT + stderr, stdout = execute("apps --all -o test-org") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === Apps joined in organization test-org org-app-1 @@ -264,10 +254,10 @@ module Heroku::Command it "renames app" do with_blank_git_repository do stderr, stdout = execute("apps:rename example2") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Renaming example to example2... done -http://example2.herokuapp.com/ | git@heroku.com:example2.git +http://example2.herokuapp.com/ | https://git.heroku.com/example2.git Don't forget to update your Git remotes on any local checkouts. STDOUT end @@ -277,11 +267,11 @@ module Heroku::Command it "displays an error if no name is specified" do stderr, stdout = execute("apps:rename") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku apps:rename NEWNAME ! Must specify NEWNAME to rename. STDERR - stdout.should == "" + expect(stdout).to eq("") end end @@ -294,8 +284,8 @@ module Heroku::Command it "succeeds with app explicitly specified with --app and user confirmation" do stderr, stdout = execute("apps:destroy --confirm example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Destroying example (including all add-ons)... done STDOUT end @@ -308,25 +298,25 @@ module Heroku::Command it "fails with explicit app but no confirmation" do stderr, stdout = execute("apps:destroy example") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Confirmation did not match example. Aborted. STDERR - stdout.should == " + expect(stdout).to eq(" ! WARNING: Potentially Destructive Action ! This command will destroy example (including all add-ons). ! To proceed, type \"example\" or re-run this command with --confirm example -> " +> ") end it "fails without explicit app" do stderr, stdout = execute("apps:destroy") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku apps:destroy --app APP ! Must specify APP to destroy. STDERR - stdout.should == "" + expect(stdout).to eq("") end end @@ -338,13 +328,13 @@ module Heroku::Command it "creates adding heroku to git remote" do with_blank_git_repository do stderr, stdout = execute("apps:create example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git Git remote heroku added STDOUT - `git remote`.strip.should match(/^heroku$/) + expect(`git remote`.strip).to match(/^heroku$/) api.delete_app("example") end end @@ -352,13 +342,13 @@ module Heroku::Command it "creates adding a custom git remote" do with_blank_git_repository do stderr, stdout = execute("apps:create example --remote myremote") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git Git remote myremote added STDOUT - `git remote`.strip.should match(/^myremote$/) + expect(`git remote`.strip).to match(/^myremote$/) api.delete_app("example") end end @@ -367,10 +357,10 @@ module Heroku::Command with_blank_git_repository do `git remote add heroku /tmp/git_spec_#{Process.pid}` stderr, stdout = execute("apps:create example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git STDOUT api.delete_app("example") end @@ -379,33 +369,33 @@ module Heroku::Command it "renames updating the corresponding heroku git remote" do with_blank_git_repository do `git remote add github git@github.com:test/test.git` - `git remote add production git@heroku.com:example.git` - `git remote add staging git@heroku.com:example-staging.git` + `git remote add production https://git.heroku.com/example.git` + `git remote add staging https://git.heroku.com/example-staging.git` api.post_app("name" => "example", "stack" => "cedar") stderr, stdout = execute("apps:rename example2") api.delete_app("example2") remotes = `git remote -v` - remotes.should == <<-REMOTES + expect(remotes).to eq <<-REMOTES github\tgit@github.com:test/test.git (fetch) github\tgit@github.com:test/test.git (push) -production\tgit@heroku.com:example2.git (fetch) -production\tgit@heroku.com:example2.git (push) -staging\tgit@heroku.com:example-staging.git (fetch) -staging\tgit@heroku.com:example-staging.git (push) +production\thttps://git.heroku.com/example2.git (fetch) +production\thttps://git.heroku.com/example2.git (push) +staging\thttps://git.heroku.com/example-staging.git (fetch) +staging\thttps://git.heroku.com/example-staging.git (push) REMOTES end end it "destroys removing any remotes pointing to the app" do with_blank_git_repository do - `git remote add heroku git@heroku.com:example.git` + `git remote add heroku https://git.heroku.com/example.git` api.post_app("name" => "example", "stack" => "cedar") stderr, stdout = execute("apps:destroy --confirm example") - `git remote`.strip.should_not include('heroku') + expect(`git remote`.strip).not_to include('heroku') end end end diff --git a/spec/heroku/command/auth_spec.rb b/spec/heroku/command/auth_spec.rb index c58eb0412..b8ff58807 100644 --- a/spec/heroku/command/auth_spec.rb +++ b/spec/heroku/command/auth_spec.rb @@ -6,10 +6,10 @@ it "displays heroku help auth" do stderr, stdout = execute("auth") - stderr.should == "" - stdout.should include "Additional commands" - stdout.should include "auth:login" - stdout.should include "auth:logout" + expect(stderr).to eq("") + expect(stdout).to include "Additional commands" + expect(stdout).to include "auth:login" + expect(stdout).to include "auth:logout" end end @@ -17,22 +17,10 @@ it "displays the user's api key" do stderr, stdout = execute("auth:token") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT apikey01 STDOUT end end - - describe "auth:whoami" do - it "displays the user's email address" do - stderr, stdout = execute("auth:whoami") - stderr.should == "" - stdout.should == <<-STDOUT -email@example.com -STDOUT - end - - end - end diff --git a/spec/heroku/command/base_spec.rb b/spec/heroku/command/base_spec.rb index acbd3af94..779564558 100644 --- a/spec/heroku/command/base_spec.rb +++ b/spec/heroku/command/base_spec.rb @@ -5,37 +5,37 @@ module Heroku::Command describe Base do before do @base = Base.new - @base.stub!(:display) - @client = mock('heroku client', :host => 'heroku.com') + allow(@base).to receive(:display) + @client = double('heroku client', :host => 'heroku.com') end describe "confirming" do it "confirms the app via --confirm" do - Heroku::Command.stub(:current_options).and_return(:confirm => "example") - @base.stub(:app).and_return("example") - @base.confirm_command.should be_true + allow(Heroku::Command).to receive(:current_options).and_return(:confirm => "example") + allow(@base).to receive(:app).and_return("example") + expect(@base.confirm_command).to be_truthy end it "does not confirms the app via --confirm on a mismatch" do - Heroku::Command.stub(:current_options).and_return(:confirm => "badapp") - @base.stub(:app).and_return("example") - lambda { @base.confirm_command}.should raise_error CommandFailed + allow(Heroku::Command).to receive(:current_options).and_return(:confirm => "badapp") + allow(@base).to receive(:app).and_return("example") + expect { @base.confirm_command}.to raise_error CommandFailed end it "confirms the app interactively via ask" do - @base.stub(:app).and_return("example") - @base.stub(:ask).and_return("example") - Heroku::Command.stub(:current_options).and_return({}) - @base.confirm_command.should be_true + allow(@base).to receive(:app).and_return("example") + allow(@base).to receive(:ask).and_return("example") + allow(Heroku::Command).to receive(:current_options).and_return({}) + expect(@base.confirm_command).to be_truthy end it "fails if the interactive confirm doesn't match" do - @base.stub(:app).and_return("example") - @base.stub(:ask).and_return("badresponse") - Heroku::Command.stub(:current_options).and_return({}) - capture_stderr do - lambda { @base.confirm_command }.should raise_error(SystemExit) - end.should == <<-STDERR + allow(@base).to receive(:app).and_return("example") + allow(@base).to receive(:ask).and_return("badresponse") + allow(Heroku::Command).to receive(:current_options).and_return({}) + expect(capture_stderr do + expect { @base.confirm_command }.to raise_error(SystemExit) + end).to eq <<-STDERR ! Confirmation did not match example. Aborted. STDERR end @@ -43,65 +43,65 @@ module Heroku::Command context "detecting the app" do it "attempts to find the app via the --app option" do - @base.stub!(:options).and_return(:app => "example") - @base.app.should == "example" + allow(@base).to receive(:options).and_return(:app => "example") + expect(@base.app).to eq("example") end it "attempts to find the app via the --confirm option" do - @base.stub!(:options).and_return(:confirm => "myconfirmapp") - @base.app.should == "myconfirmapp" + allow(@base).to receive(:options).and_return(:confirm => "myconfirmapp") + expect(@base.app).to eq("myconfirmapp") end it "attempts to find the app via HEROKU_APP when not explicitly specified" do ENV['HEROKU_APP'] = "myenvapp" - @base.app.should == "myenvapp" - @base.stub!(:options).and_return([]) - @base.app.should == "myenvapp" + expect(@base.app).to eq("myenvapp") + allow(@base).to receive(:options).and_return([]) + expect(@base.app).to eq("myenvapp") ENV.delete('HEROKU_APP') end it "overrides HEROKU_APP when explicitly specified" do ENV['HEROKU_APP'] = "myenvapp" - @base.stub!(:options).and_return(:app => "example") - @base.app.should == "example" + allow(@base).to receive(:options).and_return(:app => "example") + expect(@base.app).to eq("example") ENV.delete('HEROKU_APP') end it "read remotes from git config" do - Dir.stub(:chdir) - File.should_receive(:exists?).with(".git").and_return(true) - @base.should_receive(:git).with('remote -v').and_return(<<-REMOTES) -staging\tgit@heroku.com:example-staging.git (fetch) -staging\tgit@heroku.com:example-staging.git (push) -production\tgit@heroku.com:example.git (fetch) -production\tgit@heroku.com:example.git (push) + allow(Dir).to receive(:chdir) + expect(File).to receive(:exists?).with(".git").and_return(true) + expect(@base).to receive(:git).with('remote -v').and_return(<<-REMOTES) +staging\thttps://git.heroku.com/example-staging.git (fetch) +staging\thttps://git.heroku.com/example-staging.git (push) +production\thttps://git.heroku.com/example.git (fetch) +production\thttps://git.heroku.com/example.git (push) other\tgit@other.com:other.git (fetch) other\tgit@other.com:other.git (push) REMOTES - @heroku = mock - @heroku.stub(:host).and_return('heroku.com') - @base.stub(:heroku).and_return(@heroku) + @heroku = double + allow(@heroku).to receive(:host).and_return('heroku.com') + allow(@base).to receive(:heroku).and_return(@heroku) # need a better way to test internal functionality - @base.send(:git_remotes, '/home/dev/example').should == { 'staging' => 'example-staging', 'production' => 'example' } + expect(@base.send(:git_remotes, '/home/dev/example')).to eq({ 'staging' => 'example-staging', 'production' => 'example' }) end it "gets the app from remotes when there's only one app" do - @base.stub!(:git_remotes).and_return({ 'heroku' => 'example' }) - @base.stub!(:git).with("config heroku.remote").and_return("") - @base.app.should == 'example' + allow(@base).to receive(:git_remotes).and_return({ 'heroku' => 'example' }) + allow(@base).to receive(:git).with("config heroku.remote").and_return("") + expect(@base.app).to eq('example') end it "accepts a --remote argument to choose the app from the remote name" do - @base.stub!(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) - @base.stub!(:options).and_return(:remote => "staging") - @base.app.should == 'example-staging' + allow(@base).to receive(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) + allow(@base).to receive(:options).and_return(:remote => "staging") + expect(@base.app).to eq('example-staging') end it "raises when cannot determine which app is it" do - @base.stub!(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) - lambda { @base.app }.should raise_error(Heroku::Command::CommandFailed) + allow(@base).to receive(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) + expect { @base.app }.to raise_error(Heroku::Command::CommandFailed) end end diff --git a/spec/heroku/command/buildpacks_spec.rb b/spec/heroku/command/buildpacks_spec.rb new file mode 100644 index 000000000..3c202fda1 --- /dev/null +++ b/spec/heroku/command/buildpacks_spec.rb @@ -0,0 +1,562 @@ +require "spec_helper" +require "heroku/command/buildpacks" + +module Heroku::Command + describe Buildpacks do + + def stub_put(*buildpacks) + Excon.stub({ + :method => :put, + :path => "/apps/example/buildpack-installations", + :body => {"updates" => buildpacks.map{|bp| {"buildpack" => bp}}}.to_json + }, + {:status => 200}) + end + + def stub_get(*buildpacks) + Excon.stub({:method => :get, :path => "/apps/example/buildpack-installations"}, + { + :body => buildpacks.map.with_index { |bp, i| + { + "buildpack" => { + "url" => bp + }, + "ordinal" => i + } + }, + :status => 200 + }) + end + + before(:each) do + stub_core + api.post_app("name" => "example", "stack" => "cedar-14") + + Excon.stub({:method => :put, :path => "/apps/example/buildpack-installations"}, + {:status => 200}) + stub_get("https://github.com/heroku/heroku-buildpack-ruby") + end + + after(:each) do + Excon.stubs.shift + Excon.stubs.shift + api.delete_app("example") + end + + describe "index" do + it "displays the buildpack URL" do + stderr, stdout = execute("buildpacks") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Buildpack URL +https://github.com/heroku/heroku-buildpack-ruby + STDOUT + end + + context "with no buildpack URL set" do + before(:each) do + Excon.stubs.shift + stub_get + end + + it "does not display a buildpack URL" do + stderr, stdout = execute("buildpacks") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +example has no Buildpack URL set. + STDOUT + end + end + + context "with two buildpack URLs set" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-ruby") + end + + it "does not display a buildpack URL" do + stderr, stdout = execute("buildpacks") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Buildpack URLs +1. https://github.com/heroku/heroku-buildpack-java +2. https://github.com/heroku/heroku-buildpack-ruby + STDOUT + end + end + end + + describe "set" do + context "with no buildpacks" do + before do + Excon.stubs.shift + stub_get + end + + it "sets the buildpack URL" do + stderr, stdout = execute("buildpacks:set https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + + it "handles a missing buildpack URL arg" do + stderr, stdout = execute("buildpacks:set") + expect(stderr).to eq <<-STDERR + ! Usage: heroku buildpacks:set BUILDPACK_URL. + ! Must specify target buildpack URL. + STDERR + expect(stdout).to eq("") + end + + it "sets the buildpack URL with index" do + stderr, stdout = execute("buildpacks:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + end + + context "with one existing buildpack" do + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java") + end + + it "overwrites an existing buildpack URL at index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + end + + context "unsuccessfully" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-ruby") + end + + it "fails if buildpack is already set" do + stderr, stdout = execute("buildpacks:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! The buildpack https://github.com/heroku/heroku-buildpack-ruby is already set on your app. + STDOUT + end + end + end + + context "with two existing buildpacks" do + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end + + it "overwrites an existing buildpack URL at index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-ruby", + "https://github.com/heroku/heroku-buildpack-nodejs") + stderr, stdout = execute("buildpacks:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack set. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-ruby + 2. https://github.com/heroku/heroku-buildpack-nodejs +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + + it "adds buildpack URL to the end of list" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs", + "https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:set -i 99 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack set. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-nodejs + 3. https://github.com/heroku/heroku-buildpack-ruby +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + end + + context "unsuccessfully" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end + + it "fails if buildpack is already set" do + stderr, stdout = execute("buildpacks:set -i 2 https://github.com/heroku/heroku-buildpack-java") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! The buildpack https://github.com/heroku/heroku-buildpack-java is already set on your app. + STDOUT + end + end + end + end + + describe "add" do + context "with no buildpacks" do + before(:each) do + Excon.stubs.shift + stub_get + end + + it "adds the buildpack URL" do + stderr, stdout = execute("buildpacks:add https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + + it "handles a missing buildpack URL arg" do + stderr, stdout = execute("buildpacks:add") + expect(stderr).to eq <<-STDERR + ! Usage: heroku buildpacks:add BUILDPACK_URL. + ! Must specify target buildpack URL. + STDERR + expect(stdout).to eq("") + end + + it "adds the buildpack URL with index" do + stderr, stdout = execute("buildpacks:add -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + end + + context "with one existing buildpack" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java") + end + + it "inserts a buildpack URL at index" do + stub_put("https://github.com/heroku/heroku-buildpack-ruby", "https://github.com/heroku/heroku-buildpack-java") + stderr, stdout = execute("buildpacks:add -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-ruby + 2. https://github.com/heroku/heroku-buildpack-java +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + + it "adds a buildpack URL to the end of the list" do + stub_put("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:add https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-ruby +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + end + + context "with two existing buildpacks" do + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end + + it "inserts a buildpack URL at index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-ruby", + "https://github.com/heroku/heroku-buildpack-nodejs") + stderr, stdout = execute("buildpacks:add -i 2 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-ruby + 3. https://github.com/heroku/heroku-buildpack-nodejs +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + + it "adds a buildpack URL to the end of the list" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs", + "https://github.com/heroku/heroku-buildpack-ruby") + stub_put("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + stderr, stdout = execute("buildpacks:add https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-nodejs + 3. https://github.com/heroku/heroku-buildpack-ruby +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + end + + context "successfully" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end + + it "inserts a buildpack URL at index" do + stderr, stdout = execute("buildpacks:add https://github.com/heroku/heroku-buildpack-java") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! The buildpack https://github.com/heroku/heroku-buildpack-java is already set on your app. + STDOUT + end + end + end + end + + describe "clear" do + it "clears the buildpack URL" do + stderr, stdout = execute("buildpacks:clear") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpacks cleared. Next release on example will detect buildpack normally. + STDOUT + end + + it "clears and warns about buildpack URL config var" do + execute("config:set BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:clear") + expect(stderr).to eq <<-STDERR +WARNING: The BUILDPACK_URL config var is still set and will be used for the next release + STDERR + expect(stdout).to eq <<-STDOUT +Buildpacks cleared. + STDOUT + end + + it "clears and warns about language pack URL config var" do + execute("config:set LANGUAGE_PACK_URL=https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:clear") + expect(stderr).to eq <<-STDERR +WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release + STDERR + expect(stdout).to eq <<-STDOUT +Buildpacks cleared. + STDOUT + end + end + + describe "remove" do + context "with no buildpacks" do + before(:each) do + Excon.stubs.shift + stub_get + end + + it "reports an error removing index" do + stderr, stdout = execute("buildpacks:remove -i 1") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! No buildpacks were found. Next release on example will detect buildpack normally. + STDOUT + end + + it "reports an error removing buildpack_url" do + stderr, stdout = execute("buildpacks:remove https://github.com/heroku/heroku-buildpack-ruby") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! No buildpacks were found. Next release on example will detect buildpack normally. + STDOUT + end + end + + context "with one buildpack" do + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-ruby") + stub_put + end + + it "removes index" do + stderr, stdout = execute("buildpacks:remove -i 1") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will detect buildpack normally. + STDOUT + end + + it "removes url" do + stderr, stdout = execute("buildpacks:remove https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will detect buildpack normally. + STDOUT + end + end + + context "unsuccessfully" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java") + end + + it "validates arguments" do + stderr, stdout = execute("buildpacks:remove -i 1 https://github.com/heroku/heroku-buildpack-java") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! Please choose either index or Buildpack URL, but not both. + STDOUT + end + + it "checks if index is in range" do + stderr, stdout = execute("buildpacks:remove -i 9") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! Invalid index. Only valid value is 1. + STDOUT + end + + it "checks if buildpack_url is found" do + stderr, stdout = execute("buildpacks:remove https://github.com/heroku/heroku-buildpack-foobar") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! Buildpack not found. Nothing was removed. + STDOUT + end + end + end + + context "with two buildpacks" do + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-ruby") + end + + it "removes index" do + stub_put("https://github.com/heroku/heroku-buildpack-java") + stderr, stdout = execute("buildpacks:remove -i 2") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will use https://github.com/heroku/heroku-buildpack-java. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + + it "removes index" do + stub_put("https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:remove -i 1") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + + it "removes url" do + stub_put("https://github.com/heroku/heroku-buildpack-java") + stderr, stdout = execute("buildpacks:remove https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will use https://github.com/heroku/heroku-buildpack-java. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + end + + context "unsuccessfully" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end + + it "checks if index is in range" do + stderr, stdout = execute("buildpacks:remove -i 9") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! Invalid index. Please choose a value between 1 and 2 + STDOUT + end + + it "checks if index or url is provided" do + stderr, stdout = execute("buildpacks:remove") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! Usage: heroku buildpacks:remove [BUILDPACK_URL]. + ! Must specify a buildpack to remove, either by index or URL. + STDOUT + end + end + end + + context "with three buildpacks" do + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs", + "https://github.com/heroku/heroku-buildpack-ruby") + end + + it "removes index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:remove -i 2") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-ruby +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + + it "removes url" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs") + stderr, stdout = execute("buildpacks:remove https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-nodejs +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + end + end + end + end +end diff --git a/spec/heroku/command/certs_spec.rb b/spec/heroku/command/certs_spec.rb index e1801f52b..a5c36fca8 100644 --- a/spec/heroku/command/certs_spec.rb +++ b/spec/heroku/command/certs_spec.rb @@ -43,7 +43,7 @@ module Heroku::Command it "shows a list of certs" do stub_core.ssl_endpoint_list("example").returns([endpoint, endpoint2]) stderr, stdout = execute("certs") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Endpoint Common Name(s) Expires Trusted ------------------------ -------------- -------------------- ------- tokyo-1050.herokussl.com example.org 2013-08-01 21:34 UTC False @@ -54,7 +54,7 @@ module Heroku::Command it "warns about no SSL Endpoints if the app has no certs" do stub_core.ssl_endpoint_list("example").returns([]) stderr, stdout = execute("certs") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT example has no SSL Endpoints. Use `heroku certs:add CRT KEY` to add one. STDOUT @@ -63,12 +63,12 @@ module Heroku::Command describe "certs:add" do it "adds an endpoint" do - File.should_receive(:read).with("pem_file").and_return("pem content") - File.should_receive(:read).with("key_file").and_return("key content") + expect(File).to receive(:read).with("pem_file").and_return("pem content") + expect(File).to receive(:read).with("key_file").and_return("key content") stub_core.ssl_endpoint_add('example', 'pem content', 'key content').returns(endpoint) stderr, stdout = execute("certs:add --bypass pem_file key_file") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Adding SSL Endpoint to example... done example now served by tokyo-1050.herokussl.com Certificate details: @@ -77,7 +77,7 @@ module Heroku::Command end it "shows usage if two arguments are not provided" do - lambda { execute("certs:add --bypass") }.should raise_error(CommandFailed, /Usage:/) + expect { execute("certs:add --bypass") }.to raise_error(CommandFailed, /Usage:/) end end @@ -87,7 +87,7 @@ module Heroku::Command stub_core.ssl_endpoint_info('example', 'tokyo-1050.herokussl.com').returns(endpoint) stderr, stdout = execute("certs:info") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Fetching SSL Endpoint tokyo-1050.herokussl.com info for example... done Certificate details: #{certificate_details} @@ -98,7 +98,7 @@ module Heroku::Command stub_core.ssl_endpoint_info('example', 'tokyo-1050').returns(endpoint) stderr, stdout = execute("certs:info --endpoint tokyo-1050") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Fetching SSL Endpoint tokyo-1050 info for example... done Certificate details: #{certificate_details} @@ -109,7 +109,7 @@ module Heroku::Command stub_core.ssl_endpoint_list("example").returns([]) stderr, stdout = execute("certs:info") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! example has no SSL Endpoints. STDERR end @@ -121,31 +121,31 @@ module Heroku::Command stub_core.ssl_endpoint_remove('example', 'tokyo-1050.herokussl.com').returns(endpoint) stderr, stdout = execute("certs:remove --confirm example") - stdout.should include "Removing SSL Endpoint tokyo-1050.herokussl.com from example..." - stdout.should include "NOTE: Billing is still active. Remove SSL Endpoint add-on to stop billing." + expect(stdout).to include "Removing SSL Endpoint tokyo-1050.herokussl.com from example..." + expect(stdout).to include "NOTE: Billing is still active. Remove SSL Endpoint add-on to stop billing." end it "allows an endpoint to be specified" do stub_core.ssl_endpoint_remove('example', 'tokyo-1050').returns(endpoint) stderr, stdout = execute("certs:remove --confirm example --endpoint tokyo-1050") - stdout.should include "Removing SSL Endpoint tokyo-1050 from example..." - stdout.should include "NOTE: Billing is still active. Remove SSL Endpoint add-on to stop billing." + expect(stdout).to include "Removing SSL Endpoint tokyo-1050 from example..." + expect(stdout).to include "NOTE: Billing is still active. Remove SSL Endpoint add-on to stop billing." end it "requires confirmation" do stub_core.ssl_endpoint_list("example").returns([endpoint]) stderr, stdout = execute("certs:remove") - stdout.should include "WARNING: Potentially Destructive Action" - stdout.should include "This command will remove the endpoint tokyo-1050.herokussl.com from example." + expect(stdout).to include "WARNING: Potentially Destructive Action" + expect(stdout).to include "This command will remove the endpoint tokyo-1050.herokussl.com from example." end it "shows an error if an app has no endpoints" do stub_core.ssl_endpoint_list("example").returns([]) stderr, stdout = execute("certs:remove") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! example has no SSL Endpoints. STDERR end @@ -153,8 +153,8 @@ module Heroku::Command describe "certs:update" do before do - File.should_receive(:read).with("pem_file").and_return("pem content") - File.should_receive(:read).with("key_file").and_return("key content") + expect(File).to receive(:read).with("pem_file").and_return("pem content") + expect(File).to receive(:read).with("key_file").and_return("key content") end it "updates an endpoint" do @@ -162,7 +162,7 @@ module Heroku::Command stub_core.ssl_endpoint_update('example', 'tokyo-1050.herokussl.com', 'pem content', 'key content').returns(endpoint) stderr, stdout = execute("certs:update --confirm example --bypass pem_file key_file") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Updating SSL Endpoint tokyo-1050.herokussl.com for example... done Updated certificate details: #{certificate_details} @@ -173,7 +173,7 @@ module Heroku::Command stub_core.ssl_endpoint_update('example', 'tokyo-1050', 'pem content', 'key content').returns(endpoint) stderr, stdout = execute("certs:update --confirm example --bypass --endpoint tokyo-1050 pem_file key_file") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Updating SSL Endpoint tokyo-1050 for example... done Updated certificate details: #{certificate_details} @@ -184,15 +184,15 @@ module Heroku::Command stub_core.ssl_endpoint_list("example").returns([endpoint]) stderr, stdout = execute("certs:update --bypass pem_file key_file") - stdout.should include "WARNING: Potentially Destructive Action" - stdout.should include "This command will change the certificate of endpoint tokyo-1050.herokussl.com on example." + expect(stdout).to include "WARNING: Potentially Destructive Action" + expect(stdout).to include "This command will change the certificate of endpoint tokyo-1050.herokussl.com on example." end it "shows an error if an app has no endpoints" do stub_core.ssl_endpoint_list("example").returns([]) stderr, stdout = execute("certs:update --bypass pem_file key_file") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! example has no SSL Endpoints. STDERR end @@ -204,7 +204,7 @@ module Heroku::Command stub_core.ssl_endpoint_rollback('example', 'tokyo-1050.herokussl.com').returns(endpoint) stderr, stdout = execute("certs:rollback --confirm example") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Rolling back SSL Endpoint tokyo-1050.herokussl.com for example... done New active certificate details: #{certificate_details} @@ -215,7 +215,7 @@ module Heroku::Command stub_core.ssl_endpoint_rollback('example', 'tokyo-1050').returns(endpoint) stderr, stdout = execute("certs:rollback --confirm example --endpoint tokyo-1050") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Rolling back SSL Endpoint tokyo-1050 for example... done New active certificate details: #{certificate_details} @@ -226,18 +226,161 @@ module Heroku::Command stub_core.ssl_endpoint_list("example").returns([endpoint]) stderr, stdout = execute("certs:rollback") - stdout.should include "WARNING: Potentially Destructive Action" - stdout.should include "This command will rollback the certificate of endpoint tokyo-1050.herokussl.com on example." + expect(stdout).to include "WARNING: Potentially Destructive Action" + expect(stdout).to include "This command will rollback the certificate of endpoint tokyo-1050.herokussl.com on example." end it "shows an error if an app has no endpoints" do stub_core.ssl_endpoint_list("example").returns([]) stderr, stdout = execute("certs:rollback") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! example has no SSL Endpoints. STDERR end end + + describe "certs:generate" do + context "fails early" do + it "if domain not specified" do + stdout, stderr = execute("certs:generate") + expect(stdout).to eq(" ! certs:generate must specify a domain\n") + end + end + + context "successfully" do + let(:request) do + request = Heroku::OpenSSL::CertificateRequest.new + expect(Heroku::OpenSSL::CertificateRequest).to receive(:new).and_return(request) + + expect(request).to receive(:generate) do + if request.self_signed + Heroku::OpenSSL::CertificateRequest::Result.new(request, 'keyfile', nil, 'crtfile') + else + Heroku::OpenSSL::CertificateRequest::Result.new(request, 'keyfile', 'csrfile', nil) + end + end + + request + end + + before(:each) do + stub_core.ssl_endpoint_list("example").returns([endpoint]) + request() + end + + describe "with subject prompts" do + it "emitted if no parts of subject provided" do + expect_prompts /Owner/ => "Heroku", /Country/ => 'US', /State/ => 'California', /City/ => 'San Francisco' + stub_core.ssl_endpoint_list("example").returns([endpoint]) + + stdout, stderr = execute("certs:generate example.com") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("/C=US/ST=California/L=San Francisco/O=Heroku/CN=example.com") + end + + it "not emitted if any part of subject is specified" do + expect_prompts() + stub_core.ssl_endpoint_list("example").returns([endpoint]) + + stdout, stderr = execute("certs:generate example.com --owner Heroku") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("/O=Heroku/CN=example.com") + end + + it "not emitted if --now is specified" do + expect_prompts() + + stdout, stderr = execute("certs:generate example.com --now") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("/CN=example.com") + end + + it "not emitted if --subject is specified" do + expect_prompts() + + stdout, stderr = execute("certs:generate example.com --subject SOMETHING") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("SOMETHING") + end + + def expect_prompts(hash = {}) + hash.each do |question, answer| + expect_any_instance_of(Heroku::Command::Certs).to receive(:prompt).with(question).and_return(answer) + end + expect_any_instance_of(Heroku::Command::Certs).not_to receive(:prompt) + end + end + + describe "without --selfsigned" do + it "does not request a self-signed certificate" do + execute("certs:generate example.com --now") + expect(request.self_signed).to be false + end + + it "says it generated a key and CSR" do + stdout, stderr = execute("certs:generate example.com --now") + expect(stderr).to match(/^Your key and certificate signing request have been generated.$/) + end + + it "says the name of the CSR file" do + stdout, stderr = execute("certs:generate example.com --now") + expect(stderr).to match(/^Submit the CSR in 'csrfile' to your preferred certificate authority.$/) + end + end + + describe "with --selfsigned" do + it "requests a self-signed certificate" do + execute("certs:generate example.com --selfsigned --now") + expect(request.self_signed).to be true + end + + it "says it generated a key and self-signed certificate" do + stdout, stderr = execute("certs:generate example.com --selfsigned --now") + expect(stderr).to match(/^Your key and self-signed certificate have been generated.$/) + end + + it "says the name of the certificate file in the command" do + stdout, stderr = execute("certs:generate example.com --selfsigned --now") + expect(stderr).to match(/crtfile keyfile$/) + end + end + + describe "suggests next step" do + it "should be certs:add when domain is new" do + stdout, stderr = execute("certs:generate example.com --now") + expect(stderr).to match(/^\$ heroku certs:add CERTFILE keyfile$/) + end + + it "should be certs:update when domain is known" do + stdout, stderr = execute("certs:generate example.org --now") + expect(stderr).to match(/^\$ heroku certs:update CERTFILE keyfile$/) + end + + it "should be addons:add and certs:add when app doesn't have ssl:endpoint" do + stub_core.ssl_endpoint_list("example") { raise RestClient::Forbidden } + stdout, stderr = execute("certs:generate example.org --now") + expect(stderr).to match(/^\$ heroku addons:add ssl:endpoint$/) + expect(stderr).to match(/^\$ heroku certs:add CERTFILE keyfile$/) + end + end + + describe "key size" do + it "is 2048 unless otherwise specified" do + execute("certs:generate example.com --now") + expect(request.key_size).to eq(2048) + end + + it "can be changed using --keysize" do + execute("certs:generate example.com --now --keysize 4096") + expect(request.key_size).to eq(4096) + end + end + end + end end end diff --git a/spec/heroku/command/config_spec.rb b/spec/heroku/command/config_spec.rb index 8c30a2269..ee4dfa45a 100644 --- a/spec/heroku/command/config_spec.rb +++ b/spec/heroku/command/config_spec.rb @@ -6,17 +6,22 @@ module Heroku::Command before(:each) do stub_core api.post_app("name" => "example", "stack" => "cedar") + + Excon.stub(method: :get, path: %r{^/apps/example/releases/current}) do + { body: MultiJson.dump({ 'name' => 'v1' }), status: 200 } + end end after(:each) do api.delete_app("example") + Excon.stubs.shift end it "shows all configs" do api.put_config_vars("example", { 'FOO_BAR' => 'one', 'BAZ_QUX' => 'two' }) stderr, stdout = execute("config") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Config Vars BAZ_QUX: two FOO_BAR: one @@ -26,8 +31,8 @@ module Heroku::Command it "does not trim long values" do api.put_config_vars("example", { 'LONG' => 'A' * 60 }) stderr, stdout = execute("config") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Config Vars LONG: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA STDOUT @@ -36,8 +41,8 @@ module Heroku::Command it "handles when value is nil" do api.put_config_vars("example", { 'FOO_BAR' => 'one', 'BAZ_QUX' => nil }) stderr, stdout = execute("config") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Config Vars BAZ_QUX: FOO_BAR: one @@ -47,8 +52,8 @@ module Heroku::Command it "handles when value is a boolean" do api.put_config_vars("example", { 'FOO_BAR' => 'one', 'BAZ_QUX' => true }) stderr, stdout = execute("config") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Config Vars BAZ_QUX: true FOO_BAR: one @@ -56,20 +61,21 @@ module Heroku::Command end it "shows configs in a shell compatible format" do - api.put_config_vars("example", { 'A' => 'one', 'B' => 'two three' }) + api.put_config_vars("example", { 'A' => 'one', 'B' => 'two three', 'C' => "foo&bar" }) stderr, stdout = execute("config --shell") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT A=one -B=two three +B=two\\ three +C=foo\\&bar STDOUT end it "shows a single config for get" do api.put_config_vars("example", { 'LONG' => 'A' * 60 }) stderr, stdout = execute("config:get LONG") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA STDOUT end @@ -78,8 +84,8 @@ module Heroku::Command it "sets config vars" do stderr, stdout = execute("config:set A=1 B=2") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Setting config vars and restarting example... done, v1 A: 1 B: 2 @@ -88,8 +94,8 @@ module Heroku::Command it "allows config vars with = in the value" do stderr, stdout = execute("config:set A=b=c") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Setting config vars and restarting example... done, v1 A: b=c STDOUT @@ -97,8 +103,8 @@ module Heroku::Command it "sets config vars without changing case" do stderr, stdout = execute("config:set a=b") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Setting config vars and restarting example... done, v1 a: b STDOUT @@ -110,19 +116,19 @@ module Heroku::Command it "exits with a help notice when no keys are provides" do stderr, stdout = execute("config:unset") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku config:unset KEY1 [KEY2 ...] ! Must specify KEY to unset. STDERR - stdout.should == "" + expect(stdout).to eq("") end context "when one key is provided" do it "unsets a single key" do stderr, stdout = execute("config:unset A") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Unsetting A and restarting example... done, v1 STDOUT end @@ -131,9 +137,16 @@ module Heroku::Command context "when more than one key is provided" do it "unsets all given keys" do + request_number = 1 + Excon.stub(method: :get, path: %r{^/apps/example/releases/current}) do |req| + response = { body: MultiJson.dump({ 'name' => "v#{request_number}" }), status: 200 } + request_number += 1 + response + end + stderr, stdout = execute("config:unset A B") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Unsetting A and restarting example... done, v1 Unsetting B and restarting example... done, v2 STDOUT diff --git a/spec/heroku/command/domains_spec.rb b/spec/heroku/command/domains_spec.rb index 2b2432d66..c19f78fb2 100644 --- a/spec/heroku/command/domains_spec.rb +++ b/spec/heroku/command/domains_spec.rb @@ -6,7 +6,7 @@ module Heroku::Command before(:all) do api.post_app("name" => "example", "stack" => "cedar") - api.post_addon("example", "custom_domains:basic") + api.post_addon("example", "pgbackups:auto-month") end after(:all) do @@ -21,8 +21,8 @@ module Heroku::Command it "lists message with no domains" do stderr, stdout = execute("domains") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT example has no domain names. STDOUT end @@ -30,8 +30,8 @@ module Heroku::Command it "lists domains when some exist" do api.post_domain("example", "example.com") stderr, stdout = execute("domains") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Domain Names example.com @@ -43,8 +43,8 @@ module Heroku::Command it "adds domain names" do stderr, stdout = execute("domains:add example.com") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Adding example.com to example... done STDOUT api.delete_domain("example", "example.com") @@ -52,7 +52,7 @@ module Heroku::Command it "shows usage if no domain specified for add" do stderr, stdout = execute("domains:add") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku domains:add DOMAIN ! Must specify DOMAIN to add. STDERR @@ -61,15 +61,15 @@ module Heroku::Command it "removes domain names" do api.post_domain("example", "example.com") stderr, stdout = execute("domains:remove example.com") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Removing example.com from example... done STDOUT end it "shows usage if no domain specified for remove" do stderr, stdout = execute("domains:remove") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku domains:remove DOMAIN ! Must specify DOMAIN to remove. STDERR @@ -78,8 +78,8 @@ module Heroku::Command it "removes all domain names" do stub_core.remove_domains("example") stderr, stdout = execute("domains:clear") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Removing all domain names from example... done STDOUT end diff --git a/spec/heroku/command/drains_spec.rb b/spec/heroku/command/drains_spec.rb index 52edddc56..629f88ce8 100644 --- a/spec/heroku/command/drains_spec.rb +++ b/spec/heroku/command/drains_spec.rb @@ -7,8 +7,8 @@ it "can list drains" do stub_core.list_drains("example").returns("drains") stderr, stdout = execute("drains") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT drains STDOUT end @@ -16,8 +16,8 @@ it "can add drains" do stub_core.add_drain("example", "syslog://localhost/add").returns("added") stderr, stdout = execute("drains:add syslog://localhost/add") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT added STDOUT end @@ -25,8 +25,8 @@ it "can remove drains" do stub_core.remove_drain("example", "syslog://localhost/remove").returns("removed") stderr, stdout = execute("drains:remove syslog://localhost/remove") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT removed STDOUT end diff --git a/spec/heroku/command/fork_spec.rb b/spec/heroku/command/fork_spec.rb deleted file mode 100644 index af55bb6eb..000000000 --- a/spec/heroku/command/fork_spec.rb +++ /dev/null @@ -1,131 +0,0 @@ -require "heroku/api/releases_v3" -require "spec_helper" -require "heroku/command/fork" - -module Heroku::Command - - describe Fork do - - before(:each) do - stub_core - api.post_app("name" => "example", "stack" => "cedar") - end - - after(:each) do - api.delete_app("example") - begin - api.delete_app("example-fork") - rescue Heroku::API::Errors::NotFound - end - end - - context "successfully" do - - before(:each) do - Excon.stub({ :method => :get, - :path => "/apps/example/releases" }, - { :body => [{"slug" => {"id" => "SLUG_ID"}}], - :status => 206}) - - Excon.stub({ :method => :post, - :path => "/apps/example-fork/releases"}, - { :status => 201}) - end - - after(:each) do - Excon.stubs.shift - Excon.stubs.shift - end - - it "forks an app" do - stderr, stdout = execute("fork example-fork") - stderr.should == "" - stdout.should == <<-STDOUT -Creating fork example-fork... done -Copying slug... done -Copying config vars... done -Fork complete, view it at http://example-fork.herokuapp.com/ -STDOUT - end - - it "copies slug" do - Heroku::API.any_instance.should_receive(:get_releases_v3).with("example", "version ..; order=desc,max=1;").and_call_original - Heroku::API.any_instance.should_receive(:post_release_v3).with("example-fork", "SLUG_ID", "Forked from example").and_call_original - execute("fork example-fork") - end - - it "copies config vars" do - config_vars = { - "SECRET" => "imasecret", - "FOO" => "bar", - "LANG_ENV" => "production" - } - api.put_config_vars("example", config_vars) - execute("fork example-fork") - api.get_config_vars("example-fork").body.should == config_vars - end - - it "re-provisions add-ons" do - addons = ["pgbackups:basic", "deployhooks:http"].sort - addons.each { |a| api.post_addon("example", a) } - execute("fork example-fork") - api.get_addons("example-fork").body.collect { |info| info["name"] }.sort.should == addons - end - end - - describe "error handling" do - it "fails if no source release exists" do - begin - Excon.stub({ :method => :get, - :path => "/apps/example/releases" }, - { :body => [], - :status => 206}) - execute("fork example-fork") - raise - rescue Heroku::Command::CommandFailed => e - e.message.should == "No releases on example" - ensure - Excon.stubs.shift - end - end - - it "fails if source slug does not exist" do - begin - Excon.stub({ :method => :get, - :path => "/apps/example/releases" }, - { :body => [{"slug" => nil}], - :status => 206}) - execute("fork example-fork") - raise - rescue Heroku::Command::CommandFailed => e - e.message.should == "No slug on example" - ensure - Excon.stubs.shift - end - end - - it "doesn't attempt to fork to the same app" do - lambda do - execute("fork example") - end.should raise_error(Heroku::Command::CommandFailed, /same app/) - end - - it "confirms before deleting the app" do - Excon.stub({:path => "/apps/example/releases"}, {:status => 500}) - begin - execute("fork example-fork") - rescue Heroku::API::Errors::ErrorWithResponse - ensure - Excon.stubs.shift - end - api.get_apps.body.map { |app| app["name"] }.should == - %w( example example-fork ) - end - - it "deletes fork app on error, before re-raising" do - stub(Heroku::Command).confirm_command.returns(true) - api.get_apps.body.map { |app| app["name"] }.should == %w( example ) - end - end - end -end diff --git a/spec/heroku/command/git_spec.rb b/spec/heroku/command/git_spec.rb deleted file mode 100644 index b878fad29..000000000 --- a/spec/heroku/command/git_spec.rb +++ /dev/null @@ -1,144 +0,0 @@ -require 'spec_helper' -require 'heroku/command/git' - -module Heroku::Command - describe Git do - - before(:each) do - stub_core - end - - context("clone") do - - before(:each) do - api.post_app("name" => "example", "stack" => "cedar") - end - - after(:each) do - api.delete_app("example") - end - - it "clones and adds remote" do - any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku git@heroku.com:example.git") do - puts "Cloning into 'example'..." - end - end - stderr, stdout = execute("git:clone example") - stderr.should == "" - stdout.should == <<-STDOUT -Cloning from app 'example'... -Cloning into 'example'... - STDOUT - end - - it "clones into another dir" do - any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku git@heroku.com:example.git somedir") do - puts "Cloning into 'somedir'..." - end - end - stderr, stdout = execute("git:clone example somedir") - stderr.should == "" - stdout.should == <<-STDOUT -Cloning from app 'example'... -Cloning into 'somedir'... - STDOUT - end - - it "can specify app with -a" do - any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku git@heroku.com:example.git") do - puts "Cloning into 'example'..." - end - end - stderr, stdout = execute("git:clone -a example") - stderr.should == "" - stdout.should == <<-STDOUT -Cloning from app 'example'... -Cloning into 'example'... - STDOUT - end - - it "can specify app with -a and a dir" do - any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku git@heroku.com:example.git somedir") do - puts "Cloning into 'somedir'..." - end - end - stderr, stdout = execute("git:clone -a example somedir") - stderr.should == "" - stdout.should == <<-STDOUT -Cloning from app 'example'... -Cloning into 'somedir'... - STDOUT - end - - it "clones and sets -r remote" do - any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o other git@heroku.com:example.git") do - puts "Cloning into 'example'..." - end - end - stderr, stdout = execute("git:clone example -r other") - stderr.should == "" - stdout.should == <<-STDOUT -Cloning from app 'example'... -Cloning into 'example'... - STDOUT - end - - end - - context("remote") do - - before(:each) do - api.post_app("name" => "example", "stack" => "cedar") - FileUtils.mkdir('example') - FileUtils.chdir('example') { `git init` } - end - - after(:each) do - api.delete_app("example") - FileUtils.rm_rf('example') - end - - it "adds remote" do - any_instance_of(Heroku::Command::Git) do |git| - stub(git).git('remote').returns("origin") - stub(git).git('remote add heroku git@heroku.com:example.git') - end - stderr, stdout = execute("git:remote") - stderr.should == "" - stdout.should == <<-STDOUT -Git remote heroku added - STDOUT - end - - it "adds -r remote" do - any_instance_of(Heroku::Command::Git) do |git| - stub(git).git('remote').returns("origin") - stub(git).git('remote add other git@heroku.com:example.git') - end - stderr, stdout = execute("git:remote -r other") - stderr.should == "" - stdout.should == <<-STDOUT -Git remote other added - STDOUT - end - - it "skips remote when it already exists" do - any_instance_of(Heroku::Command::Git) do |git| - stub(git).git('remote').returns("heroku") - end - stderr, stdout = execute("git:remote") - stderr.should == <<-STDERR - ! Git remote heroku already exists -STDERR - stdout.should == "" - end - - end - - end -end diff --git a/spec/heroku/command/help_spec.rb b/spec/heroku/command/help_spec.rb index e851cdca5..39e63bbef 100644 --- a/spec/heroku/command/help_spec.rb +++ b/spec/heroku/command/help_spec.rb @@ -7,64 +7,64 @@ describe "help" do it "should show root help with no args" do stderr, stdout = execute("help") - stderr.should == "" - stdout.should include "Usage: heroku COMMAND [--app APP] [command-specific-options]" - stdout.should include "apps" - stdout.should include "help" + expect(stderr).to eq("") + expect(stdout).to include "Usage: heroku COMMAND [--app APP] [command-specific-options]" + expect(stdout).to include "apps" + expect(stdout).to include "help" end - it "should show command help and namespace help when ambigious" do + it "should show command help and namespace help when ambiguous" do stderr, stdout = execute("help apps") - stderr.should == "" - stdout.should include "heroku apps" - stdout.should include "list your apps" - stdout.should include "Additional commands" - stdout.should include "apps:create" + expect(stderr).to eq("") + expect(stdout).to include "heroku apps" + expect(stdout).to include "list your apps" + expect(stdout).to include "Additional commands" + expect(stdout).to include "apps:create" end it "should show only command help when not ambiguous" do stderr, stdout = execute("help apps:create") - stderr.should == "" - stdout.should include "heroku apps:create" - stdout.should include "create a new app" - stdout.should_not include "Additional commands" + expect(stderr).to eq("") + expect(stdout).to include "heroku apps:create" + expect(stdout).to include "create a new app" + expect(stdout).not_to include "Additional commands" end it "should show command help with --help" do stderr, stdout = execute("apps:create --help") - stderr.should == "" - stdout.should include "Usage: heroku apps:create" - stdout.should include "create a new app" - stdout.should_not include "Additional commands" + expect(stderr).to eq("") + expect(stdout).to include "Usage: heroku apps:create" + expect(stdout).to include "create a new app" + expect(stdout).not_to include "Additional commands" end it "should redirect if the command is an alias" do stderr, stdout = execute("help create") - stderr.should == "" - stdout.should include "Alias: create redirects to apps:create" - stdout.should include "Usage: heroku apps:create" - stdout.should include "create a new app" - stdout.should_not include "Additional commands" + expect(stderr).to eq("") + expect(stdout).to include "Alias: create redirects to apps:create" + expect(stdout).to include "Usage: heroku apps:create" + expect(stdout).to include "create a new app" + expect(stdout).not_to include "Additional commands" end it "should show if the command does not exist" do stderr, stdout = execute("help sudo:sandwich") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! sudo:sandwich is not a heroku command. See `heroku help`. STDERR - stdout.should == "" + expect(stdout).to eq("") end it "should show help with naked -h" do stderr, stdout = execute("-h") - stderr.should == "" - stdout.should include "Usage: heroku COMMAND" + expect(stderr).to eq("") + expect(stdout).to include "Usage: heroku COMMAND" end it "should show help with naked --help" do stderr, stdout = execute("--help") - stderr.should == "" - stdout.should include "Usage: heroku COMMAND" + expect(stderr).to eq("") + expect(stdout).to include "Usage: heroku COMMAND" end describe "with legacy help" do @@ -72,21 +72,21 @@ it "displays the legacy group in the namespace list" do stderr, stdout = execute("help") - stderr.should == "" - stdout.should include "Foo Group" + expect(stderr).to eq("") + expect(stdout).to include "Foo Group" end it "displays group help" do stderr, stdout = execute("help foo") - stderr.should == "" - stdout.should include "do a bar to foo" - stdout.should include "do a baz to foo" + expect(stderr).to eq("") + expect(stdout).to include "do a bar to foo" + expect(stdout).to include "do a baz to foo" end it "displays legacy command-specific help" do stderr, stdout = execute("help foo:bar") - stderr.should == "" - stdout.should include "do a bar to foo" + expect(stderr).to eq("") + expect(stdout).to include "do a bar to foo" end end end diff --git a/spec/heroku/command/keys_spec.rb b/spec/heroku/command/keys_spec.rb index cfc18737e..a5221544a 100644 --- a/spec/heroku/command/keys_spec.rb +++ b/spec/heroku/command/keys_spec.rb @@ -7,40 +7,36 @@ module Heroku::Command before(:each) do stub_core + allow(Heroku::Auth).to receive(:home_directory).and_return(Heroku::Helpers.home_directory) end context("add") do - - after(:each) do - api.delete_key("pedro@heroku") - end - it "tries to find a key if no key filename is supplied" do - Heroku::Auth.should_receive(:ask).and_return("y") - Heroku::Auth.should_receive(:generate_ssh_key) - File.should_receive(:exists?).with('.git').and_return(false) - File.should_receive(:exists?).with('/.ssh/id_rsa.pub').and_return(true) - File.should_receive(:read).with('/.ssh/id_rsa.pub').and_return(KEY) + expect(Heroku::Auth).to receive(:ask).and_return("y") stderr, stdout = execute("keys:add") - stderr.should == "" - stdout.should == <<-STDOUT -Could not find an existing public key. + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Could not find an existing public key at ~/.ssh/id_rsa.pub Would you like to generate one? [Yn] Generating new SSH public key. -Uploading SSH public key /.ssh/id_rsa.pub... done +Uploading SSH public key #{Heroku::Auth.home_directory}/.ssh/id_rsa.pub... done STDOUT + api.delete_key(`whoami`.strip + '@' + `hostname`.strip) end it "adds a key from a specified keyfile path" do - File.should_receive(:exists?).with('.git').and_return(false) - File.should_receive(:exists?).with('/my/key.pub').and_return(true) - File.should_receive(:read).with('/my/key.pub').and_return(KEY) + # This is because the JSPlugin makes a call to File.exists + # Not pretty, but will always work and should be temporary + allow(Heroku::JSPlugin).to receive(:setup?).and_return(false) + expect(File).to receive(:exists?).with('.git').and_return(false) + expect(File).to receive(:exists?).with('/my/key.pub').and_return(true) + expect(File).to receive(:read).with('/my/key.pub').and_return(KEY) stderr, stdout = execute("keys:add /my/key.pub") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Uploading SSH public key /my/key.pub... done STDOUT + api.delete_key("pedro@heroku") end - end context("index") do @@ -55,8 +51,8 @@ module Heroku::Command it "list keys, trimming the hex code for better display" do stderr, stdout = execute("keys") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === email@example.com Keys ssh-rsa AAAAB3NzaC...Fyoke4MQ== pedro@heroku @@ -65,8 +61,8 @@ module Heroku::Command it "list keys showing the whole key hex with --long" do stderr, stdout = execute("keys --long") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === email@example.com Keys ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAp9AJD5QABmOcrkHm6SINuQkDefaR0MUrfgZ1Pxir3a4fM1fwa00dsUwbUaRuR7FEFD8n1E9WwDf8SwQTHtyZsJg09G9myNqUzkYXCmydN7oGr5IdVhRyv5ixcdiE0hj7dRnOJg2poSQ3Qi+Ka8SVJzF7nIw1YhuicHPSbNIFKi5s0D5a+nZb/E6MNGvhxoFCQX2IcNxaJMqhzy1ESwlixz45aT72mXYq0LIxTTpoTqma1HuKdRY8HxoREiivjmMQulYP+CxXFcMyV9kxTKIUZ/FXqlC6G5vSm3J4YScSatPOj9ID5HowpdlIx8F6y4p1/28r2tTl4CY40FFyoke4MQ== pedro@heroku @@ -85,8 +81,8 @@ module Heroku::Command it "succeeds" do stderr, stdout = execute("keys:remove pedro@heroku") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Removing pedro@heroku SSH key... done STDOUT end @@ -95,11 +91,11 @@ module Heroku::Command it "displays an error if no key is specified" do stderr, stdout = execute("keys:remove") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku keys:remove KEY ! Must specify KEY to remove. STDERR - stdout.should == "" + expect(stdout).to eq("") end end @@ -108,8 +104,8 @@ module Heroku::Command it "succeeds" do stderr, stdout = execute("keys:clear") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Removing all SSH keys... done STDOUT end diff --git a/spec/heroku/command/labs_spec.rb b/spec/heroku/command/labs_spec.rb index 60c935074..a25927934 100644 --- a/spec/heroku/command/labs_spec.rb +++ b/spec/heroku/command/labs_spec.rb @@ -16,85 +16,87 @@ module Heroku::Command it "lists available features" do stderr, stdout = execute("labs:list") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === User Features (email@example.com) -[ ] sumo-rankings Heroku Sumo ranks and visualizes the scale of your app, and suggests the optimum combination of dynos and add-ons to take it to the next level. +[ ] github-sync Allow users to set up automatic GitHub deployments from Dashboard +[ ] pipelines Pipelines adds experimental support for deploying changes between applications with a shared code base. === App Features (example) -[+] sigterm-all When stopping a dyno, send SIGTERM to all processes rather than only to the root process. -[ ] user_env_compile Add user config vars to the environment during slug compilation +[+] http-dyno-logs Enable HTTP dyno logs using log-shuttle [alpha] +[ ] log-runtime-metrics Emit dyno resource usage information into app logs STDOUT end it "lists enabled features" do stub_core.list_features("example").returns([]) stderr, stdout = execute("labs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === User Features (email@example.com) -[ ] sumo-rankings Heroku Sumo ranks and visualizes the scale of your app, and suggests the optimum combination of dynos and add-ons to take it to the next level. +[ ] github-sync Allow users to set up automatic GitHub deployments from Dashboard +[ ] pipelines Pipelines adds experimental support for deploying changes between applications with a shared code base. === App Features (example) -[+] sigterm-all When stopping a dyno, send SIGTERM to all processes rather than only to the root process. -[ ] user_env_compile Add user config vars to the environment during slug compilation +[+] http-dyno-logs Enable HTTP dyno logs using log-shuttle [alpha] +[ ] log-runtime-metrics Emit dyno resource usage information into app logs STDOUT end it "displays details of a feature" do - stderr, stdout = execute("labs:info user_env_compile") - stderr.should == "" - stdout.should == <<-STDOUT -=== user_env_compile -Docs: http://devcenter.heroku.com/articles/labs-user-env-compile -Summary: Add user config vars to the environment during slug compilation + stderr, stdout = execute("labs:info pipelines") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== pipelines +Docs: https://devcenter.heroku.com/articles/using-pipelines-to-deploy-between-applications +Summary: Pipelines adds experimental support for deploying changes between applications with a shared code base. STDOUT end it "shows usage if no feature name is specified for info" do stderr, stdout = execute("labs:info") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku labs:info FEATURE ! Must specify FEATURE for info. STDERR - stdout.should == "" + expect(stdout).to eq("") end it "enables a feature" do - stderr, stdout = execute("labs:enable user_env_compile") - stderr.should == "" - stdout.should == <<-STDOUT -Enabling user_env_compile for example... done + stderr, stdout = execute("labs:enable pipelines") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Enabling pipelines for email@example.com... done WARNING: This feature is experimental and may change or be removed without notice. -For more information see: http://devcenter.heroku.com/articles/labs-user-env-compile +For more information see: https://devcenter.heroku.com/articles/using-pipelines-to-deploy-between-applications STDOUT end it "shows usage if no feature name is specified for enable" do stderr, stdout = execute("labs:enable") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku labs:enable FEATURE ! Must specify FEATURE to enable. STDERR - stdout.should == "" + expect(stdout).to eq("") end it "disables a feature" do - api.post_feature('user_env_compile', 'example') - stderr, stdout = execute("labs:disable user_env_compile") - stderr.should == "" - stdout.should == <<-STDOUT -Disabling user_env_compile for example... done + api.post_feature('pipelines', 'example') + stderr, stdout = execute("labs:disable pipelines") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Disabling pipelines for email@example.com... done STDOUT end it "shows usage if no feature name is specified for disable" do stderr, stdout = execute("labs:disable") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku labs:disable FEATURE ! Must specify FEATURE to disable. STDERR - stdout.should == "" + expect(stdout).to eq("") end end end diff --git a/spec/heroku/command/logs_spec.rb b/spec/heroku/command/logs_spec.rb index edf15f541..06a0eb500 100644 --- a/spec/heroku/command/logs_spec.rb +++ b/spec/heroku/command/logs_spec.rb @@ -27,8 +27,8 @@ old_stdout_isatty = $stdout.isatty stub($stdout).isatty.returns(true) stderr, stdout = execute("logs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT \e[36m2011-01-01T00:00:00+00:00 app[web.1]:\e[0m test STDOUT stub($stdout).isatty.returns(old_stdout_isatty) @@ -38,8 +38,8 @@ old_stdout_isatty = $stdout.isatty stub($stdout).isatty.returns(false) stderr, stdout = execute("logs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT 2011-01-01T00:00:00+00:00 app[web.1]: test STDOUT stub($stdout).isatty.returns(old_stdout_isatty) @@ -48,13 +48,25 @@ it "does not use ansi if TERM is not set" do term = ENV.delete("TERM") stderr, stdout = execute("logs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT 2011-01-01T00:00:00+00:00 app[web.1]: test STDOUT ENV["TERM"] = term end + + it "uses ansi if --force-colors is passed, even if stdout is not a tty and TERM is not set" do + old_term = ENV.delete("TERM") + old_stdout_isatty = $stdout.isatty + stub($stdout).isatty.returns(false) + stderr, stdout = execute("logs --force-colors") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +\e[36m2011-01-01T00:00:00+00:00 app[web.1]:\e[0m test +STDOUT + ENV["TERM"] = old_term + stub($stdout).isatty.returns(old_stdout_isatty) + end end end - end diff --git a/spec/heroku/command/maintenance_spec.rb b/spec/heroku/command/maintenance_spec.rb deleted file mode 100644 index 77168e91a..000000000 --- a/spec/heroku/command/maintenance_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require "spec_helper" -require "heroku/command/maintenance" - -module Heroku::Command - describe Maintenance do - - before(:each) do - stub_core - api.post_app("name" => "example", "stack" => "cedar") - end - - after(:each) do - api.delete_app("example") - end - - it "displays off for maintenance mode of an app" do - stderr, stdout = execute("maintenance") - stderr.should == "" - stdout.should == <<-STDOUT -off -STDOUT - end - - it "displays on for maintenance mode of an app" do - api.post_app_maintenance('example', '1') - - stderr, stdout = execute("maintenance") - stderr.should == "" - stdout.should == <<-STDOUT -on -STDOUT - end - - it "turns on maintenance mode for the app" do - stderr, stdout = execute("maintenance:on") - stderr.should == "" - stdout.should == <<-STDOUT -Enabling maintenance mode for example... done -STDOUT - end - - it "turns off maintenance mode for the app" do - stderr, stdout = execute("maintenance:off") - stderr.should == "" - stdout.should == <<-STDOUT -Disabling maintenance mode for example... done -STDOUT - end - - end -end diff --git a/spec/heroku/command/orgs_spec.rb b/spec/heroku/command/orgs_spec.rb index 11eafecc8..27ccf6a5e 100644 --- a/spec/heroku/command/orgs_spec.rb +++ b/spec/heroku/command/orgs_spec.rb @@ -16,8 +16,8 @@ module Heroku::Command context(:index) do it "displays a message when you have no org memberships" do stderr, stdout = execute("orgs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT You are not a member of any organizations. STDOUT end @@ -25,14 +25,14 @@ module Heroku::Command it "lists orgs with roles that the user belongs to" do Excon.stub({ :method => :get, :path => '/v1/user/info' }, { - :body => Heroku::OkJson.encode({"organizations" => [{"organization_name" => "test-org", "role" => "collaborator"}, {"organization_name" => "test-org2", "role" => "admin"}], "user" => {}}), + :body => MultiJson.dump({"organizations" => [{"organization_name" => "test-org", "role" => "collaborator"}, {"organization_name" => "test-org2", "role" => "admin"}], "user" => {}}), :status => 200 } ) stderr, stdout = execute("orgs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT test-org collaborator test-org2 admin @@ -42,103 +42,41 @@ module Heroku::Command it "labels a user's default organization" do Excon.stub({ :method => :get, :path => '/v1/user/info' }, { - :body => Heroku::OkJson.encode({"organizations" => [{"organization_name" => "test-org", "role" => "collaborator"}, {"organization_name" => "test-org2", "role" => "admin"}], "user" => {"default_organization" => "test-org2"}}), + :body => MultiJson.dump({"organizations" => [{"organization_name" => "test-org", "role" => "collaborator"}, {"organization_name" => "test-org2", "role" => "admin"}]}), :status => 200 } ) stderr, stdout = execute("orgs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT test-org collaborator -test-org2 admin, default - -STDOUT - end - end - - context(:default) do - context "when a target org is specified" do - it "sets the default org to the target" do - org_api.should_receive(:set_default_org).with("test-org").once - stderr, stdout = execute("orgs:default test-org") - stderr.should == "" - stdout.should == <<-STDOUT -Setting test-org as the default organization... done -STDOUT - end - - it "removes the default org when the org name is 'personal'" do - org_api.should_receive(:remove_default_org).once - stderr, stdout = execute("orgs:default personal") - stderr.should == "" - stdout.should == <<-STDOUT -Setting personal account as default... done -STDOUT - end - - it "removes the defautl org when the personal flag is passed" do - org_api.should_receive(:remove_default_org).once - stderr, stdout = execute("orgs:default --personal") - stderr.should == "" - stdout.should == <<-STDOUT -Setting personal account as default... done -STDOUT - end - - end - - context "when no target is specified" do - it "displays the default organization when present" do - Excon.stub({ :method => :get, :path => '/v1/user/info' }, - { - :body => Heroku::OkJson.encode({"user" => {"default_organization" => "test-org"}}), - :status => 200 - } - ) - - stderr, stdout = execute("orgs:default") - stderr.should == "" - stdout.should == <<-STDOUT -test-org is the default organization. -STDOUT - end +test-org2 admin - it "displays personal account as default when no org present" do - stderr, stdout = execute("orgs:default") - stderr.should == "" - stdout.should == <<-STDOUT -Personal account is default. STDOUT - end end end context(:open) do before(:each) do require("launchy") - ::Launchy.should_receive(:open).with("https://dashboard.heroku.com/orgs/test-org/apps").once.and_return("") + expect(::Launchy).to receive(:open).with("https://dashboard.heroku.com/orgs/test-org/apps").once.and_return("") end it "opens the org specified in an argument" do - stderr, stdout = execute("orgs:open --org test-org") - stdout.should == <<-STDOUT + _, stdout = execute("orgs:open --org test-org") + expect(stdout).to eq <<-STDOUT Opening web interface for test-org... done STDOUT end - it "opens the default org" do - Excon.stub({ :method => :get, :path => '/v1/user/info' }, - { - :body => Heroku::OkJson.encode({"organizations" => [{"organization_name" => "test-org"}], "user" => {"default_organization" => "test-org"}}), - :status => 200 - } - ) - - stderr, stdout = execute("orgs:open") - stdout.should == <<-STDOUT + it "opens the org specified in HEROKU_ORGANIZATION" do + ENV['HEROKU_ORGANIZATION'] = 'test-org' + _, stdout = execute("orgs:open") + expect(stdout).to eq <<-STDOUT Opening web interface for test-org... done STDOUT + ENV['HEROKU_ORGANIZATION'] = nil end end end diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb new file mode 100644 index 000000000..8a6b5f980 --- /dev/null +++ b/spec/heroku/command/pg_backups_spec.rb @@ -0,0 +1,552 @@ +require "spec_helper" +require "heroku/command/pg" +require "heroku/command/pg_backups" + +module Heroku::Command + describe Pg do + let(:ivory_url) { 'postgres:///database_url' } + let(:green_url) { 'postgres:///green_database_url' } + let(:red_url) { 'postgres:///red_database_url' } + + let(:teal_url) { 'postgres:///teal_database_url' } + + let(:example_attachments) do + [ + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'example'}, + 'name' => 'HEROKU_POSTGRESQL_IVORY', + 'config_var' => 'HEROKU_POSTGRESQL_IVORY_URL', + 'resource' => {'name' => 'loudly-yelling-1232', + 'value' => ivory_url, + 'type' => 'heroku-postgresql:standard-0' }}), + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'example'}, + 'name' => 'HEROKU_POSTGRESQL_GREEN', + 'config_var' => 'HEROKU_POSTGRESQL_GREEN_URL', + 'resource' => {'name' => 'softly-mocking-123', + 'value' => green_url, + 'type' => 'heroku-postgresql:standard-0' }}), + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'example'}, + 'name' => 'HEROKU_POSTGRESQL_RED', + 'config_var' => 'HEROKU_POSTGRESQL_RED_URL', + 'resource' => {'name' => 'whatever-something-2323', + 'value' => red_url, + 'type' => 'heroku-postgresql:standard-0' }}) + ] + end + + let(:aux_example_attachments) do + [ + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'aux-example'}, + 'name' => 'HEROKU_POSTGRESQL_TEAL', + 'config_var' => 'HEROKU_POSTGRESQL_TEAL_URL', + 'resource' => {'name' => 'loudly-yelling-1232', + 'value' => teal_url, + 'type' => 'heroku-postgresql:standard-0' }}) + ] + end + + before do + stub_core + + api.post_app "name" => "example" + api.put_config_vars "example", { + "DATABASE_URL" => "postgres://database_url", + "HEROKU_POSTGRESQL_IVORY_URL" => ivory_url, + "HEROKU_POSTGRESQL_GREEN_URL" => green_url, + "HEROKU_POSTGRESQL_RED_URL" => red_url, + } + + api.post_app "name" => "aux-example" + api.put_config_vars "aux-example", { + "DATABASE_URL" => "postgres://database_url", + "HEROKU_POSTGRESQL_TEAL_URL" => teal_url + } + end + + after do + api.delete_app "aux-example" + api.delete_app "example" + end + + describe "heroku pg:copy" do + let(:copy_info) do + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'pg_dump', :to_type => 'pg_restore', + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => true } + end + + before do + # hideous hack because we can't do dependency injection + orig_new = Heroku::Helpers::HerokuPostgresql::Resolver.method(:new) + allow(Heroku::Helpers::HerokuPostgresql::Resolver).to receive(:new) do |app_name, api| + resolver = orig_new.call(app_name, api) + allow(resolver).to receive(:app_attachments) do + if resolver.app_name == 'example' + example_attachments + else + aux_example_attachments + end + end + resolver + end + end + + it "copies data from one database to another" do + stub_pg.pg_copy('IVORY', ivory_url, 'RED', red_url).returns(copy_info) + stub_pgapp.transfers_get.returns(copy_info) + + stderr, stdout = execute("pg:copy ivory red --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/Copy completed/) + end + + it "does not copy without confirmation" do + stderr, stdout = execute("pg:copy ivory red") + expect(stderr).to match(/Confirmation did not match example. Aborted./) + expect(stdout).to match(/WARNING: Destructive Action/) + expect(stdout).to match(/This command will affect the app: example/) + expect(stdout).to match(/To proceed, type "example" or re-run this command with --confirm example/) + end + + it "copies across apps" do + stub_pg.pg_copy('TEAL', teal_url, 'RED', red_url).returns(copy_info) + stub_pgapp.transfers_get.returns(copy_info) + + stderr, stdout = execute("pg:copy aux-example::teal red --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/Copy completed/) + end + end + + describe "heroku pg:backups schedules" do + let(:schedules) do + [ { name: 'HEROKU_POSTGRESQL_GREEN_URL', + uuid: 'ffffffff-ffff-ffff-ffff-ffffffffffff', + hour: 4, timezone: 'US/Pacific' }, + { name: 'DATABASE_URL', + uuid: 'ffffffff-ffff-ffff-ffff-fffffffffffe', + hour: 20, timezone: 'UTC' } ] + end + + it "lists the existing schedules" do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return(example_attachments) + stub_pg.schedules.returns(schedules) + stderr, stdout = execute("pg:backups schedules") + expect(stderr).to be_empty + expect(stdout).to eq(<<-EOF) +=== Backup Schedules +HEROKU_POSTGRESQL_GREEN_URL: daily at 4:00 (US/Pacific) +DATABASE_URL: daily at 20:00 (UTC) +EOF + end + + it "reports there are no schedules when none exist" do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return(example_attachments) + stub_pg.schedules.returns([]) + stderr, stdout = execute("pg:backups schedules") + expect(stderr).to be_empty + expect(stdout).to match(/No backup schedules found/) + end + + it "reports there are no databases when the app has none" do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return([]) + stderr, stdout = execute("pg:backups schedules") + expect(stderr).to match(/example has no heroku-postgresql databases/) + expect(stdout).to be_empty + end + end + + describe "heroku pg:backups unschedule" do + let(:schedules) do + [ { name: 'HEROKU_POSTGRESQL_GREEN_URL', + uuid: 'ffffffff-ffff-ffff-ffff-ffffffffffff' }, + { name: 'DATABASE_URL', + uuid: 'ffffffff-ffff-ffff-ffff-fffffffffffe' } ] + end + + before do + stub_pg.schedules.returns(schedules) + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return(example_attachments) + end + + it "unschedules the specified backup" do + stub_pg.unschedule + stderr, stdout = execute("pg:backups unschedule green --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/Stopped automatic daily backups for/) + end + + it "complains when called without an argument" do + stderr, stdout = execute("pg:backups unschedule --confirm example") + expect(stderr).to match(/Must specify schedule to cancel/) + expect(stdout).to be_empty + end + + it "indicates when no matching backup can be unscheduled" do + stderr, stdout = execute("pg:backups unschedule red --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/No automatic daily backups for/) + end + end + + describe "heroku pg:backups" do + let(:logged_at) { Time.now } + let(:started_at) { Time.now } + let(:finished_at) { Time.now } + let(:from_name) { 'RED' } + let(:source_size) { 42 } + let(:backup_size) { source_size / 2 } + + let(:logs) { [{ 'created_at' => logged_at, 'message' => "hello world" }] } + let(:transfers) do + [ + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_name => from_name, :to_name => 'BACKUP', + :num => 1, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_name => from_name, :to_name => 'BACKUP', + :num => 2, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :succeeded => false }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'gof3r', :to_type => 'pg_restore', num: 3, + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'gof3r', :to_type => 'pg_restore', num: 4, + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => false }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'pg_dump', :to_type => 'pg_restore', num: 5, + :started_at => Time.now, :finished_at => Time.now, + :from_name => "CRIMSON", :to_name => "CLOVER", + :processed_bytes => 42, :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'pg_dump', :to_type => 'pg_restore', num: 6, + :started_at => Time.now, :finished_at => Time.now, + :from_name => "CRIMSON", :to_name => "CLOVER", + :processed_bytes => 42, :succeeded => false }, + { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffd', + :from_name => from_name, :to_name => 'PGBACKUPS BACKUP', + :num => 7, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :options => { "pgbackups_name" => "b047" }, + :succeeded => true } + ] + end + + before do + (1..7).each do |n| + stub_pgapp.transfers_get(n, true). + returns(transfers.find { |xfer| xfer[:num] == n }) + end + stub_pgapp.transfers.returns(transfers) + end + + it "lists successful backups" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/b001\s*Finished/) + end + + it "list failed backups" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/b002\s*Failed/) + end + + it "lists old pgbackups" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/ob047\s*Finished/) + end + + it "lists successful restores" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/r003\s*Finished/) + end + + it "lists failed restores" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/r004\s*Failed/) + end + + it "lists successful copies" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/===\sCopies/) + expect(stdout).to match(/c005\s*Finished/) + end + + it "lists failed copies" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/c006\s*Failed/) + end + + describe "heroku pg:backups info" do + it "displays info for the given backup" do + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: b001 +Database: #{from_name} +Started: #{started_at} +Finished: #{finished_at} +Status: Completed Successfully +Type: Manual +Original DB Size: #{source_size}.0B +Backup Size: #{backup_size}.0B (50% compression) +=== Backup Logs +#{logged_at}: hello world + EOF + end + + it "displays info for legacy PGBackups backups" do + stderr, stdout = execute("pg:backups info ob047") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: ob047 +Database: #{from_name} +Started: #{started_at} +Finished: #{finished_at} +Status: Completed Successfully +Type: Manual +Original DB Size: #{source_size}.0B +Backup Size: #{backup_size}.0B (50% compression) +=== Backup Logs +#{logged_at}: hello world + EOF + end + + it "defaults to the latest backup if none is specified" do + stderr, stdout = execute("pg:backups info") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: ob047 +Database: #{from_name} +Started: #{started_at} +Finished: #{finished_at} +Status: Completed Successfully +Type: Manual +Original DB Size: #{source_size}.0B +Backup Size: #{backup_size}.0B (50% compression) +=== Backup Logs +#{logged_at}: hello world + EOF + end + + it "does not display finished time or compression ratio if backup is not finished" do + xfer = transfers.find { |xfer| xfer[:num] == 1 } + xfer[:finished_at] = nil + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: b001 +Database: #{from_name} +Started: #{started_at} +Status: Completed Successfully +Type: Manual +Original DB Size: #{source_size}.0B +Backup Size: #{backup_size}.0B +=== Backup Logs +#{logged_at}: hello world + EOF + end + + it "works when the progress is at 0 bytes" do + xfer = transfers.find { |xfer| xfer[:num] == 1 } + xfer[:processed_bytes] = 0 + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: b001 +Database: #{from_name} +Started: #{started_at} +Finished: #{finished_at} +Status: Completed Successfully +Type: Manual +Original DB Size: #{source_size}.0B +Backup Size: 0.00B (0% compression) +=== Backup Logs +#{logged_at}: hello world + EOF + end + + it "works when the source size is 0 bytes" do + xfer = transfers.find { |xfer| xfer[:num] == 1 } + xfer[:source_bytes] = 0 + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: b001 +Database: #{from_name} +Started: #{started_at} +Finished: #{finished_at} +Status: Completed Successfully +Type: Manual +Backup Size: #{backup_size}.0B +=== Backup Logs +#{logged_at}: hello world + EOF + end + end + end + + + describe "heroku pg:backups restore" do + let(:started_at) { Time.parse('2001-01-01 00:00:00') } + let(:finished_at_1) { Time.parse('2001-01-01 01:00:00') } + let(:finished_at_2) { Time.parse('2001-01-01 02:00:00') } + let(:finished_at_3) { Time.parse('2001-01-01 03:00:00') } + + let(:from_name) { 'RED' } + let(:to_url) { 'https://example.com/my-backup' } + + let(:transfers) do + [ + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_name => from_name, :to_name => 'BACKUP', :num => 1, + :from_type => 'pg_dump', :to_type => 'gof3r', :to_url => to_url, + :started_at => started_at, :finished_at => finished_at_2, + :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffd', + :from_name => from_name, :to_name => 'PGBACKUPS BACKUP', :num => 2, + :from_type => 'pg_dump', :to_type => 'gof3r', :to_url => to_url, + :started_at => started_at, :finished_at => finished_at_1, + :options => { "pgbackups_name" => "b047" }, + :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffe', + :from_name => from_name, :to_name => 'BACKUP', :num => 3, + :from_type => 'pg_dump', :to_type => 'gof3r', :to_url => to_url, + :started_at => started_at, :finished_at => finished_at_3, + :succeeded => false } + ] + end + + let(:restore_info) do + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'gof3r', :to_type => 'pg_restore', num: 3, + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => true } + end + + before do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return(example_attachments) + stub_pgapp.transfers.returns(transfers) + end + + it "triggers a restore of the given backup" do + stub_pg.backups_restore(to_url).returns(restore_info) + stub_pgapp.transfers_get.returns(restore_info) + + stderr, stdout = execute("pg:backups restore b001 red --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/Restore completed/) + end + + it "defaults to the latest successful backup" do + stub_pg.backups_restore(to_url).returns(restore_info) + stub_pgapp.transfers_get.returns(restore_info) + + stderr, stdout = execute("pg:backups restore red --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/Restore completed/) + end + + it "refuses to restore a backup that did not complete successfully" do + stub_pg.backups_restore(to_url).returns(restore_info) + stub_pgapp.transfers_get.returns(restore_info) + + stderr, stdout = execute("pg:backups restore b003 red --confirm example") + expect(stderr).to match(/did not complete successfully/) + expect(stdout).to be_empty + end + + it "does not restore without confirmation" do + stderr, stdout = execute("pg:backups restore b001 red") + expect(stderr).to match(/Confirmation did not match example. Aborted./) + expect(stdout).to match(/WARNING: Destructive Action/) + expect(stdout).to match(/This command will affect the app: example/) + expect(stdout).to match(/To proceed, type "example" or re-run this command with --confirm example/) + end + end + + describe "heroku pg:backups public-url" do + let(:logged_at) { Time.now } + let(:started_at) { Time.now } + let(:finished_at) { Time.now } + let(:from_name) { 'RED' } + let(:source_size) { 42 } + let(:backup_size) { source_size / 2 } + + let(:logs) { [{ 'created_at' => logged_at, 'message' => "hello world" }] } + let(:transfers) do + [ + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_name => from_name, :to_name => 'BACKUP', + :num => 1, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffe', + :from_name => from_name, :to_name => 'BACKUP', + :num => 2, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :succeeded => true } + ] + end + let(:url1_info) do + { :url => 'https://example.com/my-backup', :expires_at => Time.now } + end + let(:url2_info) do + { :url => 'https://example.com/my-other-backup', :expires_at => Time.now } + end + + before do + stub_pgapp.transfers.returns(transfers) + stub_pgapp.transfers_public_url(1).returns(url1_info) + stub_pgapp.transfers_public_url(2).returns(url2_info) + end + + it "gets a public url for the specified backup" do + stderr, stdout = execute("pg:backups public-url b001") + expect(stdout).to include url1_info[:url] + expect(stdout).to match(/will expire at #{Regexp.quote(url1_info[:expires_at].to_s)}/) + end + + it "only prints the url if stdout is not a tty" do + fake_stdout = StringIO.new + stderr, stdout = execute("pg:backups public-url b001", { :stdout => fake_stdout }) + expect(stdout.chomp).to eq url1_info[:url] + end + + it "only prints the url if called with -q" do + stderr, stdout = execute("pg:backups public-url b001 -q") + expect(stdout.chomp).to eq url1_info[:url] + end + + it "defaults to the latest backup if none is specified" do + stderr, stdout = execute("pg:backups public-url") + expect(stdout).to include url2_info[:url] + expect(stdout).to match(/will expire at #{Regexp.quote(url2_info[:expires_at].to_s)}/) + end + end + + end +end diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index dd89b357b..23009b882 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -33,7 +33,7 @@ module Heroku::Command 'app' => {'name' => 'sushi'}, 'name' => 'HEROKU_POSTGRESQL_FOLLOW', 'config_var' => 'HEROKU_POSTGRESQL_FOLLOW_URL', - 'resource' => {'name' => 'whatever-somethign-2323', + 'resource' => {'name' => 'whatever-something-2323', 'value' => 'postgres://follow_database_url', 'type' => 'heroku-postgresql:ronin' }}) ]) @@ -48,8 +48,8 @@ module Heroku::Command stub_pg.reset stderr, stdout = execute("pg:reset RONIN --confirm example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Resetting HEROKU_POSTGRESQL_RONIN_URL... done STDOUT end @@ -58,15 +58,15 @@ module Heroku::Command stub_pg.reset stderr, stdout = execute("pg:reset RONIN") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Confirmation did not match example. Aborted. STDERR - stdout.should == " + expect(stdout).to eq(" ! WARNING: Destructive Action ! This command will affect the app: example ! To proceed, type \"example\" or re-run this command with --confirm example -> " +> ") end context "index" do @@ -83,8 +83,8 @@ module Heroku::Command ]) stderr, stdout = execute("pg") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === HEROKU_POSTGRESQL_FOLLOW_URL Plan: Ronin Status: available @@ -134,8 +134,8 @@ module Heroku::Command ]) stderr, stdout = execute("pg:info RONIN") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === HEROKU_POSTGRESQL_RONIN_URL Plan: Ronin Status: available @@ -152,22 +152,80 @@ module Heroku::Command end context "promotion" do - it "promotes the specified database" do + include Support::Addons + + before do + resource = build_addon( + name: "walking-slowly-42", + addon_service: { name: "heroku-postgresql" }, + plan: { name: "ronin" }, + app: { id: 1, name: "example" }) + + ronin = build_attachment( + name: "HEROKU_POSTGRESQL_RONIN", + app: { id: 1, name: "example" }, + addon: { id: resource[:id], name: "dreaming-ably-42" }) + + Excon.stub(method: :get, path: "/addons/#{resource[:id]}") do + { body: MultiJson.encode(resource), status: 200 } + end + + Excon.stub(method: :get, path: "/addons/#{resource[:name]}") do + { body: MultiJson.encode(resource), status: 200 } + end + + Excon.stub(method: :get, path: "/apps/example/addon-attachments/HEROKU_POSTGRESQL_RONIN") do + { body: MultiJson.encode(ronin), status: 200 } + end + + Excon.stub(method: :get, path: "/apps/example/addon-attachments/RONIN") do + { body: MultiJson.encode({}), status: 404 } + end + + Excon.stub(method: :get, path: "/apps/example/addon-attachments") do + { body: MultiJson.encode([ronin]), status: 200 } + end + + Excon.stub(method: :post, path: "/addon-attachments") do + database = ronin.merge(name: "DATABASE") + { body: MultiJson.encode(database), status: 201 } + end + end + + it "promotes the specified database resource name" do + stderr, stdout = execute("pg:promote walking-slowly-42 --confirm example") + expect(stderr).to eq("") + expect(stdout).to include <<-STDOUT +Promoting walking-slowly-42 to DATABASE_URL on example... done +STDOUT + expect(api.get_config_vars("example").body["DATABASE_URL"]).to eq("postgres://database_url") + end + + it "promotes the specified database by config var" do + stderr, stdout = execute("pg:promote HEROKU_POSTGRESQL_RONIN_URL --confirm example") + expect(stderr).to eq("") + expect(stdout).to include <<-STDOUT +Promoting walking-slowly-42 to DATABASE_URL on example... done +STDOUT + expect(api.get_config_vars("example").body["DATABASE_URL"]).to eq("postgres://database_url") + end + + it "promotes the specified database by attachment substring" do stderr, stdout = execute("pg:promote RONIN --confirm example") - stderr.should == "" - stdout.should == <<-STDOUT -Promoting HEROKU_POSTGRESQL_RONIN_URL to DATABASE_URL... done + expect(stderr).to eq("") + expect(stdout).to include <<-STDOUT +Promoting walking-slowly-42 to DATABASE_URL on example... done STDOUT - api.get_config_vars("example").body["DATABASE_URL"].should == "postgres://ronin_database_url" + expect(api.get_config_vars("example").body["DATABASE_URL"]).to eq("postgres://database_url") end it "fails if no database is specified" do stderr, stdout = execute("pg:promote") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku pg:promote DATABASE ! Must specify DATABASE to promote. STDERR - stdout.should == "" + expect(stdout).to eq("") end end @@ -175,8 +233,8 @@ module Heroku::Command it "resets credentials and promotes to DATABASE_URL if it's the main DB" do stub_pg.rotate_credentials stderr, stdout = execute("pg:credentials iv --reset") - stderr.should == '' - stdout.should == <<-STDOUT + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT Resetting credentials for HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)... done Promoting HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)... done STDOUT @@ -189,8 +247,8 @@ module Heroku::Command "HEROKU_POSTGRESQL_RESETME_URL" => "postgres://something_else" } stderr, stdout = execute("pg:credentials follo --reset") - stderr.should == '' - stdout.should_not include("Promoting") + expect(stderr).to eq('') + expect(stdout).not_to include("Promoting") end end @@ -198,9 +256,9 @@ module Heroku::Command context "unfollow" do it "sends request to unfollow" do hpg_client = double('Heroku::Client::HerokuPostgresql') - Heroku::Client::HerokuPostgresql.should_receive(:new).twice.and_return(hpg_client) - hpg_client.should_receive(:unfollow) - hpg_client.should_receive(:get_database).and_return( + expect(Heroku::Client::HerokuPostgresql).to receive(:new).twice.and_return(hpg_client) + expect(hpg_client).to receive(:unfollow) + expect(hpg_client).to receive(:get_database).and_return( :following => 'postgresql://user:pass@roninhost/database', :info => [ {"name"=>"Plan", "values"=>["Ronin"]}, @@ -215,8 +273,8 @@ module Heroku::Command ] ) stderr, stdout = execute("pg:unfollow HEROKU_POSTGRESQL_FOLLOW_URL --confirm example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ! HEROKU_POSTGRESQL_FOLLOW_URL will become writable and no longer ! follow Database on roninhost:5432/database. This cannot be undone. Unfollowing HEROKU_POSTGRESQL_FOLLOW_URL... done @@ -232,7 +290,7 @@ module Heroku::Command stub(pgc).color? { false } end Excon.stub({:method => :post, :path => '/reports'}, { - :body => Heroku::OkJson.encode({ + :body => MultiJson.dump({ 'id' => 'abc123', 'app' => 'appname', 'created_at' => '2014-06-24 01:26:11.941197+00', @@ -252,8 +310,8 @@ module Heroku::Command }) stderr, stdout = execute("pg:diagnose") - stderr.should == '' - stdout.should == <<-STDOUT + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT Report abc123 for appname::dbcolor available for one month after creation on 2014-06-24 01:26:11.941197+00 @@ -276,5 +334,79 @@ module Heroku::Command end end + describe '#push' do + context 'with remote and local dbs specified' do + let(:remote) { 'MY_HEROKU_DB_FUSCIA' } + let(:local) { 'MyLocalDb' } + + it 'executes dump restore with correct targets' do + pg = Heroku::Command::Pg.new + remote_url = "postgres://someurl.test/#{remote}" + local_url = "postgres:///#{local}" + dump_restore = double() + expect(pg).to receive(:resolve_heroku_url).and_return(remote_url) + expect(dump_restore).to receive(:execute) + expect(Heroku::Command).to receive(:shift_argument).and_return(local, remote) + expect(PgDumpRestore).to receive(:new).with(local_url, remote_url, pg).and_return(dump_restore) + + pg.push + end + end + + context 'with no databases specified' do + it 'displays help' do + pg = Heroku::Command::Pg.new + expect(pg).to receive(:current_command).and_return('push') + expect(Heroku::Command).to receive(:run).with('push', ['--help']) + + expect { pg.push }.to raise_error SystemExit + end + end + end + + describe '#pull' do + context 'with remote and local dbs specified' do + let(:remote) { 'MY_HEROKU_DB_FUSCIA' } + let(:local) { 'MyLocalDb' } + + it 'executes dump restore with correct targets' do + pg = Heroku::Command::Pg.new + remote_url = "postgres://someurl.test/#{remote}" + local_url = "postgres:///#{local}" + dump_restore = double() + expect(pg).to receive(:resolve_heroku_url).and_return(remote_url) + expect(dump_restore).to receive(:execute) + expect(Heroku::Command).to receive(:shift_argument).and_return(remote, local) + expect(PgDumpRestore).to receive(:new).with(remote_url, local_url, pg).and_return(dump_restore) + + pg.pull + end + + context 'with no databases specified' do + it 'displays help' do + pg = Heroku::Command::Pg.new + expect(pg).to receive(:current_command).and_return('pull') + expect(Heroku::Command).to receive(:run).with('pull', ['--help']) + + expect { pg.pull }.to raise_error SystemExit + end + end + end + end + + describe '#parse_db_url' do + it 'returns a local url when only database name is supplied' do + pg = Heroku::Command::Pg.new + parsed_url = pg.send(:parse_db_url, 'MyLocalDb') + expect(parsed_url).to eql 'postgres:///MyLocalDb' + end + + it 'returns the original path when a url is specified' do + url = 'postgres://user:password@server:1234/'.freeze + pg = Heroku::Command::Pg.new + parsed_url = pg.send(:parse_db_url, url) + expect(parsed_url).to eql url + end + end end end diff --git a/spec/heroku/command/pgbackups_spec.rb b/spec/heroku/command/pgbackups_spec.rb index ed44be4ea..5db07aa4d 100644 --- a/spec/heroku/command/pgbackups_spec.rb +++ b/spec/heroku/command/pgbackups_spec.rb @@ -7,10 +7,10 @@ module Heroku::Command api.post_app("name" => "example") stub_core stderr, stdout = execute("pgbackups:capture") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Your app has no databases. STDERR - stdout.should == "" + expect(stdout).to eq("") api.delete_app("example") end end @@ -18,7 +18,7 @@ module Heroku::Command describe Pgbackups do before do @pgbackups = prepare_command(Pgbackups) - @pgbackups.heroku.stub!(:info).and_return({}) + allow(@pgbackups.heroku).to receive(:info).and_return({}) api.post_app("name" => "example") api.put_config_vars( @@ -64,8 +64,8 @@ module Heroku::Command }]) stderr, stdout = execute("pgbackups") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ID Backup Time Status Size Database ---- ------------------------- --------- ---- -------- b001 2012-01-01 12:00:01 +0000 Capturing 1024 DATABASE @@ -77,7 +77,7 @@ module Heroku::Command let(:from_url) { "postgres://from/bar" } let(:attachment) { double('attachment', :display_name => from_name, :url => from_url ) } before do - @pgbackups.stub!(:resolve).and_return(attachment) + allow(@pgbackups).to receive(:resolve).and_return(attachment) end it "gets the url for the latest backup if nothing is specified" do @@ -85,34 +85,34 @@ module Heroku::Command stub_pgbackups.get_latest_backup.returns({"public_url" => "http://latest/backup.dump"}) old_stdout_isatty = STDOUT.isatty - $stdout.stub!(:isatty).and_return(true) + allow($stdout).to receive(:isatty).and_return(true) stderr, stdout = execute("pgbackups:url") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT http://latest/backup.dump STDOUT - $stdout.stub!(:isatty).and_return(old_stdout_isatty) + allow($stdout).to receive(:isatty).and_return(old_stdout_isatty) end it "gets the url for the named backup if a name is specified" do stub_pgbackups.get_backup.with("b001").returns({"public_url" => "http://latest/backup.dump" }) old_stdout_isatty = STDOUT.isatty - $stdout.stub!(:isatty).and_return(true) + allow($stdout).to receive(:isatty).and_return(true) stderr, stdout = execute("pgbackups:url b001") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT http://latest/backup.dump STDOUT - $stdout.stub!(:isatty).and_return(old_stdout_isatty) + allow($stdout).to receive(:isatty).and_return(old_stdout_isatty) end it "should capture a backup when requested" do backup_obj = {'to_url' => "s3://bucket/userid/b001.dump"} - @pgbackups.stub!(:args).and_return([]) - @pgbackups.stub!(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => nil}).and_return(backup_obj) - @pgbackups.stub!(:poll_transfer!).with(backup_obj).and_return(backup_obj) + allow(@pgbackups).to receive(:args).and_return([]) + allow(@pgbackups).to receive(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => nil}).and_return(backup_obj) + allow(@pgbackups).to receive(:poll_transfer!).with(backup_obj).and_return(backup_obj) @pgbackups.capture end @@ -120,9 +120,9 @@ module Heroku::Command it "should send expiration flag to client if specified on args" do backup_obj = {'to_url' => "s3://bucket/userid/b001.dump"} - @pgbackups.stub!(:options).and_return({:expire => true}) - @pgbackups.stub!(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => true}).and_return(backup_obj) - @pgbackups.stub!(:poll_transfer!).with(backup_obj).and_return(backup_obj) + allow(@pgbackups).to receive(:options).and_return({:expire => true}) + allow(@pgbackups).to receive(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => true}).and_return(backup_obj) + allow(@pgbackups).to receive(:poll_transfer!).with(backup_obj).and_return(backup_obj) @pgbackups.capture end @@ -130,11 +130,11 @@ module Heroku::Command it "destroys no backup without a name" do stub_core stderr, stdout = execute("pgbackups:destroy") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku pgbackups:destroy BACKUP_ID ! Must specify BACKUP_ID to destroy. STDERR - stdout.should == "" + expect(stdout).to eq("") end it "destroys a backup" do @@ -143,8 +143,8 @@ module Heroku::Command stub_pgbackups.delete_backup("b001").returns({}) stderr, stdout = execute("pgbackups:destroy b001") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Destroying b001... done STDOUT end @@ -172,11 +172,11 @@ def stub_failed_capture(log) it 'aborts on a generic error' do stub_failed_capture "something generic" stderr, stdout = execute("pgbackups:capture") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! An error occurred and your backup did not finish. ! Please run `heroku logs --ps pgbackups` for details. STDERR - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar @@ -187,12 +187,12 @@ def stub_failed_capture(log) it 'aborts and informs when the database isnt up yet' do stub_failed_capture 'could not translate host name "ec2-42-42-42-42.compute-1.amazonaws.com" to address: Name or service not known' stderr, stdout = execute("pgbackups:capture") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! An error occurred and your backup did not finish. ! Please run `heroku logs --ps pgbackups` for details. ! The database is not yet online. Please try again. STDERR - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar @@ -203,12 +203,12 @@ def stub_failed_capture(log) it 'aborts and informs when the credentials are incorrect' do stub_failed_capture 'psql: FATAL: database "randomname" does not exist' stderr, stdout = execute("pgbackups:capture") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! An error occurred and your backup did not finish. ! Please run `heroku logs --ps pgbackups` for details. ! The database credentials are incorrect. STDERR - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar @@ -221,24 +221,22 @@ def stub_failed_capture(log) context "restore" do let(:attachment) { double('attachment', :display_name => 'someconfigvar', :url => 'postgres://fromhost/database') } before do - from_name, from_url = "FROM_NAME", "postgres://fromhost/database" - - @pgbackups_client = RSpec::Mocks::Mock.new("pgbackups_client") # avoid double r mock - @pgbackups.stub!(:pgbackup_client).and_return(@pgbackups_client) + @pgbackups_client = double("pgbackups_client") + allow(@pgbackups).to receive(:pgbackup_client).and_return(@pgbackups_client) end it "should receive a confirm_command on restore" do - @pgbackups_client.stub!(:get_latest_backup) { {"to_url" => "s3://bucket/user/bXXX.dump"} } + allow(@pgbackups_client).to receive(:get_latest_backup) { {"to_url" => "s3://bucket/user/bXXX.dump"} } - @pgbackups.should_receive(:confirm_command).and_return(false) - @pgbackups_client.should_not_receive(:transfer!) + expect(@pgbackups).to receive(:confirm_command).and_return(false) + expect(@pgbackups_client).not_to receive(:transfer!) @pgbackups.restore end it "aborts if no database addon is present" do - @pgbackups.should_receive(:resolve).and_raise(SystemExit) - lambda { @pgbackups.restore }.should raise_error(SystemExit) + expect(@pgbackups).to receive(:resolve).and_raise(SystemExit) + expect { @pgbackups.restore }.to raise_error(SystemExit) end context "for commands which perform restores" do @@ -250,13 +248,13 @@ def stub_failed_capture(log) "from_name" => "postgres://databasehost/dbname" } - @pgbackups.stub!(:confirm_command).and_return(true) - @pgbackups_client.should_receive(:create_transfer).and_return(@backup_obj) - @pgbackups.stub!(:poll_transfer!).and_return(@backup_obj) + allow(@pgbackups).to receive(:confirm_command).and_return(true) + expect(@pgbackups_client).to receive(:create_transfer).and_return(@backup_obj) + allow(@pgbackups).to receive(:poll_transfer!).and_return(@backup_obj) end it "should default to the latest backup" do - @pgbackups.stub(:args).and_return([]) + allow(@pgbackups).to receive(:args).and_return([]) mock(@pgbackups_client).get_latest_backup.returns(@backup_obj) @pgbackups.restore end @@ -265,28 +263,28 @@ def stub_failed_capture(log) it "should restore the named backup" do name = "backupname" args = ['DATABASE', name] - @pgbackups.stub(:args).and_return(args) - @pgbackups.stub(:shift_argument).and_return(*args) - @pgbackups.stub(:resolve).and_return(attachment) + allow(@pgbackups).to receive(:args).and_return(args) + allow(@pgbackups).to receive(:shift_argument).and_return(*args) + allow(@pgbackups).to receive(:resolve).and_return(attachment) mock(@pgbackups_client).get_backup.with(name).returns(@backup_obj) @pgbackups.restore end it "should handle external restores" do args = ['db_name_gets_shifted_out_in_resolve_db', 'http://external/file.dump'] - @pgbackups.stub(:args).and_return(args) - @pgbackups.stub(:shift_argument).and_return(*args) - @pgbackups.stub(:resolve).and_return(attachment) - @pgbackups_client.should_not_receive(:get_backup) - @pgbackups_client.should_not_receive(:get_latest_backup) + allow(@pgbackups).to receive(:args).and_return(args) + allow(@pgbackups).to receive(:shift_argument).and_return(*args) + allow(@pgbackups).to receive(:resolve).and_return(attachment) + expect(@pgbackups_client).not_to receive(:get_backup) + expect(@pgbackups_client).not_to receive(:get_latest_backup) @pgbackups.restore end end context "on errors" do before(:each) do - @pgbackups_client.stub!(:get_latest_backup => {"to_url" => "s3://bucket/user/bXXX.dump"} ) - @pgbackups.stub!(:confirm_command => true) + allow(@pgbackups_client).to receive(:get_latest_backup).and_return("to_url" => "s3://bucket/user/bXXX.dump") + allow(@pgbackups).to receive(:confirm_command).and_return(true) end def stub_error_backup_with_log(log) @@ -295,19 +293,19 @@ def stub_error_backup_with_log(log) "log" => log } - @pgbackups_client.should_receive(:create_transfer) { @backup_obj } - @pgbackups.stub!(:poll_transfer!) { @backup_obj } + expect(@pgbackups_client).to receive(:create_transfer) { @backup_obj } + allow(@pgbackups).to receive(:poll_transfer!) { @backup_obj } end it 'aborts for a generic error' do stub_error_backup_with_log 'something generic' - @pgbackups.should_receive(:error).with("An error occurred and your restore did not finish.\nPlease run `heroku logs --ps pgbackups` for details.") + expect(@pgbackups).to receive(:error).with("An error occurred and your restore did not finish.\nPlease run `heroku logs --ps pgbackups` for details.") @pgbackups.restore end it 'aborts and informs for expired s3 urls' do stub_error_backup_with_log 'Invalid dump format: /tmp/aDMyoXPrAX/b031.dump: XML document text' - @pgbackups.should_receive(:error).with { |message| message.should =~ /backup url is invalid/ } + expect(@pgbackups).to receive(:error).with(/backup url is invalid/) @pgbackups.restore end end diff --git a/spec/heroku/command/plugins_spec.rb b/spec/heroku/command/plugins_spec.rb index d45c03fdc..ef39d23ed 100644 --- a/spec/heroku/command/plugins_spec.rb +++ b/spec/heroku/command/plugins_spec.rb @@ -13,52 +13,25 @@ module Heroku::Command context("install") do before do - Heroku::Plugin.should_receive(:new).with('git://github.com/heroku/Plugin.git').and_return(@plugin) - @plugin.should_receive(:install).and_return(true) + expect(Heroku::Plugin).to receive(:new).with('git://github.com/heroku/Plugin.git').and_return(@plugin) + expect(@plugin).to receive(:install).and_return(true) end it "installs plugins" do - Heroku::Plugin.should_receive(:load_plugin).and_return(true) + expect(Heroku::Plugin).to receive(:load_plugin).and_return(true) stderr, stdout = execute("plugins:install git://github.com/heroku/Plugin.git") - stderr.should == "" - stdout.should == <<-STDOUT -Installing Plugin... done + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Installing git://github.com/heroku/Plugin.git... done STDOUT end it "does not install plugins that do not load" do - Heroku::Plugin.should_receive(:load_plugin).and_return(false) - @plugin.should_receive(:uninstall).and_return(true) + expect(Heroku::Plugin).to receive(:load_plugin).and_return(false) + expect(@plugin).to receive(:uninstall).and_return(true) stderr, stdout = execute("plugins:install git://github.com/heroku/Plugin.git") - stderr.should == '' # normally would have error, but mocks/stubs don't allow - stdout.should == "Installing Plugin... " # also inaccurate, would end in ' failed' - end - - end - - context("uninstall") do - - before do - Heroku::Plugin.should_receive(:new).with('Plugin').and_return(@plugin) - end - - it "uninstalls plugins" do - @plugin.should_receive(:uninstall).and_return(true) - stderr, stdout = execute("plugins:uninstall Plugin") - stderr.should == "" - stdout.should == <<-STDOUT -Uninstalling Plugin... done -STDOUT - end - - it "does not uninstall plugins that do not exist" do - stderr, stdout = execute("plugins:uninstall Plugin") - stderr.should == <<-STDERR - ! Plugin plugin not found. -STDERR - stdout.should == <<-STDOUT -Uninstalling Plugin... failed -STDOUT + expect(stderr).to eq('') # normally would have error, but mocks/stubs don't allow + expect(stdout).to eq("Installing git://github.com/heroku/Plugin.git... ") # also inaccurate, would end in ' failed' end end @@ -66,34 +39,34 @@ module Heroku::Command context("update") do before do - Heroku::Plugin.should_receive(:new).with('Plugin').and_return(@plugin) + expect(Heroku::Plugin).to receive(:new).with('Plugin').and_return(@plugin) end it "updates plugin by name" do - @plugin.should_receive(:update).and_return(true) + expect(@plugin).to receive(:update).and_return(true) stderr, stdout = execute("plugins:update Plugin") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Updating Plugin... done STDOUT end it "updates all plugins" do - Heroku::Plugin.stub(:list).and_return(['Plugin']) - @plugin.should_receive(:update).and_return(true) + allow(Heroku::Plugin).to receive(:list).and_return(['Plugin']) + expect(@plugin).to receive(:update).and_return(true) stderr, stdout = execute("plugins:update") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Updating Plugin... done STDOUT end it "does not update plugins that do not exist" do stderr, stdout = execute("plugins:update Plugin") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Plugin plugin not found. STDERR - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Updating Plugin... failed STDOUT end diff --git a/spec/heroku/command/ps_spec.rb b/spec/heroku/command/ps_spec.rb index 3949a4b5d..e126500bf 100644 --- a/spec/heroku/command/ps_spec.rb +++ b/spec/heroku/command/ps_spec.rb @@ -18,11 +18,11 @@ end it "ps:dynos errors out on cedar apps" do - lambda { execute("ps:dynos") }.should raise_error(Heroku::Command::CommandFailed, "For Cedar apps, use `heroku ps`") + expect { execute("ps:dynos") }.to raise_error(Heroku::Command::CommandFailed, "For Cedar apps, use `heroku ps`") end it "ps:workers errors out on cedar apps" do - lambda { execute("ps:workers") }.should raise_error(Heroku::Command::CommandFailed, "For Cedar apps, use `heroku ps`") + expect { execute("ps:workers") }.to raise_error(Heroku::Command::CommandFailed, "For Cedar apps, use `heroku ps`") end describe "ps" do @@ -44,10 +44,14 @@ end.to_json, :status => 200 ) - Heroku::Command::Ps.any_instance.should_receive(:time_ago).exactly(10).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") + Excon.stub( + { :method => :post, :path => "/apps/example/actions/get-quota" }, + :status => 404 + ) + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).exactly(10).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === web (1X): `bundle exec thin start -p $PORT` web.1: created 2012/09/11 12:34:56 (~ 0s ago) web.2: created 2012/09/11 12:34:56 (~ 0s ago) @@ -80,10 +84,14 @@ end.to_json, :status => 200 ) - Heroku::Command::Ps.any_instance.should_receive(:time_ago).twice.and_return('2012/09/11 12:34:56 (~ 0s ago)') + Excon.stub( + { :method => :post, :path => "/apps/example/actions/get-quota" }, + :status => 404 + ) + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return('2012/09/11 12:34:56 (~ 0s ago)') stderr, stdout = execute("ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === run: one-off processes run.1 (1X): created 2012/09/11 12:34:56 (~ 0s ago): `bash` run.2 (1X): created 2012/09/11 12:34:56 (~ 0s ago): `bash` @@ -108,11 +116,15 @@ end.to_json, :status => 200 ) - Heroku::Command::Ps.any_instance.should_receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") + Excon.stub( + { :method => :post, :path => "/apps/example/actions/get-quota" }, + :status => 404 + ) + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === web (2X): `bundle exec thin start -p $PORT` web.1: created 2012/09/11 12:34:56 (~ 0s ago) web.2: created 2012/09/11 12:34:56 (~ 0s ago) @@ -137,11 +149,15 @@ end.to_json, :status => 200 ) - Heroku::Command::Ps.any_instance.should_receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") + Excon.stub( + { :method => :post, :path => "/apps/example/actions/get-quota" }, + :status => 404 + ) + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === web (PX): `bundle exec thin start -p $PORT` web.1: created 2012/09/11 12:34:56 (~ 0s ago) web.2: created 2012/09/11 12:34:56 (~ 0s ago) @@ -167,10 +183,14 @@ end.to_json, :status => 200 ) - Heroku::Command::Ps.any_instance.should_receive(:time_ago).exactly(4).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") + Excon.stub( + { :method => :post, :path => "/apps/example/actions/get-quota" }, + :status => 404 + ) + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).exactly(4).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === run: one-off processes run.1 (PX): created 2012/09/11 12:34:56 (~ 0s ago): `bash` run.2 (2X): created 2012/09/11 12:34:56 (~ 0s ago): `bash` @@ -183,28 +203,67 @@ end + it "displays how much run-time is left if the application has quota (seconds)" do + allow_until = (Time.now + 30).getutc + Excon.stub( + { :method => :get, :path => "/apps/example/dynos" }, + :body => 1.times.map do |i| + { + "size" => "1X", + "updated_at" => "2012-09-11T12:34:56Z", + "command" => "bundle exec thin start -p $PORT", + "created_at" => "2012-09-11T12:30:56Z", + "id" => "a94d0fa2-8509-4dab-8742-be7bfe768ecc", + "name" => "web.#{i+1}", + "state" => "up", + "type" => "web" + } + end.to_json, + :status => 200 + ) + Excon.stub( + { :method => :post, :path => "/apps/example/actions/get-quota" }, + :body => + { + "allow_until" => allow_until.iso8601, + "deny_until" => nil, + }.to_json, + :status => 200 + ) + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).once.times.and_return("2012/09/11 12:34:56 (~ 0s ago)") + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_remaining).and_return("20s") + stderr, stdout = execute("ps") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Free quota left: 20s +=== web (1X): `bundle exec thin start -p $PORT` +web.1: up 2012/09/11 12:34:56 (~ 0s ago) + +STDOUT + end + describe "ps:restart" do it "restarts all dynos with no args" do stderr, stdout = execute("ps:restart") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Restarting dynos... done STDOUT end it "restarts one dyno" do stderr, stdout = execute("ps:restart web.1") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Restarting web.1 dyno... done STDOUT end it "restarts a type of dyno" do stderr, stdout = execute("ps:restart web") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Restarting web dynos... done STDOUT end @@ -218,8 +277,8 @@ { :body => [{"quantity" => "5", "size" => "1X", "type" => "web"}], :status => 200}) stderr, stdout = execute("ps:scale web=5") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Scaling dynos... done, now running web at 5:1X. STDOUT end @@ -229,8 +288,8 @@ { :body => [{"quantity" => "3", "size" => "1X", "type" => "web"}], :status => 200}) stderr, stdout = execute("ps:scale web+2") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Scaling dynos... done, now running web at 3:1X. STDOUT end @@ -247,8 +306,8 @@ :status => 200 ) stderr, stdout = execute("ps:scale web=4:2X") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Scaling dynos... done, now running web at 4:2X. STDOUT end @@ -272,8 +331,8 @@ :status => 200 ) stderr, stdout = execute("ps:scale web=4:1X worker=2:2x") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Scaling dynos... done, now running web at 4:1X, worker at 2:2X. STDOUT end @@ -290,8 +349,8 @@ :status => 200 ) stderr, stdout = execute("ps:scale web=4:PX") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Scaling dynos... done, now running web at 4:PX. STDOUT end @@ -311,10 +370,10 @@ :status => 200 ) stderr, stdout = execute("ps:resize web=2X") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Resizing and restarting the specified dynos... done -web dynos now 2X ($0.10/dyno-hour) +web dynos now 2X ($72/month) STDOUT end @@ -330,17 +389,17 @@ }.to_json }, :body => [ - {"quantity" => 2, "size" => "4X", "type" => "web"}, + {"quantity" => 2, "size" => "1X", "type" => "web"}, {"quantity" => 1, "size" => "2X", "type" => "worker"} ], :status => 200 ) stderr, stdout = execute("ps:resize web=4x worker=2X") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Resizing and restarting the specified dynos... done -web dynos now 4X ($0.20/dyno-hour) -worker dynos now 2X ($0.10/dyno-hour) +web dynos now 1X ($36/month) +worker dynos now 2X ($72/month) STDOUT end @@ -362,11 +421,11 @@ :status => 200 ) stderr, stdout = execute("ps:resize web=PX worker=Px") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Resizing and restarting the specified dynos... done -web dynos now PX ($0.80/dyno-hour) -worker dynos now PX ($0.80/dyno-hour) +web dynos now PX ($576/month) +worker dynos now PX ($576/month) STDOUT end @@ -376,16 +435,16 @@ it "restarts one dyno" do stderr, stdout = execute("ps:restart ps.1") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Restarting ps.1 dyno... done STDOUT end it "restarts a type of dyno" do stderr, stdout = execute("ps:restart ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Restarting ps dynos... done STDOUT end @@ -408,8 +467,8 @@ it "displays the current number of dynos" do stderr, stdout = execute("ps:dynos") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ~ `heroku ps:dynos QTY` has been deprecated and replaced with `heroku ps:scale dynos=QTY` example is running 0 dynos STDOUT @@ -417,8 +476,8 @@ it "sets the number of dynos" do stderr, stdout = execute("ps:dynos 5") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ~ `heroku ps:dynos QTY` has been deprecated and replaced with `heroku ps:scale dynos=QTY` Scaling dynos... done, now running 5 STDOUT @@ -430,8 +489,8 @@ it "displays the current number of workers" do stderr, stdout = execute("ps:workers") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ~ `heroku ps:workers QTY` has been deprecated and replaced with `heroku ps:scale workers=QTY` example is running 0 workers STDOUT @@ -439,8 +498,8 @@ it "sets the number of workers" do stderr, stdout = execute("ps:workers 5") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ~ `heroku ps:workers QTY` has been deprecated and replaced with `heroku ps:scale workers=QTY` Scaling workers... done, now running 5 STDOUT diff --git a/spec/heroku/command/releases_spec.rb b/spec/heroku/command/releases_spec.rb index 8e600a8ec..fb75c5db9 100644 --- a/spec/heroku/command/releases_spec.rb +++ b/spec/heroku/command/releases_spec.rb @@ -23,10 +23,10 @@ end it "should list releases" do - Heroku::Command::Releases.any_instance.should_receive(:time_ago).exactly(5).times.and_return('2012/09/10 11:36:44 (~ 0s ago)', '2012/09/10 11:36:43 (~ 1s ago)', '2012/09/10 11:35:44 (~ 1m ago)', '2012/09/10 10:36:44 (~ 1h ago)', '2012/01/02 12:34:56') + expect_any_instance_of(Heroku::Command::Releases).to receive(:time_ago).exactly(5).times.and_return('2012/09/10 11:36:44 (~ 0s ago)', '2012/09/10 11:36:43 (~ 1s ago)', '2012/09/10 11:35:44 (~ 1m ago)', '2012/09/10 10:36:44 (~ 1h ago)', '2012/01/02 12:34:56') @stderr, @stdout = execute("releases") - @stderr.should == "" - @stdout.should == <<-STDOUT + expect(@stderr).to eq("") + expect(@stdout).to eq <<-STDOUT === example Releases v5 Config add SUPER_LONG_CONFIG_VAR_TO_GE.. email@example.com 2012/09/10 11:36:44 (~ 0s ago) v4 Config add QUX_QUUX email@example.com 2012/09/10 11:36:43 (~ 1s ago) @@ -38,10 +38,10 @@ end it "should list a specified number of releases" do - Heroku::Command::Releases.any_instance.should_receive(:time_ago).exactly(3).times.and_return('2012/09/10 11:36:44 (~ 0s ago)', '2012/09/10 11:36:43 (~ 1s ago)', '2012/09/10 11:35:44 (~ 1m ago)') + expect_any_instance_of(Heroku::Command::Releases).to receive(:time_ago).exactly(3).times.and_return('2012/09/10 11:36:44 (~ 0s ago)', '2012/09/10 11:36:43 (~ 1s ago)', '2012/09/10 11:35:44 (~ 1m ago)') @stderr, @stdout = execute("releases -n 3") - @stderr.should == "" - @stdout.should == <<-STDOUT + expect(@stderr).to eq("") + expect(@stdout).to eq <<-STDOUT === example Releases v5 Config add SUPER_LONG_CONFIG_VAR_TO_GE.. email@example.com 2012/09/10 11:36:44 (~ 0s ago) v4 Config add QUX_QUUX email@example.com 2012/09/10 11:36:43 (~ 1s ago) @@ -63,17 +63,17 @@ it "requires a release to be specified" do stderr, stdout = execute("releases:info") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku releases:info RELEASE STDERR - stdout.should == "" + expect(stdout).to eq("") end it "shows info for a single release" do - Heroku::Command::Releases.any_instance.should_receive(:time_ago).and_return("2012/09/11 12:34:56 (~ 0s ago)") + expect_any_instance_of(Heroku::Command::Releases).to receive(:time_ago).and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("releases:info v1") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === Release v1 By: email@example.com Change: Config add FOO_BAR @@ -88,10 +88,10 @@ end it "shows info for a single release in shell compatible format" do - Heroku::Command::Releases.any_instance.should_receive(:time_ago).and_return("2012/09/11 12:34:56 (~ 0s ago)") + expect_any_instance_of(Heroku::Command::Releases).to receive(:time_ago).and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("releases:info v1 --shell") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === Release v1 By: email@example.com Change: Config add FOO_BAR @@ -120,16 +120,16 @@ it "rolls back to the latest release with no argument" do stderr, stdout = execute("releases:rollback") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Rolling back example... done, v2 STDOUT end it "rolls back to the specified release" do stderr, stdout = execute("releases:rollback v1") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Rolling back example... done, v1 STDOUT end diff --git a/spec/heroku/command/run_spec.rb b/spec/heroku/command/run_spec.rb index 80a45dc84..ba47a7c75 100644 --- a/spec/heroku/command/run_spec.rb +++ b/spec/heroku/command/run_spec.rb @@ -20,8 +20,8 @@ stub_rendezvous.start { $stdout.puts "output" } stderr, stdout = execute("run bin/foo") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Running `bin/foo` attached to terminal... up, run.1 output STDOUT @@ -31,10 +31,10 @@ describe "run:detached" do it "runs a command detached" do stderr, stdout = execute("run:detached bin/foo") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Running `bin/foo` detached... up, run.1 -Use `heroku logs -p run.1` to view the output. +Use `heroku logs -p run.1 -a example` to view the output. STDOUT end @@ -52,8 +52,8 @@ stub_rendezvous.start { $stdout.puts("rake_output") } stderr, stdout = execute("run:rake foo") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT WARNING: `heroku run:rake` has been deprecated. Please use `heroku run rake` instead. Running `rake foo` attached to terminal... up, run.1 rake_output @@ -64,8 +64,8 @@ stub_rendezvous.start { $stdout.puts("rake_output") } stderr, stdout = execute("rake foo") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT WARNING: `heroku rake` has been deprecated. Please use `heroku run rake` instead. Running `rake foo` attached to terminal... up, run.1 rake_output @@ -76,8 +76,8 @@ describe "run:console" do it "has been removed" do stderr, stdout = execute("run:console") - stderr.should == "" - stdout.should =~ /has been removed/ + expect(stderr).to eq("") + expect(stdout).to match(/has been removed/) end end end diff --git a/spec/heroku/command/sharing_spec.rb b/spec/heroku/command/sharing_spec.rb index 029696226..080506f82 100644 --- a/spec/heroku/command/sharing_spec.rb +++ b/spec/heroku/command/sharing_spec.rb @@ -18,8 +18,8 @@ module Heroku::Command it "lists collaborators" do api.post_collaborator("example", "collaborator@example.com") stderr, stdout = execute("sharing") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Access List collaborator@example.com collaborator email@example.com collaborator @@ -31,8 +31,8 @@ module Heroku::Command it "adds collaborators with default access to view only" do stderr, stdout = execute("sharing:add collaborator@example.com") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Adding collaborator@example.com to example as collaborator... done STDOUT end @@ -40,8 +40,8 @@ module Heroku::Command it "removes collaborators" do api.post_collaborator("example", "collaborator@example.com") stderr, stdout = execute("sharing:remove collaborator@example.com") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Removing collaborator@example.com from example collaborators... done STDOUT end @@ -49,8 +49,8 @@ module Heroku::Command it "transfers ownership" do api.post_collaborator("example", "collaborator@example.com") stderr, stdout = execute("sharing:transfer collaborator@example.com") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Transferring example to collaborator@example.com... done STDOUT end diff --git a/spec/heroku/command/stack_spec.rb b/spec/heroku/command/stack_spec.rb index 7aba8d710..49bc5cea3 100644 --- a/spec/heroku/command/stack_spec.rb +++ b/spec/heroku/command/stack_spec.rb @@ -15,12 +15,12 @@ module Heroku::Command it "index should provide list" do stderr, stdout = execute("stack") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Available Stacks aspen-mri-1.8.6 bamboo-ree-1.8.7 - cedar (beta) + cedar-10 (beta) * bamboo-mri-1.9.2 STDOUT @@ -28,8 +28,8 @@ module Heroku::Command it "migrate should succeed" do stderr, stdout = execute("stack:migrate bamboo-ree-1.8.7") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Stack set. Next release on example will use bamboo-ree-1.8.7. Run `git push heroku master` to create a new release on bamboo-ree-1.8.7. STDOUT diff --git a/spec/heroku/command/status_spec.rb b/spec/heroku/command/status_spec.rb deleted file mode 100644 index 9ab629a5a..000000000 --- a/spec/heroku/command/status_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require "spec_helper" -require "heroku/command/status" - -module Heroku::Command - describe Status do - - before(:each) do - stub_core - end - - it "displays status information" do - Excon.stub( - { - :host => 'status.heroku.com', - :method => :get, - :path => '/api/v3/current-status.json' - }, - { - :body => Heroku::OkJson.encode({"status"=>{"Production"=>"red", "Development"=>"red"}, "issues"=>[{"created_at"=>"2012-06-07T15:55:51Z", "id"=>372, "resolved"=>false, "title"=>"HTTP Routing Errors", "updated_at"=>"2012-06-07T16:14:37Z", "href"=>"https://status.heroku.com/api/v3/issues/372", "updates"=>[{"contents"=>"The number of applications seeing H99 errors is continuing to decrease as we continue to work toward a full resolution of the HTTP routing issues. The API is back online now as well. ", "created_at"=>"2012-06-07T17:47:26Z", "id"=>1088, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T17:47:26Z"}, {"contents"=>"Our engineers are continuing to work toward a full resolution of the HTTP routing issues. The API is currently in maintenance mode intentionally as we restore application operations. ", "created_at"=>"2012-06-07T17:16:40Z", "id"=>1086, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T17:26:55Z"}, {"contents"=>"Most applications are back online at this time. Our engineers are working on getting the remaining apps back online. ", "created_at"=>"2012-06-07T16:50:21Z", "id"=>1085, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:50:21Z"}, {"contents"=>"Our routing engineers have pushed out a patch to our routing tier. The platform is recovering and applications are coming back online. Our engineers are continuing to fully restore service.", "created_at"=>"2012-06-07T16:36:37Z", "id"=>1084, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:36:37Z"}, {"contents"=>"We have identified an issue with our routers that is causing errors on HTTP requests to applications. Engineers are working to resolve the issue.\r\n", "created_at"=>"2012-06-07T16:15:25Z", "id"=>1083, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:15:28Z"}, {"contents"=>"We have confirmed widespread errors on the platform. Our engineers are continuing to investigate.\r\n", "created_at"=>"2012-06-07T15:58:56Z", "id"=>1082, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T15:58:58Z"}, {"contents"=>"Our automated systems have detected potential platform errors. We are investigating.\r\n", "created_at"=>"2012-06-07T15:55:51Z", "id"=>1081, "incident_id"=>372, "status_dev"=>"yellow", "status_prod"=>"yellow", "update_type"=>"issue", "updated_at"=>"2012-06-07T15:55:55Z"}]}]}), - :status => 200 - } - ) - - Heroku::Command::Status.any_instance.should_receive(:time_ago).and_return('2012/09/11 09:34:56 (~ 3h ago)', '2012/09/11 12:33:56 (~ 1m ago)', '2012/09/11 12:29:56 (~ 5m ago)', '2012/09/11 12:24:56 (~ 10m ago)', '2012/09/11 12:04:56 (~ 30m ago)', '2012/09/11 11:34:56 (~ 1h ago)', '2012/09/11 10:34:56 (~ 2h ago)', '2012/09/11 09:34:56 (~ 3h ago)') - - stderr, stdout = execute("status") - stderr.should == '' - stdout.should == <<-STDOUT -=== Heroku Status -Development: red -Production: red - -=== HTTP Routing Errors 2012/09/11 09:34:56 (~ 3h+) -2012/09/11 12:33:56 (~ 1m ago) update The number of applications seeing H99 errors is continuing to decrease as we continue to work toward a full resolution of the HTTP routing issues. The API is back online now as well. -2012/09/11 12:29:56 (~ 5m ago) update Our engineers are continuing to work toward a full resolution of the HTTP routing issues. The API is currently in maintenance mode intentionally as we restore application operations. -2012/09/11 12:24:56 (~ 10m ago) update Most applications are back online at this time. Our engineers are working on getting the remaining apps back online. -2012/09/11 12:04:56 (~ 30m ago) update Our routing engineers have pushed out a patch to our routing tier. The platform is recovering and applications are coming back online. Our engineers are continuing to fully restore service. -2012/09/11 11:34:56 (~ 1h ago) update We have identified an issue with our routers that is causing errors on HTTP requests to applications. Engineers are working to resolve the issue. -2012/09/11 10:34:56 (~ 2h ago) update We have confirmed widespread errors on the platform. Our engineers are continuing to investigate. -2012/09/11 09:34:56 (~ 3h ago) issue Our automated systems have detected potential platform errors. We are investigating. - -STDOUT - - Excon.stubs.shift - end - - end -end diff --git a/spec/heroku/command/version_spec.rb b/spec/heroku/command/version_spec.rb index cdd8c09bf..28ce58684 100644 --- a/spec/heroku/command/version_spec.rb +++ b/spec/heroku/command/version_spec.rb @@ -6,9 +6,10 @@ module Heroku::Command it "shows version info" do stderr, stdout = execute("version") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT #{Heroku.user_agent} +You have no installed plugins. STDOUT end diff --git a/spec/heroku/command_spec.rb b/spec/heroku/command_spec.rb index ff3e277ab..d1f0ea38c 100644 --- a/spec/heroku/command_spec.rb +++ b/spec/heroku/command_spec.rb @@ -23,6 +23,7 @@ def to_s } describe "when the command requires confirmation" do + include Support::Addons let(:response_that_requires_confirmation) do {:status => 423, @@ -30,6 +31,16 @@ def to_s :body => 'terms of service required'} end + before do + Excon.stub(method: :post, path: %r(/apps/[^/]+/addons)) do |args| + { body: MultiJson.encode(build_addon(name: "my_addon", app: { name: "example" })), status: 201 } + end + end + + after do + Excon.stubs.shift + end + context "when the app is unknown" do context "and the user includes --confirm APP" do it "should set --app to APP and not ask for confirmation" do @@ -41,9 +52,9 @@ def to_s context "and the user includes --confirm APP --app APP2" do it "should warn that the app and confirm do not match and not continue" do - capture_stderr do + expect(capture_stderr do run "addons:add my_addon --confirm APP --app APP2" - end.should == " ! Mismatch between --app and --confirm\n" + end).to eq(" ! Mismatch between --app and --confirm\n") end end end @@ -65,10 +76,16 @@ def to_s context "and the user includes --confirm APP" do it "should set --app to APP and not ask for confirmation" do - stub_request(:post, %r{apps/example/addons/my_addon$}). - with(:body => {:confirm => 'example'}) + addon = build_addon(name: "my_addon", app: { name: "example" }) + + Excon.stub(method: :post, path: %r(/apps/example/addons)) { |args| + expect(args[:body]).to include '"confirm":"example"' + { body: MultiJson.encode(build_addon(name: "my_addon", app: { name: "example" })), status: 201 } + } run "addons:add my_addon --confirm example" + + Excon.stubs.shift end end @@ -83,11 +100,11 @@ def to_s end it "should not continue if the confirmation does not match" do - Heroku::Command.stub(:current_options).and_return(:confirm => 'not_example') + allow(Heroku::Command).to receive(:current_options).and_return(:confirm => 'not_example') - lambda do + expect do Heroku::Command.confirm_command('example') - end.should raise_error(Heroku::Command::CommandFailed) + end.to raise_error(Heroku::Command::CommandFailed) end it "should not continue if the user doesn't confirm" do @@ -103,42 +120,52 @@ def to_s end describe "parsing errors" do + before do + Excon.stub(method: :post, path: %r(/apps/example/addons)) { |args| + { body: MultiJson.encode(build_addon(name: "my_addon", app: { name: "example" })), status: 201 } + } + end + + after do + Excon.stubs.shift + end + it "extracts error messages from response when available in XML" do - Heroku::Command.extract_error('Invalid app name').should == 'Invalid app name' + expect(Heroku::Command.extract_error('Invalid app name')).to eq('Invalid app name') end it "extracts error messages from response when available in JSON" do - Heroku::Command.extract_error("{\"error\":\"Invalid app name\"}").should == 'Invalid app name' + expect(Heroku::Command.extract_error("{\"error\":\"Invalid app name\"}")).to eq('Invalid app name') end it "extracts error messages from response when available in plain text" do response = FakeResponse.new(:body => "Invalid app name", :headers => { :content_type => "text/plain; charset=UTF8" }) - Heroku::Command.extract_error(response).should == 'Invalid app name' + expect(Heroku::Command.extract_error(response)).to eq('Invalid app name') end it "shows Internal Server Error when the response doesn't contain a XML or JSON" do - Heroku::Command.extract_error('

HTTP 500

').should == "Internal server error.\nRun `heroku status` to check for known platform issues." + expect(Heroku::Command.extract_error('

HTTP 500

')).to eq("Internal server error.\nRun `heroku status` to check for known platform issues.") end it "shows Internal Server Error when the response is not plain text" do response = FakeResponse.new(:body => "Foobar", :headers => { :content_type => "application/xml" }) - Heroku::Command.extract_error(response).should == "Internal server error.\nRun `heroku status` to check for known platform issues." + expect(Heroku::Command.extract_error(response)).to eq("Internal server error.\nRun `heroku status` to check for known platform issues.") end it "allows a block to redefine the default error" do - Heroku::Command.extract_error("Foobar") { "Ok!" }.should == 'Ok!' + expect(Heroku::Command.extract_error("Foobar") { "Ok!" }).to eq('Ok!') end it "doesn't format the response if set to raw" do - Heroku::Command.extract_error("Foobar", :raw => true) { "Ok!" }.should == 'Ok!' + expect(Heroku::Command.extract_error("Foobar", :raw => true) { "Ok!" }).to eq('Ok!') end it "handles a nil body in parse_error_xml" do - lambda { Heroku::Command.parse_error_xml(nil) }.should_not raise_error + expect { Heroku::Command.parse_error_xml(nil) }.not_to raise_error end it "handles a nil body in parse_error_json" do - lambda { Heroku::Command.parse_error_json(nil) }.should_not raise_error + expect { Heroku::Command.parse_error_json(nil) }.not_to raise_error end end @@ -149,28 +176,29 @@ class Heroku::Command::Test::Multiple; end require "heroku/command/help" require "heroku/command/apps" - Heroku::Command.parse("unknown").should be_nil - Heroku::Command.parse("list").should include(:klass => Heroku::Command::Apps, :method => :index) - Heroku::Command.parse("apps").should include(:klass => Heroku::Command::Apps, :method => :index) - Heroku::Command.parse("apps:create").should include(:klass => Heroku::Command::Apps, :method => :create) + expect(Heroku::Command.parse("unknown")).to be_nil + expect(Heroku::Command.parse("list")).to include(:klass => Heroku::Command::Apps, :method => :index) + expect(Heroku::Command.parse("apps")).to include(:klass => Heroku::Command::Apps, :method => :index) + expect(Heroku::Command.parse("apps:create")).to include(:klass => Heroku::Command::Apps, :method => :create) end context "help" do it "works as a prefix" do - heroku("help ps:scale").should =~ /scale dynos by/ + expect(heroku("help ps:scale")).to match(/scale dynos by/) end it "works as an option" do - heroku("ps:scale -h").should =~ /scale dynos by/ - heroku("ps:scale --help").should =~ /scale dynos by/ + expect(heroku("ps:scale -h")).to match(/scale dynos by/) + expect(heroku("ps:scale --help")).to match(/scale dynos by/) end end context "when no commands match" do it "displays the version if --version is used" do - heroku("--version").should == <<-STDOUT + expect(heroku("--version")).to eq <<-STDOUT #{Heroku.user_agent} +You have no installed plugins. STDOUT end @@ -182,12 +210,12 @@ class Heroku::Command::Test::Multiple; end execute("aps") rescue SystemExit end - captured_stderr.string.should == <<-STDERR + expect(captured_stderr.string).to eq <<-STDERR ! `aps` is not a heroku command. ! Perhaps you meant `apps` or `ps`. ! See `heroku help` for a list of available commands. STDERR - captured_stdout.string.should == "" + expect(captured_stdout.string).to eq("") $stderr, $stdout = original_stderr, original_stdout end @@ -199,11 +227,11 @@ class Heroku::Command::Test::Multiple; end execute("sandwich") rescue SystemExit end - captured_stderr.string.should == <<-STDERR + expect(captured_stderr.string).to eq <<-STDERR ! `sandwich` is not a heroku command. ! See `heroku help` for a list of available commands. STDERR - captured_stdout.string.should == "" + expect(captured_stdout.string).to eq("") $stderr, $stdout = original_stderr, original_stdout end diff --git a/spec/heroku/git_spec.rb b/spec/heroku/git_spec.rb new file mode 100644 index 000000000..eb796e71a --- /dev/null +++ b/spec/heroku/git_spec.rb @@ -0,0 +1,48 @@ +require "heroku/git" + +describe Heroku::Git do + # Secure versions from http://article.gmane.org/gmane.linux.kernel/1853266 + it "determines an insecure 1.7 version is insecure" do + expect(Heroku::Git.git_is_insecure('1.7')).to eq(true) + end + + it "determines an insecure 1.8 version is insecure" do + expect(Heroku::Git.git_is_insecure('1.8.5')).to eq(true) + end + + it "determines an secure 1.8 version is secure" do + expect(Heroku::Git.git_is_insecure('1.8.5.6')).to eq(false) + end + + it "determines an insecure 1.9 version is insecure" do + expect(Heroku::Git.git_is_insecure('1.9.3')).to eq(true) + end + + it "determines an secure 1.9 version is secure" do + expect(Heroku::Git.git_is_insecure('1.9.5')).to eq(false) + end + + it "determines an insecure 2.0 version is insecure" do + expect(Heroku::Git.git_is_insecure('2.0')).to eq(true) + end + + it "determines an secure 2.0 version is secure" do + expect(Heroku::Git.git_is_insecure('2.0.5')).to eq(false) + end + + it "determines an insecure 2.1 version is insecure" do + expect(Heroku::Git.git_is_insecure('2.1')).to eq(true) + end + + it "determines an secure 2.1 version is secure" do + expect(Heroku::Git.git_is_insecure('2.1.4')).to eq(false) + end + + it "determines an insecure 2.2 version is insecure" do + expect(Heroku::Git.git_is_insecure('2.2')).to eq(true) + end + + it "determines an secure 2.2 version is secure" do + expect(Heroku::Git.git_is_insecure('2.2.1')).to eq(false) + end +end diff --git a/spec/heroku/helpers/heroku_postgresql_spec.rb b/spec/heroku/helpers/heroku_postgresql_spec.rb index 5f0d285a9..3720d3732 100644 --- a/spec/heroku/helpers/heroku_postgresql_spec.rb +++ b/spec/heroku/helpers/heroku_postgresql_spec.rb @@ -6,9 +6,9 @@ describe Heroku::Helpers::HerokuPostgresql::Resolver do before do - @resolver = described_class.new('appname', mock(:api)) - @resolver.stub(:app_config_vars) { app_config_vars } - @resolver.stub(:app_attachments) { app_attachments } + @resolver = described_class.new('appname', double(:api)) + allow(@resolver).to receive(:app_config_vars) { app_config_vars } + allow(@resolver).to receive(:app_attachments) { app_attachments } end let(:app_config_vars) do @@ -47,37 +47,37 @@ it "resolves DATABASE" do att = @resolver.resolve('DATABASE') - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end end context "when no app is specified or inferred, and identifier does not have app::db shorthand" do it 'exits, complaining about the missing app' do - api = mock('api') - api.stub(:get_attachments).and_raise("getting this far will cause an inaccurate 'internal server error' message") + api = double('api') + allow(api).to receive(:get_attachments).and_raise("getting this far will cause an inaccurate 'internal server error' message") no_app_resolver = described_class.new(nil, api) - no_app_resolver.should_receive(:error).with { |msg| expect(msg).to match(/No app specified/) }.and_raise(SystemExit) + expect(no_app_resolver).to receive(:error).with(/No app specified/).and_raise(SystemExit) expect { no_app_resolver.resolve('black') }.to raise_error(SystemExit) end end context "when the identifier has ::" do it 'changes the resolver app to the left of the ::' do - @resolver.app_name.should == 'appname' + expect(@resolver.app_name).to eq('appname') att = @resolver.resolve('app2::black') - @resolver.app_name.should == 'app2' + expect(@resolver.app_name).to eq('app2') end it 'resolves database names on the right of the ::' do att = @resolver.resolve('app2::black') - att.url.should == "postgres://black" # since we're mocking out the app_config_vars + expect(att.url).to eq("postgres://black") # since we're mocking out the app_config_vars end it 'looks allows nothing after the :: to use the default' do att = @resolver.resolve('app2::', 'DATABASE_URL') - att.url.should == "postgres://default" + expect(att.url).to eq("postgres://default") end end @@ -93,86 +93,86 @@ it "resolves DATABASE" do att = @resolver.resolve('DATABASE') - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end end it "resolves default using NAME" do att = @resolver.resolve('IVORY') - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end it "resolves non-default using NAME" do att = @resolver.resolve('BLACK') - att.display_name.should == "HEROKU_POSTGRESQL_BLACK_URL" - att.url.should == "postgres://black" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_BLACK_URL") + expect(att.url).to eq("postgres://black") end it "resolves default using NAME_URL" do att = @resolver.resolve('IVORY_URL') - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end it "resolves non-default using NAME_URL" do att = @resolver.resolve('BLACK_URL') - att.display_name.should == "HEROKU_POSTGRESQL_BLACK_URL" - att.url.should == "postgres://black" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_BLACK_URL") + expect(att.url).to eq("postgres://black") end it "resolves default using lowercase" do att = @resolver.resolve('ivory') - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end it "resolves non-default using lowercase" do att = @resolver.resolve('black') - att.display_name.should == "HEROKU_POSTGRESQL_BLACK_URL" - att.url.should == "postgres://black" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_BLACK_URL") + expect(att.url).to eq("postgres://black") end it "resolves non-default using part of name" do att = @resolver.resolve('bla') - att.display_name.should == "HEROKU_POSTGRESQL_BLACK_URL" - att.url.should == "postgres://black" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_BLACK_URL") + expect(att.url).to eq("postgres://black") end it "throws an error if it doesnt exist" do - @resolver.should_receive(:error).with("Unknown database: violet. Valid options are: DATABASE_URL, HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") + expect(@resolver).to receive(:error).with("Unknown database: violet. Valid options are: DATABASE_URL, HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") @resolver.resolve("violet") end context "default" do it "errors if there is no default" do - @resolver.should_receive(:error).with("Unknown database. Valid options are: DATABASE_URL, HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") + expect(@resolver).to receive(:error).with("Unknown database. Valid options are: DATABASE_URL, HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") @resolver.resolve(nil) end it "uses the default if nothing(nil) specified" do att = @resolver.resolve(nil, "DATABASE_URL") - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end it "uses the default if nothing(empty) specified" do att = @resolver.resolve('', "DATABASE_URL") - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end it 'throws an error if given an empty string and asked for the default and there is no default' do app_config_vars.delete 'DATABASE_URL' - @resolver.should_receive(:error).with("Unknown database. Valid options are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") + expect(@resolver).to receive(:error).with("Unknown database. Valid options are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") att = @resolver.resolve('', "DATABASE_URL") end it 'throws an error if given an empty string and asked for the default and the default doesnt match' do app_config_vars['DATABASE_URL'] = 'something different' - @resolver.should_receive(:error).with("Unknown database. Valid options are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") + expect(@resolver).to receive(:error).with("Unknown database. Valid options are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") att = @resolver.resolve('', "DATABASE_URL") end diff --git a/spec/heroku/helpers_spec.rb b/spec/heroku/helpers_spec.rb index fc7925ec0..62ae06a5b 100644 --- a/spec/heroku/helpers_spec.rb +++ b/spec/heroku/helpers_spec.rb @@ -5,12 +5,32 @@ module Heroku describe Helpers do include Heroku::Helpers + context "time_remaining" do + it "should display seconds remaining correctly" do + now = Time.now + future = Time.now + 30 + expect(time_remaining(now, future)).to eq("30s") + end + + it "should display minutes remaining correctly" do + now = Time.now + future = Time.now + 65 + expect(time_remaining(now, future)).to eq("1m 5s") + end + + it "should display hours remaining correctly" do + now = Time.now + future = Time.now + (70*60) + expect(time_remaining(now, future)).to eq("1h 10m") + end + end + context "display_object" do it "should display Array correctly" do - capture_stdout do + expect(capture_stdout do display_object([1,2,3]) - end.should == <<-OUT + end).to eq <<-OUT 1 2 3 @@ -18,9 +38,9 @@ module Heroku end it "should display { :header => [] } list correctly" do - capture_stdout do + expect(capture_stdout do display_object({:first_header => [1,2,3], :last_header => [7,8,9]}) - end.should == <<-OUT + end).to eq <<-OUT === first_header 1 2 @@ -35,9 +55,9 @@ module Heroku end it "should display String properly" do - capture_stdout do + expect(capture_stdout do display_object('string') - end.should == <<-OUT + end).to eq <<-OUT string OUT end diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb new file mode 100644 index 000000000..9956db669 --- /dev/null +++ b/spec/heroku/open_ssl_spec.rb @@ -0,0 +1,194 @@ +require "heroku/open_ssl" + +describe Heroku::OpenSSL do + # This undoes any temporary changes to the property, and also + # resets the flag indicating the path has already been checked. + before(:all) do + Heroku::OpenSSL.openssl = nil + end + after(:each) do + Heroku::OpenSSL.openssl = nil + end + + describe :openssl do + it "returns 'openssl' when nothing else is set" do + expect(Heroku::OpenSSL.openssl).to eq("openssl") + end + + it "returns the environment's 'OPENSSL' variable when it's set" do + ENV['OPENSSL'] = '/usr/bin/openssl' + expect(Heroku::OpenSSL.openssl).to eq('/usr/bin/openssl') + ENV['OPENSSL'] = nil + end + + it "can be set with openssl=" do + Heroku::OpenSSL.openssl = '/usr/local/bin/openssl' + expect(Heroku::OpenSSL.openssl).to eq('/usr/local/bin/openssl') + Heroku::OpenSSL.openssl = nil + end + + it "runs openssl(1) when passed arguments" do + expect(Heroku::OpenSSL).to receive(:system).with("openssl", "version").and_return(true) + expect(Heroku::OpenSSL.openssl("version")).to be true + end + end + + describe :ensure_openssl_installed! do + it "calls openssl(1) to ensure it's available" do + expect(Heroku::OpenSSL).to receive(:openssl).with("version").and_return(true) + Heroku::OpenSSL.ensure_openssl_installed! + end + + it "detects openssl(1) is available when it is available" do + expect { Heroku::OpenSSL.ensure_openssl_installed! }.not_to raise_error + end + + it "detects openssl(1) is absent when it isn't available" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) + end + + it "gives good installation advice on a Mac" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| + allow(ex).to receive(:running_on_a_mac?).and_return(true) + allow(ex).to receive(:running_on_windows?).and_return(false) + expect(ex.installation_hint).to match(/brew install openssl/) + } + end + + it "gives good installation advice on Windows" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| + allow(ex).to receive(:running_on_a_mac?).and_return(false) + allow(ex).to receive(:running_on_windows?).and_return(true) + expect(ex.installation_hint).to match(/Win32OpenSSL\.html/) + } + end + + it "gives good installation advice on miscellaneous Unixen" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| + allow(ex).to receive(:running_on_a_mac?).and_return(false) + allow(ex).to receive(:running_on_windows?).and_return(false) + expect(ex.installation_hint).to match(/'openssl' package/) + } + end + end + + describe :certificate_request do + it "initializes with good defaults" do + request = Heroku::OpenSSL::CertificateRequest.new + expect(request).not_to be_nil + expect(request.key_size).to eq(2048) + expect(request.self_signed).to be false + end + + context "generating with self_signed off" do + before(:all) do + @prev_dir = Dir.getwd + @dir = Dir.mktmpdir + Dir.chdir @dir + + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + + # Would like to do this, but the current version of rspec doesn't support it + # expect { result = request.generate }.to output.to_stdout_from_any_process + @result = request.generate + end + + it "should create Result object" do + expect(@result).to be_kind_of Heroku::OpenSSL::CertificateRequest::Result + end + + it "should have a key filename" do + expect(@result.key_file).to eq('example.com.key') + end + + it "should have a CSR filename" do + expect(@result.csr_file).to eq('example.com.csr') + end + + it "should not have a certificate filename" do + expect(@result.crt_file).to be_nil + end + + it "should produce a PEM key file" do + expect(File.read(@result.key_file)).to match(/\A-----BEGIN (RSA )?PRIVATE KEY-----\n/) + end + + it "should produce a PEM certificate file" do + expect(File.read(@result.csr_file)).to start_with("-----BEGIN CERTIFICATE REQUEST-----\n") + end + + after(:all) do + Dir.chdir @prev_dir + FileUtils.remove_entry_secure @dir + end + end + + context "generating with self_signed on" do + before(:all) do + @prev_dir = Dir.getwd + @dir = Dir.mktmpdir + Dir.chdir @dir + + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + request.self_signed = true + + # Would like to do this, but the current version of rspec doesn't support it + # expect { result = request.generate }.to output.to_stdout_from_any_process + @result = request.generate + end + + it "should create Result object" do + expect(@result).to be_kind_of Heroku::OpenSSL::CertificateRequest::Result + end + + it "should have a key filename" do + expect(@result.key_file).to eq('example.com.key') + end + + it "should not have a CSR filename" do + expect(@result.csr_file).to be_nil + end + + it "should have a certificate filename" do + expect(@result.crt_file).to eq('example.com.crt') + end + + it "should produce a PEM key file" do + expect(File.read(@result.key_file)).to match(/\A-----BEGIN (RSA )?PRIVATE KEY-----\n/) + end + + it "should produce a PEM certificate file" do + expect(File.read(@result.crt_file)).to start_with("-----BEGIN CERTIFICATE-----\n") + end + + after(:all) do + Dir.chdir @prev_dir + FileUtils.remove_entry_secure @dir + end + end + + it "raises installation error when openssl(1) isn't installed" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + + expect { request.generate }.to raise_error(Heroku::OpenSSL::NotInstalledError) + end + end + + Heroku::OpenSSL.openssl = nil + end + end +end diff --git a/spec/heroku/plugin_spec.rb b/spec/heroku/plugin_spec.rb index f6c650162..599161fe1 100644 --- a/spec/heroku/plugin_spec.rb +++ b/spec/heroku/plugin_spec.rb @@ -6,20 +6,20 @@ module Heroku include SandboxHelper it "lives in ~/.heroku/plugins" do - Plugin.stub!(:home_directory).and_return('/home/user') - Plugin.directory.should == '/home/user/.heroku/plugins' + allow(Plugin).to receive(:home_directory).and_return('/home/user') + expect(Plugin.directory).to eq('/home/user/.heroku/plugins') end it "extracts the name from git urls" do - Plugin.new('git://github.com/heroku/plugin.git').name.should == 'plugin' + expect(Plugin.new('git://github.com/heroku/plugin.git').name).to eq('plugin') end describe "management" do before(:each) do @sandbox = "/tmp/heroku_plugins_spec_#{Process.pid}" FileUtils.mkdir_p(@sandbox) - Dir.stub!(:pwd).and_return(@sandbox) - Plugin.stub!(:directory).and_return(@sandbox) + allow(Dir).to receive(:pwd).and_return(@sandbox) + allow(Plugin).to receive(:directory).and_return(@sandbox) end after(:each) do @@ -29,8 +29,8 @@ module Heroku it "lists installed plugins" do FileUtils.mkdir_p(@sandbox + '/plugin1') FileUtils.mkdir_p(@sandbox + '/plugin2') - Plugin.list.should include 'plugin1' - Plugin.list.should include 'plugin2' + expect(Plugin.list).to include 'plugin1' + expect(Plugin.list).to include 'plugin2' end it "installs pulling from the plugin url" do @@ -38,8 +38,8 @@ module Heroku FileUtils.mkdir_p(plugin_folder) `cd #{plugin_folder} && git init && echo 'test' > README && git add . && git commit -m 'my plugin'` Plugin.new(plugin_folder).install - File.directory?("#{@sandbox}/heroku_plugin").should be_true - File.read("#{@sandbox}/heroku_plugin/README").should == "test\n" + expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq("test\n") end it "reinstalls over old copies" do @@ -48,8 +48,8 @@ module Heroku `cd #{plugin_folder} && git init && echo 'test' > README && git add . && git commit -m 'my plugin'` Plugin.new(plugin_folder).install Plugin.new(plugin_folder).install - File.directory?("#{@sandbox}/heroku_plugin").should be_true - File.read("#{@sandbox}/heroku_plugin/README").should == "test\n" + expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq("test\n") end context "update" do @@ -64,27 +64,13 @@ module Heroku it "updates existing copies" do Plugin.new('heroku_plugin').update - File.directory?("#{@sandbox}/heroku_plugin").should be_true - File.read("#{@sandbox}/heroku_plugin/README").should == "updated\n" - end - - it "warns on legacy plugins" do - `cd #{@sandbox}/heroku_plugin && git config --unset branch.master.remote` - stderr = capture_stderr do - begin - Plugin.new('heroku_plugin').update - rescue SystemExit - end - end - stderr.should == <<-STDERR - ! heroku_plugin is a legacy plugin installation. - ! Enable updating by reinstalling with `heroku plugins:install`. -STDERR + expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq("updated\n") end it "raises exception on symlinked plugins" do `cd #{@sandbox} && ln -s heroku_plugin heroku_plugin_symlink` - lambda { Plugin.new('heroku_plugin_symlink').update }.should raise_error Heroku::Plugin::ErrorUpdatingSymlinkPlugin + expect { Plugin.new('heroku_plugin_symlink').update }.to raise_error Heroku::Plugin::ErrorUpdatingSymlinkPlugin end end @@ -93,21 +79,21 @@ module Heroku it "uninstalls removing the folder" do FileUtils.mkdir_p(@sandbox + '/plugin1') Plugin.new('git://github.com/heroku/plugin1.git').uninstall - Plugin.list.should == [] + expect(Plugin.list).to eq([]) end it "adds the lib folder in the plugin to the load path, if present" do FileUtils.mkdir_p(@sandbox + '/plugin/lib') File.open(@sandbox + '/plugin/lib/my_custom_plugin_file.rb', 'w') { |f| f.write "" } Plugin.load! - lambda { require 'my_custom_plugin_file' }.should_not raise_error(LoadError) + expect { require 'my_custom_plugin_file' }.not_to raise_error end it "loads init.rb, if present" do FileUtils.mkdir_p(@sandbox + '/plugin') File.open(@sandbox + '/plugin/init.rb', 'w') { |f| f.write "LoadedInit = true" } Plugin.load! - LoadedInit.should be_true + expect(LoadedInit).to be_truthy end describe "when there are plugin load errors" do @@ -118,7 +104,7 @@ module Heroku it "should not throw an error" do capture_stderr do - lambda { Plugin.load! }.should_not raise_error + expect { Plugin.load! }.not_to raise_error end end @@ -126,7 +112,7 @@ module Heroku stderr = capture_stderr do Plugin.load! end - stderr.should include('some_non_existant_file (LoadError)') + expect(stderr).to include('some_non_existant_file (LoadError)') end it "should still load other plugins" do @@ -135,8 +121,8 @@ module Heroku stderr = capture_stderr do Plugin.load! end - stderr.should include('some_non_existant_file (LoadError)') - LoadedPlugin2.should be_true + expect(stderr).to include('some_non_existant_file (LoadError)') + expect(LoadedPlugin2).to be_truthy end end @@ -151,20 +137,20 @@ module Heroku it "should show confirmation to remove deprecated plugins if in an interactive shell" do old_stdin_isatty = STDIN.isatty - STDIN.stub!(:isatty).and_return(true) - Plugin.should_receive(:confirm).with("The plugin heroku-releases has been deprecated. Would you like to remove it? (y/N)").and_return(true) - Plugin.should_receive(:remove_plugin).with("heroku-releases") + allow(STDIN).to receive(:isatty).and_return(true) + expect(Plugin).to receive(:confirm).with("The plugin heroku-releases has been deprecated. Would you like to remove it? (y/N)").and_return(true) + expect(Plugin).to receive(:remove_plugin).with("heroku-releases") Plugin.load! - STDIN.stub!(:isatty).and_return(old_stdin_isatty) + allow(STDIN).to receive(:isatty).and_return(old_stdin_isatty) end it "should not prompt for deprecation if not in an interactive shell" do old_stdin_isatty = STDIN.isatty - STDIN.stub!(:isatty).and_return(false) - Plugin.should_not_receive(:confirm) - Plugin.should_not_receive(:remove_plugin).with("heroku-releases") + allow(STDIN).to receive(:isatty).and_return(false) + expect(Plugin).not_to receive(:confirm) + expect(Plugin).not_to receive(:remove_plugin).with("heroku-releases") Plugin.load! - STDIN.stub!(:isatty).and_return(old_stdin_isatty) + allow(STDIN).to receive(:isatty).and_return(old_stdin_isatty) end end end diff --git a/spec/heroku/updater_spec.rb b/spec/heroku/updater_spec.rb index bc1f4be3e..cd9ecec51 100644 --- a/spec/heroku/updater_spec.rb +++ b/spec/heroku/updater_spec.rb @@ -5,40 +5,110 @@ module Heroku describe Updater do - it "calculates the latest local version" do - Heroku::Updater.latest_local_version.should == Heroku::VERSION + before do + allow(subject).to receive(:stderr_puts) + allow(subject).to receive(:stderr_print) end - it "calculates compare_versions" do - Heroku::Updater.compare_versions('1.1.1', '1.1.1').should == 0 + describe('::latest_local_version') do + it 'calculates the latest local version' do + expect(subject.latest_local_version).to eq(Heroku::VERSION) + end + end + + describe('::compare_versions') do + it 'calculates compare_versions' do + expect(subject.compare_versions('1.1.1', '1.1.1')).to eq(0) - Heroku::Updater.compare_versions('2.1.1', '1.1.1').should == 1 - Heroku::Updater.compare_versions('1.1.1', '2.1.1').should == -1 + expect(subject.compare_versions('2.1.1', '1.1.1')).to eq(1) + expect(subject.compare_versions('1.1.1', '2.1.1')).to eq(-1) - Heroku::Updater.compare_versions('1.2.1', '1.1.1').should == 1 - Heroku::Updater.compare_versions('1.1.1', '1.2.1').should == -1 + expect(subject.compare_versions('1.2.1', '1.1.1')).to eq(1) + expect(subject.compare_versions('1.1.1', '1.2.1')).to eq(-1) - Heroku::Updater.compare_versions('1.1.2', '1.1.1').should == 1 - Heroku::Updater.compare_versions('1.1.1', '1.1.2').should == -1 + expect(subject.compare_versions('1.1.2', '1.1.1')).to eq(1) + expect(subject.compare_versions('1.1.1', '1.1.2')).to eq(-1) - Heroku::Updater.compare_versions('2.1.1', '1.2.1').should == 1 - Heroku::Updater.compare_versions('1.2.1', '2.1.1').should == -1 + expect(subject.compare_versions('2.1.1', '1.2.1')).to eq(1) + expect(subject.compare_versions('1.2.1', '2.1.1')).to eq(-1) - Heroku::Updater.compare_versions('2.1.1', '1.1.2').should == 1 - Heroku::Updater.compare_versions('1.1.2', '2.1.1').should == -1 + expect(subject.compare_versions('2.1.1', '1.1.2')).to eq(1) + expect(subject.compare_versions('1.1.2', '2.1.1')).to eq(-1) - Heroku::Updater.compare_versions('1.2.4', '1.2.3').should == 1 - Heroku::Updater.compare_versions('1.2.3', '1.2.4').should == -1 + expect(subject.compare_versions('1.2.4', '1.2.3')).to eq(1) + expect(subject.compare_versions('1.2.3', '1.2.4')).to eq(-1) - Heroku::Updater.compare_versions('1.2.1', '1.2' ).should == 1 - Heroku::Updater.compare_versions('1.2', '1.2.1').should == -1 + expect(subject.compare_versions('1.2.1', '1.2' )).to eq(1) + expect(subject.compare_versions('1.2', '1.2.1')).to eq(-1) - Heroku::Updater.compare_versions('1.1.1.pre1', '1.1.1').should == 1 - Heroku::Updater.compare_versions('1.1.1', '1.1.1.pre1').should == -1 + expect(subject.compare_versions('1.1.1.pre1', '1.1.1')).to eq(1) + expect(subject.compare_versions('1.1.1', '1.1.1.pre1')).to eq(-1) - Heroku::Updater.compare_versions('1.1.1.pre2', '1.1.1.pre1').should == 1 - Heroku::Updater.compare_versions('1.1.1.pre1', '1.1.1.pre2').should == -1 + expect(subject.compare_versions('1.1.1.pre2', '1.1.1.pre1')).to eq(1) + expect(subject.compare_versions('1.1.1.pre1', '1.1.1.pre2')).to eq(-1) + end end + describe '::update' do + before do + Excon.stub({:host => 'assets.heroku.com', :path => '/heroku-client/VERSION'}, {:body => "3.9.7\n"}) + end + + describe 'non-beta' do + before do + zip = File.read(File.expand_path('../../fixtures/heroku-client-3.9.7.zip', __FILE__)) + hash = "615792e1f06800a6d744f518887b10c09aa914eab51d0f7fbbefd81a8a64af93" + Excon.stub({:host => 'toolbelt.heroku.com', :path => '/download/zip'}, {:body => zip}) + Excon.stub({:host => 'toolbelt.heroku.com', :path => '/update/hash'}, {:body => "#{hash}\n"}) + end + + context 'with no update available' do + before do + allow(subject).to receive(:latest_local_version).and_return('3.9.7') + end + + it 'does not update' do + expect(subject.update(false)).to be_nil + end + end + + context 'with an update available' do + before do + allow(subject).to receive(:latest_local_version).and_return('3.9.6') + end + + it 'updates' do + expect(subject.update(false)).to eq('3.9.7') + end + end + end + + describe 'beta' do + before do + zip = File.read(File.expand_path('../../fixtures/heroku-client-3.9.7.zip', __FILE__)) + Excon.stub({:host => 'toolbelt.heroku.com', :path => '/download/beta-zip'}, {:body => zip}) + end + + context 'with no update available' do + before do + allow(subject).to receive(:latest_local_version).and_return('3.9.7') + end + + it 'still updates' do + expect(subject.update(true)).to eq('3.9.7') + end + end + + context 'with a beta older than what we have' do + before do + allow(subject).to receive(:latest_local_version).and_return('3.9.8') + end + + it 'does not update' do + expect(subject.update(true)).to be_nil + end + end + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b3bfda110..e92d25ae6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,14 +2,13 @@ require "rubygems" -require "excon" +require "coveralls" +Coveralls.wear! -# ensure these are around for errors -# as their require is generally deferred -require "heroku-api" -require "rest_client" +require "excon" require "heroku/cli" +require "heroku/client" require "rspec" require "rr" require "fakefs/safe" @@ -35,16 +34,16 @@ def stub_api_request(method, path) def prepare_command(klass) command = klass.new - command.stub!(:app).and_return("example") - command.stub!(:ask).and_return("") - command.stub!(:display) - command.stub!(:hputs) - command.stub!(:hprint) - command.stub!(:heroku).and_return(mock('heroku client', :host => 'heroku.com')) + allow(command).to receive(:app).and_return("example") + allow(command).to receive(:ask).and_return("") + allow(command).to receive(:display) + allow(command).to receive(:hputs) + allow(command).to receive(:hprint) + allow(command).to receive(:heroku).and_return(double('heroku client', :host => 'heroku.com')) command end -def execute(command_line) +def execute(command_line, opts={}) extend RR::Adapters::RRMethods args = command_line.split(" ") @@ -62,15 +61,17 @@ def execute(command_line) original_stdin, original_stderr, original_stdout = $stdin, $stderr, $stdout - $stdin = captured_stdin = StringIO.new - $stderr = captured_stderr = StringIO.new - $stdout = captured_stdout = StringIO.new - class << captured_stdout + fake_tty_stdout = StringIO.new + class << fake_tty_stdout def tty? true end end + $stdin = captured_stdin = opts.fetch(:stdin, StringIO.new) + $stderr = captured_stderr = opts.fetch(:stderr, StringIO.new) + $stdout = captured_stdout = opts.fetch(:stdout, fake_tty_stdout) + begin object.send(method) rescue SystemExit @@ -133,6 +134,7 @@ def stub_core stub(Heroku::Auth).user.returns("email@example.com") stub(Heroku::Auth).password.returns("pass") stub(Heroku::Client).auth.returns("apikey01") + stub(Heroku::Updater).autoupdate stubbed_core end end @@ -147,6 +149,16 @@ def stub_pg end end +def stub_pgapp + @stubbed_pgapp ||= begin + stubbed_pgapp = nil + any_instance_of(Heroku::Client::HerokuPostgresqlApp) do |pg| + stubbed_pgapp = stub(pg) + end + stubbed_pgapp + end +end + def stub_pgbackups @stubbed_pgbackups ||= begin stubbed_pgbackups = nil @@ -205,13 +217,33 @@ module Heroku::Helpers def home_directory @home_directory end + undef_method :has_http_git_entry_in_netrc + def has_http_git_entry_in_netrc + true + end + undef_method :error_log + def error_log(*obj); end + undef_method :error_log_path + def error_log_path + 'error_log_path' + end +end + +require "heroku/git" +module Heroku::Git + def self.check_git_version; end +end + +require "heroku/rollbar" +module Heroku::Rollbar + def self.error(e); end end require "support/display_message_matcher" require "support/organizations_mock_helper" +require "support/addons_helper" RSpec.configure do |config| - config.color_enabled = true config.include DisplayMessageMatcher config.order = 'rand' config.before { Heroku::Helpers.error_with_failure = false } diff --git a/spec/support/addons_helper.rb b/spec/support/addons_helper.rb new file mode 100644 index 000000000..949ae8309 --- /dev/null +++ b/spec/support/addons_helper.rb @@ -0,0 +1,55 @@ +module Support + module Addons + def build_addon(addon={}) + addon_id = addon[:id] || SecureRandom.uuid + { + config_vars: addon.fetch(:config_vars, []), + created_at: Time.now, + id: addon_id, + name: addon[:name] || addon_name(addon[:plan][:name]), + + addon_service: { + id: SecureRandom.uuid, + }.merge(addon.fetch(:addon_service, {})), + + plan: { + id: SecureRandom.uuid, + price: { + cents: 0, unit: 'month' + } + }.merge(addon.fetch(:plan, {})), + + app: { + id: SecureRandom.uuid, + }.merge(addon.fetch(:app, {})), + + provider_id: addon[:provider_id], + updated_at: Time.now, + web_url: "https://addons-sso.heroku.com/apps/#{addon[:app][:name]}/addons/#{addon_id}" + } + end + + def build_attachment(attachment={}) + { + addon: { + id: SecureRandom.uuid, + }.merge(attachment.fetch(:addon, {})), + + app: { + id: SecureRandom.uuid, + }.merge(attachment.fetch(:app, {})), + + created_at: Time.now, + id: attachment.fetch(:id, SecureRandom.uuid), + name: attachment[:name], + updated_at: Time.now + } + end + + # Helpers generate Hashes with symbol keys. When using as outside of + # a request stub, we need them all the be strings. See "understands foo=baz". + def stringify(options) + MultiJson.decode(MultiJson.encode(options)) + end + end +end diff --git a/spec/support/openssl_mock_helper.rb b/spec/support/openssl_mock_helper.rb index 70268c482..fda0e6a76 100644 --- a/spec/support/openssl_mock_helper.rb +++ b/spec/support/openssl_mock_helper.rb @@ -1,8 +1,8 @@ def mock_openssl - @ctx_mock = mock "SSLContext", :key= => nil, :cert= => nil, :ssl_version= => nil - @tcp_socket_mock = mock "TCPSocket", :close => true - @ssl_socket_mock = mock "SSLSocket", :sync= => true, :connect => true, :close => true, :to_io => $stdin + @ctx_mock = double "SSLContext", :key= => nil, :cert= => nil, :ssl_version= => nil + @tcp_socket_mock = double "TCPSocket", :close => true + @ssl_socket_mock = double "SSLSocket", :sync= => true, :connect => true, :close => true, :to_io => $stdin - OpenSSL::SSL::SSLSocket.stub(:new).and_return(@ssl_socket_mock) - OpenSSL::SSL::SSLContext.stub(:new).and_return(@ctx_mock) + allow(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(@ssl_socket_mock) + allow(OpenSSL::SSL::SSLContext).to receive(:new).and_return(@ctx_mock) end diff --git a/tasks/deb.rake b/tasks/deb.rake new file mode 100644 index 000000000..f0e5cbe35 --- /dev/null +++ b/tasks/deb.rake @@ -0,0 +1,72 @@ +FOREMAN_VERSION = "0.75.0" + +namespace :deb do + desc "build deb" + task :build => dist("heroku-toolbelt-#{version}.apt") + + desc "release deb" + task :release => :build do |t| + s3_store_dir dist("heroku-toolbelt-#{version}.apt"), "apt", "heroku-toolbelt" + end + + file dist("heroku-toolbelt-#{version}.apt") => [ dist("heroku-toolbelt-#{version}.apt/foreman-#{FOREMAN_VERSION}.deb"), dist("heroku-toolbelt-#{version}.apt/heroku-#{version}.deb"), dist("heroku-toolbelt-#{version}.apt/heroku-toolbelt-#{version}.deb") ] do |t| + abort "Don't publish .debs of pre-releases!" if version =~ /[a-zA-Z]$/ + + cd t.name do |dir| + touch "Sources" + + sh "apt-ftparchive packages . > Packages" + sh "gzip -c Packages > Packages.gz" + sh "apt-ftparchive -c #{resource("deb/heroku-toolbelt/apt-ftparchive.conf")} release . > Release" + sh "gpg -abs -u 0F1B0520 -o Release.gpg Release" + end + end + + + file dist("heroku-toolbelt-#{version}.apt/foreman-#{FOREMAN_VERSION}.deb") do |t| + mkdir_p File.dirname(t.name) + unless File.exist? "dist/foreman" + sh "git clone https://github.com/ddollar/foreman.git dist/foreman" + end + cd "dist/foreman" do + sh "git checkout v#{FOREMAN_VERSION}" + rm_rf ".bundle" + rm_rf "apt-#{FOREMAN_VERSION}" + Bundler.with_clean_env do + sh "unset GEM_HOME RUBYOPT; bundle install --path vendor/bundle" or abort + sh "unset GEM_HOME RUBYOPT; bundle exec rake deb:build" or abort + end + mv "pkg/apt-#{FOREMAN_VERSION}/foreman-#{FOREMAN_VERSION}.deb", t.name + end + end + + file dist("heroku-toolbelt-#{version}.apt/heroku-#{version}.deb") => distribution_files("deb") do |t| + tempdir do + mkdir_p "usr/local/heroku" + cd "usr/local/heroku" do + assemble_distribution + assemble_gems + assemble resource("deb/heroku/heroku"), "bin/heroku", 0755 + end + + assemble resource("deb/heroku/control"), "control" + assemble resource("deb/heroku/postinst"), "postinst" + + sh "tar czf data.tar.gz usr/local/heroku --owner=root --group=root" + sh "tar czf control.tar.gz control postinst" + + File.open("debian-binary", "w") do |f| + f.puts "2.0" + end + + sh "ar -r #{t.name} debian-binary control.tar.gz data.tar.gz" + end + end + + file dist("heroku-toolbelt-#{version}.apt/heroku-toolbelt-#{version}.deb") do |t| + tempdir do |dir| + assemble resource("deb/heroku-toolbelt/control"), "DEBIAN/control" + sh "dpkg-deb --build . #{t.name}" + end + end +end diff --git a/tasks/exe.rake b/tasks/exe.rake new file mode 100644 index 000000000..64cfa3495 --- /dev/null +++ b/tasks/exe.rake @@ -0,0 +1,158 @@ +require "erb" +require "shellwords" + +$is_mac = RUBY_PLATFORM =~ /darwin/ +$base_path = File.expand_path(File.join(File.dirname(__FILE__), "..")) +$cache_path = File.join($base_path, "dist", "cache") +def windows_path(path); `winepath -w #{path.shellescape}`.chomp; end + +def setup_wine_env + ENV["WINEPREFIX"] = "#$base_path/dist/wine" # keep it contained; by default it goes in $HOME/.wine + ENV["WINEDEBUG"] = "-all" # wine is full of errors, no one cares + ENV["WINEDLLOVERRIDES"] = "winemenubuilder.exe=n" # tell wine to use our custom winemenubuilder.exe, see comment in exe:init-wine + ENV["DISPLAY"] = ':42' + $xvfb_pid = spawn 'Xvfb', ':42', [:out,:err] => '/dev/null' # use a virtual x server so we can run headless + sleep(2) # give Xvfb some time to boot up +end + +def cleanup_after_wine + # terminate our Xvfb process + sleep(2) # give Xvfb some time to finish up; seems to prevent some error messages + Process.kill "INT", $xvfb_pid + Process.wait $xvfb_pid + # wine leaves the terminal all sorts of broken. + # pretty much every time it'll switch input to cursor key application mode (cf. http://www.tldp.org/HOWTO/Keyboard-and-Console-HOWTO-21.html), + # fairly often it'll turn echo off, a couple other odd things have also been observed. + # this sends a soft reset to the terminal, albeit I suspect it only works in xterm emulators, + # but then again maybe it's an xterm-only problem anyway? who knows… + system "echo \033[!p" + system "stty echo" +end + +# ensure cleanup_after_wine runs when aborted too +trap("INT") { cleanup_after_wine; exit } + +# see comment on build_zip +def extract_zip(filename, destination) + tempdir do |dir| + sh %{ unzip -q "#{filename}" } + sh %{ mv * "#{destination}" } + end +end + +# a bunch of needed binaries are in an amazon bucket. not sure I love this, but I guess it keeps the repo small +def cache_file_from_bucket(filename) + FileUtils.mkdir_p $cache_path + file_cache_path = File.join($cache_path, filename) + system "curl -# https://heroku-toolbelt.s3.amazonaws.com/#{filename} -o '#{file_cache_path}'" unless File.exists? file_cache_path + file_cache_path +end + +# file task for the final windows installer file. +# if you ask me, it's fairly pointless to be using a file task for the final +# file if the intermediates get placed in all sorts of temp dirs that then get +# destroyed, so we don't get to benefit from the time savings of not generating +# the same thing over and over again. +file dist("heroku-toolbelt-#{version}.exe") => "zip:build" do |exe_task| + tempdir do |build_path| + installer_path = "#{build_path}/heroku-installer" + heroku_cli_path = "#{installer_path}/heroku" + mkdir_p heroku_cli_path + extract_zip "#{$base_path}/dist/heroku-#{version}.zip", "#{heroku_cli_path}/" + + # gather the ruby and git installers, downlading from s3 + mkdir "#{installer_path}/installers" + cd "#{installer_path}/installers" do + ["rubyinstaller.exe", "git.exe"].each { |i| cp cache_file_from_bucket(i), i } + end + + # add windows helper executables to the heroku cli + cp resource("exe/heroku.bat"), "#{heroku_cli_path}/bin/heroku.bat" + cp resource("exe/heroku"), "#{heroku_cli_path}/bin/heroku" + cp resource("exe/foreman.bat"), "#{heroku_cli_path}/bin/foreman.bat" + cp resource("exe/foreman"), "#{heroku_cli_path}/bin/foreman" + cp resource("exe/ssh-keygen.bat"), "#{heroku_cli_path}/bin/ssh-keygen.bat" + + # render the iss file used by inno setup to compile the installer + # this sets the version and the output filename + File.write("#{installer_path}/heroku.iss", ERB.new(File.read(resource("exe/heroku.iss"))).result(binding)) + + # the codesign command used by inno to sign the installer and uninstaller + sign_cmd = 'c:\windows\mono\mono-2.0\lib\mono\4.5\signcode.exe' + %Q[ + -spc "#{windows_path(resource('exe/heroku-codesign-cert.spc'))}" + -v "#{windows_path(resource('exe/heroku-codesign-cert.pvk'))}" + -a sha1 -$ commercial + -n "Heroku Toolbelt" + $f ]. # $f gets replaced by iscc with the path to the file it wants to compile + gsub("\n", ' '). # everything on a single line now + gsub('"', '$q') # iscc requires quotes to be escaped this way, don't ask + + # compile installer under wine! + setup_wine_env + system 'wine', 'C:\inno\ISCC.exe', + "/Smono-signcode=#{sign_cmd}", '/qp', + windows_path("#{installer_path}/heroku.iss") + cleanup_after_wine + + # move final installer from build_path to pkg dir + mv File.basename(exe_task.name), exe_task.name + end +end + +desc "Build exe" +task "exe:build" => dist("heroku-toolbelt-#{version}.exe") + +desc "Release exe" +task "exe:release" => "exe:build" do |t| + s3_store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt-#{version}.exe" + s3_store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt-beta.exe" if beta? + s3_store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt.exe" unless beta? +end + +desc "Create wine environment to build windows installer" +task "exe:init-wine" do + setup_wine_env + rm_rf ENV["WINEPREFIX"] + system "wineboot --init" # init wine dir + # replace winemenubuilder with a thing that does nothing, preventing it from poopin' a .config dir into your $HOME + system %q[ + echo "int main(){return 0;}" > noop.c + winegcc noop.c -o noop + mv noop.exe.so "$WINEPREFIX/drive_c/windows/system32/winemenubuilder.exe" + rm noop.* + ] + # set mac wine to use the x11 display driver; iscc borks without this, also it lets us run headless with Xvfb + system %Q[echo '[HKEY_CURRENT_USER\\Software\\Wine\\Drivers]\n"Graphics"="x11"' | regedit -] if $is_mac + # install inno setup + isetup_path = windows_path(cache_file_from_bucket("isetup.exe")).shellescape + system "wine #{isetup_path} /verysilent /suppressmsgboxes /nocancel /norestart /noicons /dir=c:\\inno" + cleanup_after_wine +end + +# Mono's signcode tool can't take the private key passphrase non-interactively (i.e. read file, or as a parameter), so +# in order to run the build non-interactively we have to use a passphrase-less key. To keep the private key secure, the +# key that comes from the repository is encrypted. You can either run exe:build and type in the passphrase manually +# (twice!), or decode it for good with this task. +# +# Ensure your build environment is secure before leaving an unencrypted private key lying around. +# +# Additionally, Mac OS X's default openssl, as of Mavericks, is 0.9.8y, which doesn't support the pvk format. The 1.0.x +# tree does, and you can install it via homebrew (brew install openssl), but it's keg-only, so it'll not be in your +# PATH. You could `brew link` it, but it's safer to leave it alone. Instead, you can pass the full path to the openssl +# binary to be used via the OPENSSL_PATH environment variable: +# +# OPENSSL_PATH=`brew --prefix openssl`/bin/openssl rake exe:pvk-nocrypt +desc "Remove passphrase from heroku-codesign-cert.pvk; see source comments" +task "exe:pvk-nocrypt" do + openssl = (ENV["OPENSSL_PATH"] || "openssl").shellescape + version = `#{openssl} version`.chomp + keyfile_in = resource('exe/heroku-codesign-cert.encrypted.pvk').shellescape + keyfile_out = resource('exe/heroku-codesign-cert.pvk').shellescape + raise "OpenSSL version should be 1.0.x; instead got: #{version}" if version !~ /^OpenSSL 1\./ + system "#{openssl} rsa -inform PVK -outform PVK -pvk-none -in #{keyfile_in} -out #{keyfile_out}" +end + +desc "Link the encrypted pvk" +task "exe:pvk" do + symlink resource("exe/heroku-codesign-cert.encrypted.pvk"), resource("exe/heroku-codesign-cert.pvk") +end diff --git a/tasks/gem.rake b/tasks/gem.rake new file mode 100644 index 000000000..7de28aa6c --- /dev/null +++ b/tasks/gem.rake @@ -0,0 +1,12 @@ +namespace :gem do + desc "build gem" + task :build do + sh "gem build heroku.gemspec" + mv "heroku-#{version}.gem", dist("heroku-#{version}.gem") + end + + desc "release gem" + task :release => :build do + sh "gem push #{dist("heroku-#{version}.gem")}" + end +end diff --git a/tasks/git.rake b/tasks/git.rake new file mode 100644 index 000000000..317f24eb5 --- /dev/null +++ b/tasks/git.rake @@ -0,0 +1,7 @@ +namespace :git do + desc "tags the repo at the current version and pushes it to github" + task :tag do + sh "git tag v#{version}" + sh "git push origin v#{version}" + end +end diff --git a/tasks/helpers/file.rb b/tasks/helpers/file.rb new file mode 100644 index 000000000..eff5e08fe --- /dev/null +++ b/tasks/helpers/file.rb @@ -0,0 +1,59 @@ +require "erb" +require "fileutils" +require "tmpdir" + +GEM_BLACKLIST = %w( bundler heroku ) + +def assemble(source, target, perms=0644) + FileUtils.mkdir_p(File.dirname(target)) + File.open(target, "w") do |f| + f.puts ERB.new(File.read(source)).result(binding) + end + File.chmod(perms, target) +end + +def assemble_distribution(target_dir=Dir.pwd) + distribution_files.each do |source| + target = source.gsub(/^#{PROJECT_ROOT}/, target_dir) + FileUtils.mkdir_p(File.dirname(target)) + FileUtils.cp(source, target) + end +end + +def assemble_gems(target_dir=Dir.pwd) + %x{ env BUNDLE_WITHOUT="development:test" bundle show }.split("\n").each do |line| + if line =~ /^ \* (.*?) \((.*?)\)/ + next if GEM_BLACKLIST.include?($1) + gem_dir = %x{ bundle show #{$1} }.strip + FileUtils.mkdir_p "#{target_dir}/vendor/gems" + %x{ cp -R "#{gem_dir}" "#{target_dir}/vendor/gems" } + end + end.compact +end + +def beta? + Heroku::VERSION.to_s =~ /pre/ +end + +def distribution_files(type=nil) + Dir[File.expand_path("{bin,data,lib}/**/*", PROJECT_ROOT)].select do |file| + File.file?(file) + end +end + +def dist(filename) + FileUtils.mkdir_p("dist") + File.expand_path("dist/#{filename}", PROJECT_ROOT) +end + +def resource(name) + File.expand_path("resources/#{name}", PROJECT_ROOT) +end + +def tempdir + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + yield(dir) + end + end +end diff --git a/tasks/helpers/s3.rb b/tasks/helpers/s3.rb new file mode 100644 index 000000000..0cb758869 --- /dev/null +++ b/tasks/helpers/s3.rb @@ -0,0 +1,31 @@ +def s3_connect + return if @s3_connected + + require "aws/s3" + + unless ENV["HEROKU_RELEASE_ACCESS"] && ENV["HEROKU_RELEASE_SECRET"] + puts "please set HEROKU_RELEASE_ACCESS and HEROKU_RELEASE_SECRET in your environment" + exit 1 + end + + AWS::S3::Base.establish_connection!( + :access_key_id => ENV["HEROKU_RELEASE_ACCESS"], + :secret_access_key => ENV["HEROKU_RELEASE_SECRET"] + ) + + @s3_connected = true +end + +def s3_store(package_file, filename, bucket="assets.heroku.com") + s3_connect + puts "storing: #{filename}" + AWS::S3::S3Object.store(filename, File.open(package_file), bucket, :access => :public_read) +end + +def s3_store_dir(from, to, bucket="assets.heroku.com") + Dir.glob(File.join(from, "**", "*")).each do |file| + next if File.directory?(file) + remote = file.gsub(from, to) + s3_store file, remote, bucket + end +end diff --git a/tasks/manifest.rake b/tasks/manifest.rake new file mode 100644 index 000000000..16ca3543b --- /dev/null +++ b/tasks/manifest.rake @@ -0,0 +1,17 @@ +namespace :manifest do + desc "puts VERSION file into s3" + task :update do + if beta? + $stderr.puts "skipping manifest:update since this is a beta release" + next + end + + tempdir do |dir| + File.open("VERSION", "w") do |file| + file.puts version + end + puts "Current version: #{version}" + s3_store "#{dir}/VERSION", "heroku-client/VERSION" + end + end +end diff --git a/tasks/pkg.rake b/tasks/pkg.rake new file mode 100644 index 000000000..be29192e5 --- /dev/null +++ b/tasks/pkg.rake @@ -0,0 +1,63 @@ +file dist("heroku-toolbelt-#{version}.pkg") => distribution_files("pkg") do |t| + tempdir do |dir| + mkdir "heroku-client" + cd "heroku-client" do + assemble_distribution + assemble_gems + assemble resource("pkg/heroku"), "bin/heroku", 0755 + end + + mkdir_p "pkg" + mkdir_p "pkg/Resources" + mkdir_p "pkg/heroku-client.pkg" + + kbytes = %x{ du -ks pkg | cut -f 1 } + num_files = %x{ find pkg | wc -l } + + dist = File.read(resource("pkg/Distribution.erb")) + dist = ERB.new(dist).result(binding) + File.open("pkg/Distribution", "w") { |f| f.puts dist } + + dist = File.read(resource("pkg/PackageInfo.erb")) + dist = ERB.new(dist).result(binding) + File.open("pkg/heroku-client.pkg/PackageInfo", "w") { |f| f.puts dist } + + mkdir_p "pkg/Scripts" + + mkdir_p "pkg/heroku-client.pkg/Scripts" + cp resource("pkg/postinstall"), "pkg/heroku-client.pkg/Scripts/postinstall" + chmod 0755, "pkg/heroku-client.pkg/Scripts/postinstall" + + sh %{ mkbom -s heroku-client pkg/heroku-client.pkg/Bom } + + Dir.chdir("heroku-client") do + sh %{ find . | cpio -o --format odc | gzip -c > ../pkg/heroku-client.pkg/Payload } + end + + unless File.exists?(dist('foreman-0.75.0.pkg')) + sh %{ curl https://heroku-toolbelt.s3.amazonaws.com/foreman-0.75.0.pkg -o #{dist('foreman-0.75.0.pkg')} } + end + sh %{ pkgutil --expand #{dist('foreman-0.75.0.pkg')} foreman } + mv "foreman/foreman.pkg", "pkg/foreman.pkg" + + unless File.exists?(dist('ruby.pkg')) + sh %{ curl https://heroku-toolbelt.s3.amazonaws.com/ruby.pkg -o #{dist('ruby.pkg')} } + end + sh %{ pkgutil --expand #{dist('ruby.pkg')} ruby } + mv "ruby/ruby-1.9.3-p194.pkg", "pkg/ruby.pkg" + + sh %{ pkgutil --flatten pkg heroku-toolbelt-#{version}.pkg } + sh %{ productsign --sign "Developer ID Installer: Heroku INC" heroku-toolbelt-#{version}.pkg heroku-toolbelt-#{version}-signed.pkg } + cp_r "heroku-toolbelt-#{version}-signed.pkg", t.name + end +end + +desc "build pkg" +task "pkg:build" => dist("heroku-toolbelt-#{version}.pkg") + +desc "release pkg" +task "pkg:release" => dist("heroku-toolbelt-#{version}.pkg") do + s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-#{version}.pkg" + s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-beta.pkg" if beta? + s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt.pkg" unless beta? +end diff --git a/tasks/resources/tgz/heroku b/tasks/resources/tgz/heroku new file mode 100644 index 000000000..8b09b7e66 --- /dev/null +++ b/tasks/resources/tgz/heroku @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby +# encoding: UTF-8 + +# resolve bin path, ignoring symlinks +require "pathname" +bin_file = Pathname.new(__FILE__).realpath + +# add locally vendored gems to libpath +gem_dir = File.expand_path("../../vendor/gems", bin_file) +Dir["#{gem_dir}/**/lib"].each do |libdir| + $:.unshift libdir +end + +# add self to libpath +$:.unshift File.expand_path("../../lib", bin_file) + +# inject any code in ~/.heroku/client over top +require "heroku/updater" +Heroku::Updater.inject_libpath + +# start up the CLI +require "heroku/cli" +Heroku.user_agent = "heroku-toolbelt/#{Heroku::VERSION} (#{RUBY_PLATFORM}) ruby/#{RUBY_VERSION}" +Heroku::CLI.start(*ARGV) diff --git a/tasks/rspec.rake b/tasks/rspec.rake new file mode 100644 index 000000000..63a721d19 --- /dev/null +++ b/tasks/rspec.rake @@ -0,0 +1,5 @@ +begin + require 'rspec/core/rake_task' + RSpec::Core::RakeTask.new(:spec) +rescue LoadError +end diff --git a/tasks/tgz.rake b/tasks/tgz.rake new file mode 100644 index 000000000..ecac65d4d --- /dev/null +++ b/tasks/tgz.rake @@ -0,0 +1,27 @@ +namespace :tgz do + desc "build tgz" + task :build => dist("heroku-#{version}.tgz") + + desc "release tgz" + task :release => :build do |t| + s3_store dist("heroku-#{version}.tgz"), "heroku-client/heroku-client-#{version}.tgz" + s3_store dist("heroku-#{version}.tgz"), "heroku-client/heroku-client-beta.tgz" if beta? + s3_store dist("heroku-#{version}.tgz"), "heroku-client/heroku-client.tgz" unless beta? + end + + file dist("heroku-#{version}.tgz") => distribution_files("tgz") do |t| + tempdir do |dir| + mkdir "heroku-client" + cd "heroku-client" do + assemble_distribution + assemble_gems + assemble resource("tgz/heroku"), "bin/heroku", 0755 + end + + sh "chmod -R go+r heroku-client" + sh "sudo chown -R 0:0 heroku-client" + sh "tar czf #{t.name} heroku-client" + sh "sudo chown -R $(whoami) heroku-client" + end + end +end diff --git a/tasks/zip.rake b/tasks/zip.rake new file mode 100644 index 000000000..82c7a6c39 --- /dev/null +++ b/tasks/zip.rake @@ -0,0 +1,43 @@ +require "zip" + +namespace :zip do + desc "build zip" + task :build => dist("heroku-#{version}.zip") + + desc "sign zip" + task :sign => dist("heroku-#{version}.zip.sha256") + + desc "release zip" + task :release => [:build, :sign] do |t| + s3_store dist("heroku-#{version}.zip"), "heroku-client/heroku-client-#{version}.zip" + s3_store dist("heroku-#{version}.zip"), "heroku-client/heroku-client-beta.zip" if beta? + s3_store dist("heroku-#{version}.zip"), "heroku-client/heroku-client.zip" unless beta? + + sh "heroku config:add UPDATE_HASH=#{zip_signature} -a toolbelt" unless beta? + end + + file dist("heroku-#{version}.zip") => distribution_files("zip") do |t| + tempdir do |dir| + mkdir "heroku-client" + cd "heroku-client" do + assemble_distribution + assemble_gems + Zip::File.open(t.name, Zip::File::CREATE) do |zip| + Dir["**/*"].each do |file| + zip.add(file, file) { true } + end + end + end + end + end + + file dist("heroku-#{version}.zip.sha256") => dist("heroku-#{version}.zip") do |t| + File.open(t.name, "w") do |file| + file.puts Digest::SHA256.file(t.prerequisites.first).hexdigest + end + end + + def zip_signature + File.read(dist("heroku-#{version}.zip.sha256")).chomp + end +end