diff --git a/.gitignore b/.gitignore index 4fc5d3fa0..e824b39ae 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.pfx 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..26de78352 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,749 @@ +3.42.25 2015-12-08 +================== +Use system & exit on windows when non-ascii args + +3.42.24 2015-12-04 +================== +Revert exec to shellescape while I fix windows bug + +3.42.23 2015-12-03 +================== +Update RELEASE-FULL.md OSX notes +switch to osslsigncode to fix windows installs +upgrade ruby to 2.1.7 and git to 2.6.3 on windows +small copy tweak +clear up unknown database error +Remove rendezvous spec that fails when not a tty +Add plugins:command to track progress +spaces: ensure we still request version 3 +Change exec(args) to exec(shelljoin(string)) +Remove readline dep & unused protected methods +show extra plugin info +Be more descriptive about uninstalling Accounts +Fix Windows cryllic character from ENV issues + +3.42.22 2015-11-18 +================== +prefix update messaging +update v4 plugins when running plugins:update +Adding verification test for config:get not found +use $XDG_DATA_HOME if available +Adding test for config:get --shell not found +Use attachment SSO url if it can be determined +Fixing tests so they can run under windows +Making the required init-wine task more obvious +Freeze net-ssh to 2.9.2 to preserve 1.9.3 support + +3.42.21 2015-10-30 +================== +Remove protocol prefix for buildpack URN +Fix documentation for pg:maintenance +Use v4 for login +Hide updating on windows +Remove v3 domain name variant + +3.42.20 2015-10-10 +================== +Prevent running v4 hidden commands by default + +3.42.19 2015-10-10 +================== +Fix help indices for v4 topics + +3.42.18 2015-10-09 +================== +Fix LOCALAPPDATA dir on Windows with non-ASCII characters +Increase timeout when downloading v4 + +3.42.17 2015-10-09 +================== +Make utf-8 the default encoding + +3.42.16 2015-10-08 +================== +Improve handling of shareable addons with postgres +Fix bug that deleted v4 if there was an issue reading commands +Ensure windows uninstall will remove everything +Update asynchronously +Report errors in removing old versions to rollbar + +3.42.15 2015-09-31 +================== +Catch all errors when attempting to delete old v4 release + +3.42.14 2015-09-30 +================== +Ensure v4 is not 4.24.* which won't autoupdate on Windows +Revert cygwin patch in 3.42.6 since it broke git bash + +3.42.13 2015-09-30 +================== +Resolve add-ons for pg:* commands with API + +3.42.12 2015-09-29 +================== +Fixed help for v4 commands that are overridden (such as `addons`) + +3.42.11 2015-09-28 +================== +Reverted addon removal until all v4 clients have it + +3.42.10 2015-09-28 +================== +Moved addons to v4 + +3.42.9 2015-09-28 +================= +Updated Gemfile.lock + +3.42.8 2015-09-28 +================= +Fixed auto-login with --app flags + +3.42.7 2015-09-28 +================= +Moved heroku-cli back to ~/.heroku/heroku-cli + +3.42.6 2015-09-28 +================= +Fix cygwin exec + +3.42.5 2015-09-28 +================= +Move v4 windows directory to LOCALAPPDATA + +3.42.4 2015-09-28 +================= +Move v4 windows directory to LOCALAPPDIR +Move help logic to v3 +Fix cygwin setup +Fix for non-ascii windows home directories +Allow debian users to manually update v4 +Fully deprecate ddollar/heroku-accounts + +3.42.3 2015-09-22 +================= +Require v4 CLI for all commands + +3.42.2 2015-09-21 +================= +Fix heroku-account deprecation warning + +3.42.1 2015-09-21 +================= +Re-release of 3.42.0 + +3.42.0 2015-09-21 +================= +Resolve addons with API +Consolidate addon output +Use HEROKU_FORCE for pg:* commands +Add support for CET and CEST zones +Added v4 2fa shim +Fixed login under windows machines without crtdll +Ensure HEROKU_HEADERS is used with orgs requests +Make warning for using heroku-accounts more admonishing +Display current dyno formation with ps:scale +Added HEROKU_DEBUG_HEADERS flag +Lock gem versions down +Fix domains call with very large number of domains +Removed redis shim + +3.41.5 2015-08-31 +================= +Added preauth to pg:backups capture +Fixed buildpack set with buildpack at end of list +Fix addons:open service plan resolution +Remove extra SQL command for execsql + +3.41.4 2015-08-24 +================= +Always display unjoined org apps +Ensure HEROKU_HEADERS are used for pg:backups commands +HEROKU_DEBUG improvements +Support custom kernels (internal only) + +3.41.3 2015-08-12 +================= +Fix incomplete release + +3.41.2 2015-08-12 +================= +Fix OSX releases without homebrew + +3.41.1 2015-08-12 +================= +Fix debian releases without foreman + +3.41.0 2015-08-12 +================= +Fixed OSX installer for El Capitan +Removed foreman in place of heroku local + +3.40.11 2015-08-11 +================== +Removed auto-tier switching logic + +3.40.10 2015-08-11 +================== +Update dyno costs + +3.40.9 2015-08-04 +================= +Update dyno costs +Use resource name for pg services +Fixed bugs with pgbackups + +3.40.7 2015-07-29 +================= +Automatic SSH tunneling for HPG + +3.40.6 2015-07-23 +================= +Fix for space conflicts wrt HEROKU_ORGANIZATION + +3.40.5 2015-07-22 +================= +Add space option to apps, apps:info, and apps:create +Change addons:add to addons:create +Clarify release guide +Display provider messages for addons upgrade/downgrade +block specifying both space and org to app list + + +3.39.5 2015-07-10 +================= +Group attachments by resource in pg:info +Added missing method used in pg:links +Fixed pg for sudo commands +Updated help for local +Fixed SSL bug with devclouds and org api + +3.39.4 2015-07-06 +================= +Fixed bug with v4 command name splitting + +3.39.2 2015-07-03 +================= +Error out if v4 plugin fails to install +Updated domains command +Fix v4 commands with ':' in command name +Fixed ps pricing for unknown types + +3.39.1 2015-06-26 +================= +Fix pg:links remote resolver + +3.39.0 2015-06-24 +================= +Add pg:links command + +3.38.3 2015-06-19 +================= +Added HEROKU_HEADERS to append extra request headers + +3.38.2 2015-06-19 +================= +Added --wait-interval to pgbackups commands +Fixed minor display bug with ps:scale +Added default flag for v4 commands + +3.38.1 2015-06-15 +================= +Fix ps:type for new process types +Made ps:type output consistent + +3.38.0 2015-06-15 +================= +Fix ps:scale for new process types + +3.37.7 2015-06-11 +================= +pgbackups bug fixes https://github.com/heroku/heroku/pull/1604 +Ensure errors are shown when there is an error displaying the error + +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..70204ecb5 100644 --- a/Gemfile +++ b/Gemfile @@ -3,21 +3,14 @@ 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 + gem "pry" end diff --git a/Gemfile.lock b/Gemfile.lock index c41b2a239..fa40df46a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,84 +1,106 @@ PATH remote: . specs: - heroku (3.9.4) - heroku-api (= 0.3.17) - launchy (>= 0.3.2) - netrc (~> 0.7.7) - rest-client (~> 1.6.1) - rubyzip + heroku (3.42.25) + heroku-api (= 0.3.23) + launchy (= 2.4.3) + multi_json (= 1.11.2) + net-ssh (= 2.9.2) + net-ssh-gateway (= 1.2.0) + netrc (= 0.10.3) + rest-client (= 1.6.8) + rubyzip (= 1.1.7) 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) + coderay (1.1.0) + 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.4) + 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) + method_source (0.8.2) + mime-types (1.25.1) + multi_json (1.11.2) + net-ssh (2.9.2) + net-ssh-gateway (1.2.0) + net-ssh (>= 2.6.5) + netrc (0.10.3) + pry (0.10.1) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + 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) + slop (3.6.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 + pry + rake + rr + rspec webmock + +BUNDLED WITH + 1.10.6 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-FULL.md b/RELEASE-FULL.md new file mode 100644 index 000000000..7eea7508b --- /dev/null +++ b/RELEASE-FULL.md @@ -0,0 +1,80 @@ +Heroku CLI Full Release Process +=============================== + +The following is how to do releases for OSX, Windows, and the "main" release (ubuntu, tgz, zip). If you are not a member of the CLI team and performing a release while they are out, you likely do not need to be concerned with this guide and can use the regular, much easier [release process](./RELEASE.md). + +## OSX Release + +Prerequisites: + +* OSX +* Heroku Developer ID Installer Certificate in Keychain + * `gpg --decrypt-files resources/pkg/certificate.p12.gpg` + * Enter OSX .p12 Certificate password (LastPass Shared CLI Secure Note) + * open resources/pkg/certificate.p12 + * There is no password on certificate.p12 (it was GPG encrypted instead) + * rm resources/pkg/certificate.p12 (you do not need it anymore) + +* `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`, `HEROKU_RELEASE_SECRET`, `HEROKU_WINDOWS_SIGNING_PASS` (from LastPass) +* Install [XQuartz](http://xquartz.macosforge.org/) manually, or via the terminal (restart required): +* `brew install osslsigncode` + +```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 pfx file decrypted from `resources/exe/heroku-codesign-cert.pfx.gpg` (password in LastPass) +* Initialize wine: `bundle exec rake exe:init-wine` + +To build for testing: `bundle exec rake exe:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.exe`. +To release: `bundle exec rake exe:release`. + +## 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. + +## Ruby versions + +Toolbelt bundles Ruby using different sources according to the OS: + +- 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. diff --git a/RELEASE.md b/RELEASE.md index a98ec01fb..f6d90eaee 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,47 +1,24 @@ Heroku CLI Release Process ========================== -### Ensure tests are passing +This is the normal guide on how to do a release. If you are not a member of the CLI team and would like to release a new version of the CLI while they are out, this is the guide you want. -* `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 + * Bump the minor level `Y` if the release contains new functionality or changes to existing functionality (bumping minor will also show a message to non-autoupdateable clients that a new version is out. Save these for either important releases, or features that need to go out.) +* 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://cli-build.herokai.com/. [Here is the code for the buildserver.](https://github.com/heroku/toolbelt-build-server) +* [optional] Release the OSX pkg (instructions in [full release guide](./RELEASE-FULL.md)) +* [optional] Release the WIN pkg (instructions in [full release guide](./RELEASE-FULL.md)) -### Release the gem - -* 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 - -### Release the toolbelt - -* 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) - -### 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}` -* Set the description to: - - +## When should the optional commands be run - A new version of the Heroku CLI is available with #{details}. +Because the toolbelt autoupdates after the first command is run, not releasing those versions just means the user will be on the previously released version until they run a command, then they'll be on the latest even if OSX and Windows were not released. - See the [CLI changelog](https://github.com/heroku/heroku/blob/master/CHANGELOG) for details and update by using `heroku update`. +For this reason, you can skip the OSX and Windows steps and probably should if you don't regularly release the CLI. This is because they involve getting a local environment setup to release the CLI and that is easier said than done. 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/resources/pkg/postinstall b/dist/resources/pkg/postinstall deleted file mode 100755 index f9cb2a062..000000000 --- a/dist/resources/pkg/postinstall +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh - -usershell=$(dscl localhost -read /Local/Default/Users/$USER shell | sed -e 's/[^ ]* //') - -startup_files() { - case $(basename $usershell) in - zsh) - echo ".zlogin .zshrc .zprofile .zshenv" - ;; - bash) - echo ".bashrc .bash_profile .bash_login .profile" - ;; - *) - echo ".bash_profile .zshrc .profile" - ;; - esac -} - -install_path() { - for file in $(startup_files); do - [ -f $HOME/$file ] || continue - (grep "Added by the Heroku" $HOME/$file >/dev/null) && break - - cat <>$HOME/$file - -### Added by the Heroku Toolbelt -export PATH="/usr/local/heroku/bin:\$PATH" -MESSAGE - - # done after we add to one file - break - done -} - -# if the toolbelt is not returned by `which`, let's add to the PATH -case $(which heroku) in - /usr/bin/heroku|/usr/local/heroku/bin/heroku) - ;; - *) - install_path - ;; -esac - -# symlink binary to /usr/bin/heroku -ln -sf /usr/local/heroku/bin/heroku /usr/bin/heroku 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..3aff6c37d 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -12,17 +12,21 @@ 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 "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 "heroku-api", "0.3.23" + gem.add_dependency "launchy", "2.4.3" + gem.add_dependency "netrc", "0.10.3" + gem.add_dependency "rest-client", "1.6.8" + gem.add_dependency "rubyzip", "1.1.7" + gem.add_dependency "multi_json", "1.11.2" + gem.add_dependency "net-ssh-gateway", "1.2.0" + gem.add_dependency "net-ssh", "2.9.2" # freeze net-ssh to 2.9.2 to preserve ruby 1.9.3 support 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/domains_v3.rb b/lib/heroku/api/domains_v3.rb new file mode 100644 index 000000000..079f90a4f --- /dev/null +++ b/lib/heroku/api/domains_v3.rb @@ -0,0 +1,33 @@ +module Heroku + class API + def get_domains_v3_domain_cname(app, range=nil) + rsp = request( + :expects => [200, 206], + :method => :get, + :path => "/apps/#{app}/domains", + :headers => { + 'Accept' => 'application/vnd.heroku+json; version=3', + 'Range' => range + } + ) + if rsp.headers['Next-Range'] + rsp.body + get_domains_v3_domain_cname(app, rsp.headers['Next-Range']) + else + rsp.body + end + end + + def post_domains_v3_domain_cname(app, hostname) + request( + :expects => 201, + :method => :post, + :path => "/apps/#{app}/domains", + :headers => { + 'Accept' => 'application/vnd.heroku+json; version=3', + 'Content-Type' => 'application/json' + }, + body: Heroku::Helpers.json_encode({'hostname' => hostname}) + ) + end + end +end diff --git a/lib/heroku/api/organizations_apps_v3.rb b/lib/heroku/api/organizations_apps_v3.rb new file mode 100644 index 000000000..2b6326577 --- /dev/null +++ b/lib/heroku/api/organizations_apps_v3.rb @@ -0,0 +1,15 @@ +module Heroku + class API + def post_organizations_app_v3(params={}) + request( + :method => :post, + :body => Heroku::Helpers.json_encode(params), + :expects => 201, + :path => "/organizations/apps", + :headers => { + "Accept" => "application/vnd.heroku+json; version=3" + } + ) + 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/api/spaces_v3.rb b/lib/heroku/api/spaces_v3.rb new file mode 100644 index 000000000..0e6caeb7e --- /dev/null +++ b/lib/heroku/api/spaces_v3.rb @@ -0,0 +1,14 @@ +module Heroku + class API + def get_space_v3(space_identity) + request( + :method => :get, + :expects => [200], + :path => "/spaces/#{space_identity}", + :headers => { + "Accept" => "application/vnd.heroku+json; version=3" + } + ) + end + end +end diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index d759a9188..48d2b3e33 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -2,6 +2,7 @@ require "heroku" require "heroku/client" require "heroku/helpers" +require "heroku/helpers/env" require "netrc" @@ -9,14 +10,19 @@ 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) + if ENV['HEROKU_HEADERS'] + headers = JSON.parse(ENV['HEROKU_HEADERS']) + params[:headers] ||= {} + params[:headers].merge!(headers) + end response = super if response.headers.has_key?('X-Heroku-Warning') Heroku::Command.warnings.concat(response.headers['X-Heroku-Warning'].split("\n")) @@ -37,7 +43,8 @@ def client end def login - delete_credentials + Heroku::JSPlugin.spawn('login', '', []) or exit + @netrc, @api, @client, @credentials = nil get_credentials end @@ -54,6 +61,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 +73,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 +89,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,15 +107,10 @@ 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: - @credentials ||= (read_credentials || ask_for_and_save_credentials) + @credentials ||= (read_credentials || login) end def delete_credentials @@ -102,8 +118,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 @@ -118,7 +135,10 @@ def legacy_credentials_path end def netrc_path - default = Netrc.default_path + default = File.join(Heroku::Helpers::Env['NETRC'] || home_directory, Netrc.netrc_filename) + # note: the ruby client tries to drop in `pwd` if home does not exist + # but the go client does not, so we do not want the fallback logic + encrypted = default + ".gpg" if File.exists?(encrypted) encrypted @@ -131,11 +151,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 +176,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 +197,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 +223,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 @@ -215,7 +247,7 @@ def ask_for_password_on_windows char = nil password = '' - while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do + while char = Win32API.new("msvcrt", "_getch", [ ], "L").Call do break if char == 10 || char == 13 # received carriage return or newline if char == 127 || char == 8 # backspace and delete password.slice!(-1, 1) @@ -240,45 +272,48 @@ 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" + @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 +325,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 +354,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..1834ae22f 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -1,51 +1,53 @@ +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 + +Encoding.default_internal, Encoding.default_external = ['utf-8'] * 2 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.setup + Heroku::JSPlugin.try_takeover(command, args) + require 'heroku/command' + Heroku::Git.check_git_version + Heroku::Command.load + warn_if_using_heroku_accounts + 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 + def self.warn_if_using_heroku_accounts + if defined?(Heroku::Command::Accounts.account) + $stderr.print "Uninstalling deprecated ddollar/heroku-accounts plugin..." + Heroku::Plugin.new('heroku-accounts').uninstall + $stderr.print "Done. Use https://github.com/heroku/heroku-accounts instead." + end + end end diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index 18594ac0a..013134679 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 @@ -645,15 +643,20 @@ def extract_warning(response) end def heroku_headers # :nodoc: - { + headers = { 'X-Heroku-API-Version' => '2', 'User-Agent' => Heroku.user_agent, 'X-Ruby-Version' => RUBY_VERSION, 'X-Ruby-Platform' => RUBY_PLATFORM } + if ENV['HEROKU_HEADERS'] + headers.merge! json_decode(ENV['HEROKU_HEADERS']) + end + headers 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..c636f4773 100644 --- a/lib/heroku/client/heroku_postgresql.rb +++ b/lib/heroku/client/heroku_postgresql.rb @@ -12,13 +12,15 @@ def self.add_headers(headers) end def self.headers + if ENV['HEROKU_HEADERS'] + @headers.merge! Heroku::Helpers.json_decode(ENV['HEROKU_HEADERS']) + end @headers end attr_reader :attachment def initialize(attachment) @attachment = attachment - require 'rest_client' end def heroku_postgresql_host @@ -58,6 +60,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 +101,61 @@ def maintenance_window_set(description) http_put "#{resource_name}/maintenance_window", 'description' => description end + # links + def link_list + http_get "#{resource_name}/links" + end + + def link_set(target, as = nil) + http_post "#{resource_name}/links", 'target' => target, 'as' => as + end + + def link_delete(id) + http_delete "#{resource_name}/links/#{id}" + 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 +212,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..314b624d1 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 @@ -12,6 +11,8 @@ def api options = {} key = Heroku::Auth.get_credentials[1] auth = "Basic #{Base64.encode64(':' + key).gsub("\n", '')}" hdrs = headers.merge( {"Authorization" => auth } ) + options[:ssl_verify_peer] = Heroku::Auth.verify_host?(Heroku::Auth.host) + options[:instrumentor] = HTTPInstrumentor if Heroku::Helpers.debugging? @connection = Excon.new(manager_url, options.merge(:headers => hdrs)) end @@ -23,6 +24,9 @@ def add_headers(headers) end def headers + if ENV['HEROKU_HEADERS'] + @headers.merge! Heroku::Helpers.json_decode(ENV['HEROKU_HEADERS']) + end @headers end @@ -57,7 +61,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 +89,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 +217,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 deleted file mode 100644 index be129f531..000000000 --- a/lib/heroku/client/pgbackups.rb +++ /dev/null @@ -1,113 +0,0 @@ -require "heroku/client" - -class Heroku::Client::Pgbackups - - include Heroku::Helpers - - def initialize(uri) - require 'rest_client' - @uri = URI.parse(uri) - end - - def authenticated_resource(path) - host = "#{@uri.scheme}://#{@uri.host}" - host += ":#{@uri.port}" if @uri.port - RestClient::Resource.new("#{host}#{path}", - :user => @uri.user, - :password => @uri.password, - :headers => {:x_heroku_gem_version => Heroku::Client.version} - ) - end - - def create_transfer(from_url, from_name, to_url, to_name, opts={}) - # opts[:expire] => true will delete the oldest backup if at the plan limit - resource = authenticated_resource("/client/transfers") - params = {:from_url => from_url, :from_name => from_name, :to_url => to_url, :to_name => to_name}.merge opts - json_decode post(resource, params).body - end - - def get_transfers - resource = authenticated_resource("/client/transfers") - json_decode get(resource).body - end - - def get_transfer(id) - resource = authenticated_resource("/client/transfers/#{id}") - json_decode get(resource).body - end - - def get_backups(opts={}) - resource = authenticated_resource("/client/backups") - json_decode get(resource).body - end - - def get_backup(name, opts={}) - name = URI.escape(name) - resource = authenticated_resource("/client/backups/#{name}") - json_decode get(resource).body - end - - def get_latest_backup - resource = authenticated_resource("/client/latest_backup") - json_decode get(resource).body - end - - def delete_backup(name) - name = URI.escape(name) - begin - resource = authenticated_resource("/client/backups/#{name}") - delete(resource).body - true - rescue RestClient::ResourceNotFound => e - false - end - end - - private - - def get(resource) - check_errors do - response = resource.get - display_heroku_warning response - response - end - end - - def post(resource, params) - check_errors do - response = resource.post(params) - display_heroku_warning response - response - end - end - - def delete(resource) - check_errors do - response = resource.delete - display_heroku_warning response - response - end - end - - def check_errors - yield - rescue RestClient::Unauthorized - error "Invalid PGBACKUPS_URL" - end - - def display_heroku_warning(response) - warning = response.headers[:x_heroku_warning] - display warning if warning - response - end - -end - -module Pgbackups - class Client < Heroku::Client::Pgbackups - def initialize(*args) - Heroku::Helpers.deprecate "Pgbackups::Client has been deprecated. Please use Heroku::Client::Pgbackups instead." - super - end - end -end diff --git a/lib/heroku/client/rendezvous.rb b/lib/heroku/client/rendezvous.rb index 93d6b1acc..a2c75cf86 100644 --- a/lib/heroku/client/rendezvous.rb +++ b/lib/heroku/client/rendezvous.rb @@ -97,9 +97,7 @@ def start def fixup(data) return nil if ! data - if data.respond_to?(:force_encoding) - data.force_encoding('utf-8') if data.respond_to?(:force_encoding) - end + data.force_encoding('utf-8') if data.respond_to?(:force_encoding) if running_on_windows? begin data.gsub!(/\e\[[\d;]+m/, '') diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index f944955e9..503823d23 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,20 +208,11 @@ 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 - rescue Heroku::API::Errors::VerificationRequired, RestClient::PaymentRequired => e - retry if Heroku::Helpers.confirm_billing rescue Heroku::API::Errors::NotFound => e error extract_error(e.response.body) { e.response.body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" @@ -236,7 +222,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 +237,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 +249,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,15 +267,14 @@ 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 puts "Authentication failure" - run "login" + Heroku::Auth.login true end end @@ -291,14 +285,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..326723881 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -1,185 +1,395 @@ 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 - # manage addon resources + # manage add-on resources # 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 + # + # create an add-on resource # - # list all available addons + # --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 # - # --region REGION # specify a region for addon availability + 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 # - #Example: + # attach add-on resource to an app # - # $ heroku addons:list --region eu - # === available - # adept-scale:battleship, corvette... - # adminium:enterprise, petproject... + # --as ATTACHMENT_NAME # (optional) name for add-on attachment + # --confirm APP_NAME # overwrite existing add-on attachment with same name # - 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) + 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", + "X-Heroku-Legacy-Provider-Messages" => "true" + }, + :method => :patch, + :path => "/apps/#{app}/addons/#{addon_name}" + ).body + @status = "(#{format_price @addon['plan']['price']})" if @addon['plan'].has_key?('price') end + display @addon['provision_message'] unless @addon['provision_message'].to_s.strip == "" 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 ...] + # + # destroy add-on resources # - # uninstall one or more addons + # -f, --force # allow destruction even if add-on is attached to other apps # - def remove - return unless confirm_command + def destroy + if current_command == "addons:remove" + deprecate("`heroku #{current_command}` has been deprecated. Please use `heroku addons:destroy` instead.") + end + + raise CommandFailed.new("Missing add-on name") if args.empty? + + requires_preauth + confirmed_apps = [] - args.each do |name| - messages = nil - if name.start_with? "HEROKU_POSTGRESQL_" - name = name.chomp("_URL").freeze + 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] || ENV['HEROKU_FORCE'] == '1', + }), + :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?(':') || get_service(identifier) + identifier = identifier.split(':').first + launchy("Opening #{identifier} docs", addon_docs_url(identifier)) 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 + addon = resolve_addon!(identifier) + service = addon['addon_service']['name'] + launchy("Opening #{service} docs", addon_docs_url(service)) 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! - - app_addons = api.get_addons(app).body.map {|a| a['name']} - matches = app_addons.select {|a| a =~ /^#{addon}/}.sort - - 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")) + requires_preauth + + addon = resolve_addon!(addon_name) + web_url = addon['web_url'] + + begin + attachment = resolve_attachment!(addon_name) + web_url = attachment['web_url'] + rescue Heroku::API::Errors::NotFound + # no-op + rescue Heroku::API::Errors::RequestFailed => e + if MultiJson.decode(e.response.body)["id"] != "multiple_matches" + raise 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']}", web_url) end private @@ -188,109 +398,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..899a4e7a8 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -1,4 +1,6 @@ require "heroku/command/base" +require "heroku/command/stack" +require "heroku/api/organizations_apps_v3" # manage apps (create, destroy) # @@ -8,9 +10,10 @@ class Heroku::Command::Apps < Heroku::Command::Base # # list your apps # - # -o, --org ORG # the org to list the apps for - # -A, --all # list all apps in the org. Not just joined apps - # -p, --personal # list apps in personal account when a default org is set + # -o, --org ORG # the org to list the apps for + # --space SPACE # HIDDEN: list apps in a given space + # -A, --all # list all collaborated apps, including joined org apps in personal app list + # -p, --personal # list apps in personal account when a default org is set # #Example: # @@ -32,28 +35,16 @@ def index api.get_apps.body.select { |app| options[:all] ? true : !org?(app["owner_email"]) } end + if options[:space] + apps.select! do |app| + app["space"] && [app["space"]["name"], app["space"]["id"]].include?(options[:space]) + end + end + unless apps.empty? if org - joined, unjoined = apps.partition { |app| app['joined'] == true } - - styled_header("Apps joined in organization #{org}") - unless joined.empty? - styled_array(joined.map {|app| regionized_app_name(app) + (app['locked'] ? ' (locked)' : '') }) - else - display("You haven't joined any apps.") - display("Use --all to see unjoined apps.") unless options[:all] - display - end - - if options[:all] - styled_header("Apps available to join in organization #{org}") - unless unjoined.empty? - styled_array(unjoined.map {|app| regionized_app_name(app) + (app['locked'] ? ' (locked)' : '') }) - else - display("There are no apps to join.") - display - end - end + styled_header(in_message("Apps", app_in_msg_opts)) + styled_array(apps.map {|app| regionized_app_name(app) + (app['locked'] ? ' (locked)' : '') }) else my_apps, collaborated_apps = apps.partition { |app| app["owner_email"] == Heroku::Auth.user } @@ -68,125 +59,15 @@ def index end end else - org ? display("There are no apps in organization #{org}.") : display("You have no apps.") - end - end - - alias_command "list", "apps" - - # apps:info - # - # show detailed app information - # - # -s, --shell # output more shell friendly key/value pairs - # - #Examples: - # - # $ heroku apps:info - # === example - # Git URL: git@heroku.com:example.git - # Repo Size: 5M - # ... - # - # $ heroku apps:info --shell - # git_url=git@heroku.com:example.git - # repo_size=5000000 - # ... - # - def info - validate_arguments! - 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 - collaborators_data = api.get_collaborators(app).body.map {|collaborator| collaborator["email"]}.sort - collaborators_data.reject! {|email| email == app_data["owner_email"]} - - if org? app_data['owner_email'] - app_data['owner'] = app_owner(app_data['owner_email']) - app_data.delete("owner_email") - end - - if options[:shell] - if app_data['domain_name'] - app_data['domain_name'] = app_data['domain_name']['domain'] - end - unless addons_data.empty? - app_data['addons'] = addons_data.join(',') - end - unless collaborators_data.empty? - app_data['collaborators'] = collaborators_data.join(',') - end - app_data.keys.sort_by { |a| a.to_s }.each do |key| - hputs("#{key}=#{app_data[key]}") - end - else - data = {} - - unless addons_data.empty? - data["Addons"] = addons_data - end - - if app_data["archived_at"] - data["Archived At"] = format_date(app_data["archived_at"]) - end - - data["Collaborators"] = collaborators_data - - if app_data["create_status"] && app_data["create_status"] != "complete" - data["Create Status"] = app_data["create_status"] - end - - if app_data["cron_finished_at"] - data["Cron Finished At"] = format_date(app_data["cron_finished_at"]) - end - - if app_data["cron_next_run"] - data["Cron Next Run"] = format_date(app_data["cron_next_run"]) - end - - if app_data["database_size"] - data["Database Size"] = format_bytes(app_data["database_size"]) - end - - data["Git URL"] = app_data["git_url"] - - if app_data["database_tables"] - data["Database Size"].gsub!('(empty)', '0K') + " in #{quantify("table", app_data["database_tables"])}" - end - - if app_data["dyno_hours"].is_a?(Hash) - data["Dyno Hours"] = app_data["dyno_hours"].keys.map do |type| - "%s - %0.2f dyno-hours" % [ type.to_s.capitalize, app_data["dyno_hours"][type] ] - end - end - - data["Owner Email"] = app_data["owner_email"] if app_data["owner_email"] - data["Owner"] = app_data["owner"] if app_data["owner"] - data["Region"] = app_data["region"] if app_data["region"] - data["Repo Size"] = format_bytes(app_data["repo_size"]) if app_data["repo_size"] - 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.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 + if org + display("#{in_message("There are no apps", app_in_msg_opts)}.") + else + display("You have no apps.") end - - styled_hash(data) end end - alias_command "info", "apps:info" + alias_command "list", "apps" # apps:create [NAME] # @@ -196,25 +77,30 @@ def info # -b, --buildpack BUILDPACK # a buildpack url to use for this app # -n, --no-remote # don't create a git remote # -r, --remote REMOTE # the git remote to create, default "heroku" + # --space SPACE # HIDDEN: the space in which to create the app # -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 + # -k, --kernel KERNEL # HIDDEN: Use a custom platform kernel # #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 @@ -226,22 +112,27 @@ def create name = shift_argument || options[:app] || ENV['HEROKU_APP'] validate_arguments! options[:ignore_no_org] = true + validate_space_xor_org! params = { "name" => name, "region" => options[:region], - "stack" => options[:stack], + "space" => options[:space], + "stack" => Heroku::Command::Stack::Codex.in(options[:stack]), + "kernel" => options[:kernel], "locked" => options[:locked] } - info = if org + info = if options[:space] + api.post_organizations_app_v3(params).body + elsif org org_api.post_app(params, org).body else api.post_app(params).body end begin - action("Creating #{info['name']}", :org => !!org) do + action("Creating #{info['name']}", app_in_msg_opts) do if info['create_status'] == 'creating' Timeout::timeout(options[:timeout].to_i) do loop do @@ -254,7 +145,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 +157,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 +177,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 +198,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 @@ -496,4 +390,12 @@ def region_from_app app region = app["region"].is_a?(Hash) ? app["region"]["name"] : app["region"] end + def app_in_msg_opts + display_org = !!org + if options[:space] + space_name = options[:space] + display_org = false + end + { :org => display_org, :space => space_name } + end end 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..a11267981 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -3,6 +3,7 @@ require "heroku/client/rendezvous" require "heroku/client/organizations" require "heroku/command" +require "heroku/api/spaces_v3" class Heroku::Command::Base include Heroku::Helpers @@ -20,7 +21,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,32 +42,32 @@ def org @nil = false options[:ignore_no_app] = true - @org ||= if skip_org? - nil + @org ||= if options[:space].is_a?(String) + validate_space_xor_org! + api.get_space_v3(options[:space]).body['organization']['name'] elsif 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 @org end + def validate_space_xor_org! + if options[:space] && options[:org] + error "Specify option for space or org, but not both." + end + end + def api Heroku::Auth.api end @@ -217,7 +218,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 +230,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 +255,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 +273,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 +291,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..35d77ece5 --- /dev/null +++ b/lib/heroku/command/buildpacks.rb @@ -0,0 +1,237 @@ +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 => to_buildpack_name(url)} }}) + display_buildpack_change(buildpack_urls, action) + end + + def display_buildpacks(buildpacks, indent=" ") + buildpacks.map!{|bp| to_buildpack_name(bp)} + 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 + + def to_buildpack_name(buildpack_url) + buildpack_url. + gsub(/^urn:buildpack:/, ''). + gsub(%r{^https://codon-buildpacks\.s3\.amazonaws\.com/buildpacks/heroku/(.*)\.tgz$}, 'heroku/\1') + end + + end +end diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 573013832..8bd9b89fc 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 @@ -187,10 +226,6 @@ def display_warnings(endpoint) end end - def display(msg = "", new_line = true) - super if $stdout.tty? - end - def post_to_ssl_doctor(path, action_text = nil) raise UsageError if args.size < 1 action_text ||= "Resolving trust chain" @@ -198,8 +233,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 +247,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 +265,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/domains.rb b/lib/heroku/command/domains.rb index 00ef0dac2..cf8d6f7f9 100644 --- a/lib/heroku/command/domains.rb +++ b/lib/heroku/command/domains.rb @@ -1,29 +1,48 @@ require "heroku/command/base" +require "heroku/api/domains_v3" module Heroku::Command - # manage custom domains + # manage domains # class Domains < Base # domains # - # list custom domains for an app + # list domains for an app # #Examples: # # $ heroku domains - # === Domain names for example - # example.com + # === example Heroku Domain + # example.herokuapp.com + # + # === example Custom Domains + # Domain Name DNS Target + # ----------- --------------------- + # example.com example.herokuapp.com # def index validate_arguments! - domains = api.get_domains(app).body - if domains.length > 0 - styled_header("#{app} Domain Names") - styled_array domains.map {|domain| domain["domain"]} + domains = api.get_domains_v3_domain_cname(app) + + styled_header("#{app} Heroku Domain") + heroku_domain = domains.detect { |d| d['kind'] == 'heroku' || d['kind'] == 'default' } # TODO: remove 'default' after API change + if heroku_domain + display heroku_domain['hostname'] else - display("#{app} has no domain names.") + output_with_bang "Not found" + end + + display + + styled_header("#{app} Custom Domains") + custom_domains = domains.select{ |d| d['kind'] == 'custom' } + if custom_domains.length > 0 + display_table(custom_domains, ['hostname', 'cname'], ['Domain Name', 'DNS Target']) + else + display("#{app} has no custom domains.") + display("Use `heroku domains:add DOMAIN` to add one.") end end @@ -36,14 +55,19 @@ def index # $ heroku domains:add example.com # Adding example.com to example... done # + # ! Configure your app's DNS provider to point to the DNS Target example.herokuapp.com + # ! For help with custom domains, see https://devcenter.heroku.com/articles/custom-domains + # def add unless domain = shift_argument error("Usage: heroku domains:add DOMAIN\nMust specify DOMAIN to add.") end validate_arguments! - action("Adding #{domain} to #{app}") do - api.post_domain(app, domain) + domain = action("Adding #{domain} to #{app}") do + api.post_domains_v3_domain_cname(app, domain).body end + output_with_bang "Configure your app's DNS provider to point to the DNS Target #{domain['cname']}" + output_with_bang "For help, see https://devcenter.heroku.com/articles/custom-domains" end # domains:remove DOMAIN 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 deleted file mode 100644 index f92bb2d29..000000000 --- a/lib/heroku/command/fork.rb +++ /dev/null @@ -1,180 +0,0 @@ -require "heroku/api/releases_v3" -require "heroku/command/base" - -module Heroku::Command - - # clone an existing app - # - class Fork < Base - - # fork [NEWNAME] - # - # Fork an existing app -- copy config vars and Heroku Postgres data, and re-provision add-ons to a new app. - # New app name should not be an existing app. The new app will be created as part of the forking process. - # - # -s, --stack STACK # specify a stack for the new app - # --region REGION # specify a region - # - def index - options[:ignore_no_org] = true - - from = app - to = shift_argument || "#{from}-#{(rand*1000).to_i}" - if from == to - raise Heroku::Command::CommandFailed.new("Cannot fork to the same app.") - end - - from_info = api.get_app(from).body - - to_info = action("Creating fork #{to}", :org => !!org) do - params = { - "name" => to, - "region" => options[:region] || from_info["region"], - "stack" => options[:stack] || from_info["stack"], - "tier" => from_info["tier"] == "legacy" ? "production" : from_info["tier"] - } - - info = if org - org_api.post_app(params, org).body - else - api.post_app(params).body - end - end - - action("Copying slug") do - copy_slug(from, to) - end - - from_config = api.get_config_vars(from).body - from_addons = api.get_addons(from).body - - from_addons.each do |addon| - print "Adding #{addon["name"]}... " - begin - to_addon = api.post_addon(to, addon["name"]).body - puts "done" - rescue Heroku::API::Errors::RequestFailed => ex - puts "skipped (%s)" % json_decode(ex.response.body)["error"] - rescue Heroku::API::Errors::NotFound - puts "skipped (not found)" - end - if addon["name"] =~ /^heroku-postgresql:/ - from_var_name = "#{addon["attachment_name"]}_URL" - from_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - if from_config[from_var_name] == from_config["DATABASE_URL"] - from_config["DATABASE_URL"] = api.get_config_vars(to).body["#{from_attachment}_URL"] - end - from_config.delete(from_var_name) - - plan = addon["name"].split(":").last - unless %w(dev basic hobby-dev hobby-basic).include? plan - wait_for_db to, to_addon - end - - check_for_pgbackups! from - check_for_pgbackups! to - migrate_db addon, from, to_addon, to - end - end - - to_config = api.get_config_vars(to).body - - action("Copying config vars") do - diff = from_config.inject({}) do |ax, (key, val)| - ax[key] = val unless to_config[key] - ax - end - api.put_config_vars to, diff - end - - puts "Fork complete, view it at #{to_info['web_url']}" - rescue Exception => e - raise if e.is_a?(Heroku::Command::CommandFailed) - - puts "Failed to fork app #{from} to #{to}." - message = "WARNING: Potentially Destructive Action\nThis command will destroy #{to} (including all add-ons)." - - if confirm_command(to, message) - action("Deleting #{to}") do - begin - api.delete_app(to) - rescue Heroku::API::Errors::NotFound - end - end - end - puts "Original exception below:" - raise e - end - - private - - 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 deleted file mode 100644 index b54e80076..000000000 --- a/lib/heroku/command/git.rb +++ /dev/null @@ -1,64 +0,0 @@ -require "heroku/command/base" - -# manage git for apps -# -class Heroku::Command::Git < Heroku::Command::Base - - # git:clone APP [DIRECTORY] - # - # clones a heroku app to your local machine at DIRECTORY (defaults to app name) - # - # -r, --remote REMOTE # the git remote to create, default "heroku" - # - #Examples: - # - # $ heroku git:clone example - # Cloning from app '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 - end - - alias_command "clone", "git:clone" - - # git:remote [OPTIONS] - # - # adds a git remote to an app repo - # - # if OPTIONS are specified they will be passed to git remote add - # - # -r, --remote REMOTE # the git remote to create, default "heroku" - # - #Examples: - # - # $ heroku git:remote -a example - # Git remote heroku added - # - # $ heroku git:remote -a example - # ! Git remote heroku already exists - # - 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 - end - -end diff --git a/lib/heroku/command/help.rb b/lib/heroku/command/help.rb index 2fa8449e2..4eba1336a 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 @@ -148,7 +149,7 @@ def help_for_command(name) display("Alias: #{name} redirects to #{command_alias}") name = command_alias end - if command = commands[name] + if command = Heroku::JSPlugin.find_command(name) || commands[name] puts "Usage: heroku #{command[:banner]}" if command[:help].strip.length > 0 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/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 deleted file mode 100644 index eb7d887a4..000000000 --- a/lib/heroku/command/maintenance.rb +++ /dev/null @@ -1,61 +0,0 @@ -require "heroku/command/base" - -# manage maintenance mode for an app -# -class Heroku::Command::Maintenance < Heroku::Command::Base - - # maintenance - # - # display the current maintenance status of app - # - #Example: - # - # $ heroku maintenance - # off - # - def index - validate_arguments! - - case api.get_app_maintenance(app).body['maintenance'] - when true - display('on') - when false - display('off') - end - end - - # maintenance:on - # - # put the app into maintenance mode - # - #Example: - # - # $ heroku maintenance:on - # Enabling maintenance mode for example - # - def on - validate_arguments! - - action("Enabling maintenance mode for #{app}") do - api.post_app_maintenance(app, '1') - end - end - - # maintenance:off - # - # take the app out of maintenance mode - # - #Example: - # - # $ heroku maintenance:off - # Disabling maintenance mode for example - # - def off - validate_arguments! - - action("Disabling maintenance mode for #{app}") do - api.post_app_maintenance(app, '0') - end - 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..0b92cbbce 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? @@ -32,15 +42,16 @@ def index # pg:info [DATABASE] # - # -x, --extended # Show extended information - # # display database information # + # -x, --extended # Show extended information + # # If DATABASE is not specified, displays all databases # 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,27 +79,57 @@ 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) + addon = resolve_addon!(db) + + if addon['addon_service']['name'] != 'heroku-postgresql' + name = db == addon['name'] ? db : "#{db} (#{addon['name']})" + error("Cannot promote #{name}. It needs to be heroku-postgresql, not #{addon['addon_service']['name']}.") + end + + 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 + + end - action "Promoting #{attachment.display_name} to DATABASE_URL" do - hpg_promote(attachment.url) + 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 # pg:psql [DATABASE] # - # -c, --command COMMAND # optional SQL command to run - # # open a psql shell to the database # + # -c, --command COMMAND # optional SQL command to run + # # 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 +137,26 @@ 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]}" + attachment.maybe_tunnel do |uri| + command = "psql -U #{uri.user} -h #{uri.host} -p #{uri.port || 5432} #{set_commands} #{prompt_flags} #{command} #{uri.path[1..-1]}" + if attachment.uses_bastion? + spawn(command) + Process.wait + exit($?.exitstatus) + else + exec(command) + end + end 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 +169,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 +191,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 +224,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 +249,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 +281,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 +297,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,11 +321,12 @@ 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 - cmd = options[:force] ? 'pg_terminate_backend' : 'pg_cancel_backend' + cmd = force? ? 'pg_terminate_backend' : 'pg_cancel_backend' sql = %Q(SELECT #{cmd}(#{procpid});) puts exec_sql(sql) @@ -268,6 +337,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,71 +357,77 @@ 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}" - pgdr = PgDumpRestore.new( - local_uri, - remote_uri, - self) + target_attachment = resolve_heroku_attachment(remote) + source_uri = parse_db_url(local) - pgdr.execute + target_attachment.maybe_tunnel do |uri| + pgdr = PgDumpRestore.new( + source_uri, + uri.to_s, + self) + pgdr.execute + end end - # pg:pull + # pg:pull + # + # pull from REMOTE_SOURCE_DATABASE to TARGET_DATABASE + # TARGET_DATABASE must not already exist. # - # pull from REMOTE_SOURCE_DATABASE to LOCAL_TARGET_DATABASE - # LOCAL_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}" - pgdr = PgDumpRestore.new( - remote_uri, - local_uri, - self) + source_attachment = resolve_heroku_attachment(remote) + target_uri = parse_db_url(local) - pgdr.execute + source_attachment.maybe_tunnel do |uri| + pgdr = PgDumpRestore.new( + uri.to_s, + target_uri, + self) + pgdr.execute + end end - # pg:maintenance + # pg:maintenance # - # manage maintenance for - # info # show current maintenance information - # run # start maintenance - # -f, --force # run pg:maintenance without entering application maintenance mode - # window="" # set weekly UTC maintenance window for DATABASE + # manage maintenance for + # info # show current maintenance information + # run # start maintenance + # -f, --force # run pg:maintenance without entering application maintenance mode + # 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] + no_maintenance = force? if mode.nil? || db.nil? || !(%w[info run window].include? mode) Heroku::Command.run(current_command, ["--help"]) exit(1) @@ -382,6 +466,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 @@ -418,15 +503,140 @@ def upgrade end end + # pg:links + # + # create links between data stores. Without a subcommand, it lists all + # databases and information on the link. + # + # create # Create a data link + # --as # override the default link name + # destroy # Destroy a data link between a local and remote database + # + def links + requires_preauth + mode = shift_argument || 'list' + + if !(%w(list create destroy).include?(mode)) + Heroku::Command.run(current_command, ["--help"]) + exit(1) + end + + case mode + when 'list' + db = shift_argument + resolver = generate_resolver + + if db + dbs = [resolver.resolve(db, "DATABASE_URL")] + else + dbs = resolver.all_databases.values + end + + dbs_by_addons = dbs.group_by(&:resource_name) + error("No database attached to this app.") if dbs.compact.empty? + + dbs_by_addons.each_with_index do |(resource, attachments), index| + response = hpg_client(attachments.first).link_list + display "\n" if index.nonzero? + + styled_header("#{attachments.map(&:config_var).join(", ")} (#{resource})") + + next display response[:message] if response.kind_of?(Hash) + next display "No data sources are linked into this database." if response.empty? + + response.each do |link| + display "==== #{link[:name]}" + + link[:created] = time_format(link[:created_at]) + link[:remote] = "#{link[:remote]['attachment_name']} (#{link[:remote]['name']})" + link.reject! { |k,_| [:id, :created_at, :name].include?(k) } + styled_hash(Hash[link.map {|k, v| [humanize(k), v] }]) + end + end + when 'create' + remote = shift_argument + local = shift_argument -private + error("Usage links ") unless [local, remote].all? + + local_attachment = generate_resolver.resolve(local, "DATABASE_URL") + remote_attachment = resolve_service(remote) + + output_with_bang("No source database specified.") unless local_attachment + output_with_bang("No remote database specified.") unless remote_attachment + + response = hpg_client(local_attachment).link_set(remote_attachment.name, options[:as]) + + if response.has_key?(:message) + output_with_bang(response[:message]) + else + display("New link '#{response[:name]}' successfully created.") + end + when 'destroy' + local = shift_argument + link = shift_argument + + error("No local database specified.") unless local + error("No link name specified.") unless link + + local_attachment = generate_resolver.resolve(local, "DATABASE_URL") + + message = [ + "WARNING: Destructive Action", + "This command will affect the database: #{local}", + "This will delete #{link} along with the tables and views created within it.", + "This may have adverse effects for software written against the #{link} schema." + ].join("\n") + + if confirm_command(app, message) + action("Deleting link #{link} in #{local}") do + hpg_client(local_attachment).link_delete(link) + end + end + end + end + + private + + def humanize(key) + key.to_s.gsub(/_/, ' ').split(" ").map(&:capitalize).join(" ") + end + + def resolve_service(name) + addon = resolve_addon!(name) + + error("Remote database is invalid.") unless addon['addon_service']['name'] =~ /heroku-(redis|postgresql)/ + + MaybeAttachment.new(addon['name'], nil, addon) + rescue Heroku::API::Errors::NotFound + error("Remote database could not be found.") + end + + def get_config_var(name) + res = api.get_config_vars(app) + res.data[:body][name] + end + + def resolve_heroku_attachment(remote) + generate_resolver.resolve(remote) + 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 +654,14 @@ def display_db(name, db) display end + def in_maintenance?(app) + api.get_app_maintenance(app).body['maintenance'] + end + + def time_format(time) + Time.parse(time).getutc.strftime("%Y-%m-%d %H:%M %Z") + end + def hpg_client(attachment) Heroku::Client::HerokuPostgresql.new(attachment) end @@ -452,22 +670,30 @@ def hpg_databases_with_info return @hpg_databases_with_info if @hpg_databases_with_info @resolver = generate_resolver - dbs = @resolver.all_databases + attachments = @resolver.all_databases - unique_dbs = dbs.reject { |config, att| 'DATABASE_URL' == config }.map{|config, att| att}.compact + attachments_by_db = attachments.values.group_by(&:resource_name) db_infos = {} mutex = Mutex.new - threads = (0..unique_dbs.size-1).map do |i| + threads = attachments_by_db.map do |resource, attachments| Thread.new do - att = unique_dbs[i] begin - info = hpg_info(att, options[:extended]) + info = hpg_info(attachments.first, options[:extended]) rescue info = nil end + + # Make headers as per heroku/heroku#1605 + names = attachments.map(&:config_var) + names << 'DATABASE_URL' if attachments.any? { |att| att.primary_attachment? } + name = names. + uniq. + sort_by { |n| n=='DATABASE_URL' ? '{' : n }. # Weight DATABASE_URL last + join(', ') + mutex.synchronize do - db_infos[att.display_name] = info + db_infos[name] = info end end end @@ -478,7 +704,11 @@ def hpg_databases_with_info end def hpg_info(attachment, extended=false) - hpg_client(attachment).get_database(extended) + info = hpg_client(attachment).get_database(extended) + + # TODO: Make this the section title and list the current `name` as an + # "Attachments" item here: + info.merge(:info => info[:info] + [{"name" => "Add-on", "values" => [attachment.resource_name]}]) end def hpg_info_display(item) @@ -492,17 +722,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? @@ -515,22 +745,11 @@ def wait_for(attach) end end - def find_uri - return @uri if defined? @uri - - attachment = generate_resolver.resolve(shift_argument, "DATABASE_URL") - if attachment.kind_of? Array - uri = URI.parse( attachment.last ) - else - uri = URI.parse( attachment.url ) - end - - @uri = uri - end - 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? @@ -555,16 +774,23 @@ def query_column end def exec_sql(sql) - uri = find_uri - exec_sql_on_uri(sql, uri) + @attachment ||= generate_resolver.resolve(shift_argument, "DATABASE_URL") + @attachment.maybe_tunnel do |uri| + exec_sql_on_uri(sql, uri) + end end 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 +798,57 @@ 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 + + def force? + options[:force] || ENV['HEROKU_FORCE'] == '1' + end end diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb new file mode 100644 index 000000000..e3dd95004 --- /dev/null +++ b/lib/heroku/command/pg_backups.rb @@ -0,0 +1,673 @@ +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 + # + # --wait-interval SECONDS # how frequently to poll (to avoid rate-limiting) + # + # 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! + + interval = options[:wait_interval].to_i || 3 + interval = [3, interval].max + + 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 + + if target.attachment.nil? + target_url = URI.parse(target.url) + confirm_with = target_url.path[1..-1] + confirm_with = target_url.host if confirm_with.empty? + affected = target.name.downcase + else + confirm_with = target.attachment.app + affected = "the app: #{target.attachment.app}" + end + + 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 #{affected}" + + if confirm_command(confirm_with, message) + xfer = hpg_client(attachment).pg_copy(source.name, source.url, + target.name, target.url) + poll_transfer('copy', xfer[:uuid], interval) + 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 + # --wait-interval SECONDS # how frequently to poll (to avoid rate-limiting) + # restore [[BACKUP_ID] DATABASE] # restore a backup (default latest) to a database (default DATABASE_URL) + # --wait-interval SECONDS # how frequently to poll (to avoid rate-limiting) + # 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.find do |attachment| + attachment.billing_app == app + end + 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] + warnings = t[:warnings] + if warnings && warnings > 0 + "Finished with #{warnings} warnings" #{t[:finished_at]}" + else + "Completed #{t[:finished_at]}" + end + 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] + warnings = backup[:warnings] + if warnings && warnings > 0 + "Finished with #{warnings} warnings" + else + "Completed" + end + 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 + requires_preauth + db = shift_argument + attachment = generate_resolver.resolve(db, "DATABASE_URL") + validate_arguments! + + interval = options[:wait_interval].to_i || 3 + interval = [3, interval].max + + + 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], interval) + end + + def restore_backup + # heroku pg:backups restore [[backup_id] database] + requires_preauth + 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! + interval = options[:wait_interval].to_i || 3 + interval = [3, interval].max + + restore_url = nil + if restore_from =~ %r{\Ahttps?://} + restore_url = restore_from + else + # assume we're restoring from a backup + if restore_from =~ /::/ + backup_app, backup_name = restore_from.split('::') + else + backup_app, backup_name = [app, restore_from] + end + backups = hpg_app_client(backup_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 for #{backup_app}. Capture one with `heroku pg:backups capture`.") + elsif backup.nil? + abort("Backup #{backup_name} not found for #{backup_app}.") + elsif !backup[:succeeded] + abort("Backup #{backup_name} for #{backup_app} did not complete successfully; cannot restore it.") + end + restore_url = backup[:to_url] + end + + if confirm_command(attachment.app) + 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], interval) + end + end + + def poll_transfer(action, transfer_id, interval) + # 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 interval + 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] + if !at + error("You must specifiy a time to schedule backups, i.e --at '04:00 UTC'") + end + + 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 do |k,v| + k =~ /#{db}/i && v == attachment.url + end + if alias_url.nil? + 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', + 'Z' => 'UTC', + 'GMT' => 'Europe/London', + 'BST' => 'Europe/London', + 'CET' => 'Europe/Paris', + 'CEST' => 'Europe/Paris' + } + 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..5ae3888e2 100644 --- a/lib/heroku/command/pgbackups.rb +++ b/lib/heroku/command/pgbackups.rb @@ -1,42 +1,19 @@ -require "heroku/client/pgbackups" require "heroku/command/base" -require "heroku/helpers/heroku_postgresql" module Heroku::Command # manage backups of heroku postgresql databases - class Pgbackups < Base - - include Heroku::Helpers::HerokuPostgresql + # removed: see heroku pg:backups + class Pgbackups < Base # pgbackups # # list captured backups # def index - validate_arguments! - - backups = [] - pgbackup_client.get_transfers.each { |t| - next unless backup_types.member?(t['to_name']) && !t['error_at'] && !t['destroyed_at'] - backups << { - 'id' => backup_name(t['to_url']), - 'started_at' => t['started_at'], - 'status' => transfer_status(t), - 'size' => t['size'], - 'database' => t['from_name'] - } - } - - if backups.empty? - no_backups_error! - else - display_table( - backups, - %w{ id started_at status size database }, - ["ID", "Backup Time", "Status", "Size", "Database"] - ) - end + error("'heroku pgbackups' has been removed. +Please see 'heroku pg:backups' instead. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end # pgbackups:url [BACKUP_ID] @@ -44,22 +21,9 @@ def index # get a temporary URL for a backup # def url - name = shift_argument - validate_arguments! - - if name - b = pgbackup_client.get_backup(name) - else - b = pgbackup_client.get_latest_backup - end - unless b['public_url'] - error("No backup found.") - end - if $stdout.isatty - display '"'+b['public_url']+'"' - else - display b['public_url'] - end + error("'heroku pgbackups url' has been removed. +Please see 'heroku pg:backups public-url' instead. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end # pgbackups:capture [DATABASE] @@ -71,31 +35,9 @@ def url # -e, --expire # if no slots are available, destroy the oldest manual backup to make room # def capture - attachment = resolve(shift_argument, "DATABASE_URL") - validate_arguments! - - from_name = attachment.display_name - from_url = attachment.url - to_url = nil # server will assign - to_name = "BACKUP" - - opts = {:expire => options[:expire]} - - backup = transfer!(from_url, from_name, to_url, to_name, opts) - - to_uri = URI.parse backup["to_url"] - backup_id = to_uri.path.empty? ? "error" : File.basename(to_uri.path, '.*') - display "\n#{from_name} ----backup---> #{backup_id}" - - backup = poll_transfer!(backup) - - if backup["error_at"] - message = "An error occurred and your backup did not finish." - message += "\nPlease run `heroku logs --ps pgbackups` for details." - message += "\nThe database is not yet online. Please try again." if backup['log'] =~ /Name or service not known/ - message += "\nThe database credentials are incorrect." if backup['log'] =~ /psql: FATAL:/ - error(message) - end + error("'heroku pgbackups capture' has been removed. +Please see 'heroku pg:backups capture' instead. The '-e/--expire' flag is no longer supported. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end # pgbackups:restore [ [BACKUP_ID|BACKUP_URL]] @@ -106,67 +48,9 @@ def capture # if DATABASE is specified, but no BACKUP_ID, defaults to latest backup # def restore - if 0 == args.size - attachment = resolve(nil, "DATABASE_URL") - to_name = attachment.display_name - to_url = attachment.url - backup_id = :latest - elsif 1 == args.size - attachment = resolve(shift_argument) - to_name = attachment.display_name - to_url = attachment.url - backup_id = :latest - else - attachment = resolve(shift_argument) - to_name = attachment.display_name - to_url = attachment.url - backup_id = shift_argument - end - - if :latest == backup_id - backup = pgbackup_client.get_latest_backup - no_backups_error! if {} == backup - to_uri = URI.parse backup["to_url"] - backup_id = File.basename(to_uri.path, '.*') - backup_id = "#{backup_id} (most recent)" - from_url = backup["to_url"] - from_name = "BACKUP" - elsif backup_id =~ /^http(s?):\/\// - from_url = backup_id - from_name = "EXTERNAL_BACKUP" - from_uri = URI.parse backup_id - backup_id = from_uri.path.empty? ? from_uri : File.basename(from_uri.path) - else - backup = pgbackup_client.get_backup(backup_id) - abort("Backup #{backup_id} already destroyed.") if backup["destroyed_at"] - - from_url = backup["to_url"] - from_name = "BACKUP" - end - - message = "#{to_name} <---restore--- " - padding = " " * message.length - display "\n#{message}#{backup_id}" - if backup - display padding + "#{backup['from_name']}" - display padding + "#{backup['created_at']}" - display padding + "#{backup['size']}" - end - - if confirm_command - restore = transfer!(from_url, from_name, to_url, to_name) - restore = poll_transfer!(restore) - - if restore["error_at"] - message = "An error occurred and your restore did not finish." - if restore['log'] =~ /Invalid dump format: .*: XML document text/ - message += "\nThe backup url is invalid. Use `pgbackups:url` to generate a new temporary URL." - else - message += "\nPlease run `heroku logs --ps pgbackups` for details." - end - error(message) - end - end + error("'heroku pgbackups restore' has been removed. +Please see 'heroku pg:backups restore' instead. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end # pgbackups:destroy BACKUP_ID @@ -174,17 +58,9 @@ def restore # destroys a backup # def destroy - unless name = shift_argument - error("Usage: heroku pgbackups:destroy BACKUP_ID\nMust specify BACKUP_ID to destroy.") - end - backup = pgbackup_client.get_backup(name) - if backup["destroyed_at"] - error("Backup #{name} already destroyed.") - end - - action("Destroying #{name}") do - pgbackup_client.delete_backup(name) - end + error("'heroku pgbackups destroy' has been removed. +Please see 'heroku pg:backups delete' instead. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end # pgbackups:transfer [SOURCE DATABASE] DESTINATION DATABASE @@ -202,196 +78,9 @@ def destroy #$ heroku pgbackups:transfer DATABASE postgres://user:password@host/dbname --app example # def transfer - db1 = shift_argument - db2 = shift_argument - - if db1.nil? - error("pgbackups:transfer requires at least one argument") - end - - if db2.nil? - db2 = db1 - db1 = "DATABASE_URL" - end - - from = resolve_transfer(db1) - to = resolve_transfer(db2) - - validate_arguments! - - opts = {} - verify_app = to.app || app - if confirm_command(verify_app, "WARNING: Destructive Action\nTransfering data from #{from.name} to #{to.name}") - backup = transfer!(from.url, from.name, to.url, to.name, opts) - backup = poll_transfer!(backup) - - if backup["error_at"] - message = "An error occurred and your backup did not finish." - message += "\nThe database is not yet online. Please try again." if backup['log'] =~ /Name or service not known/ - message += "\nThe database credentials are incorrect." if backup['log'] =~ /psql: FATAL:/ - error(message) - end - end - end - protected - - def transfer_status(t) - if t['finished_at'] - "Finished @ #{t["finished_at"]}" - elsif t['started_at'] - step = t['progress'] && t['progress'].split[0] - step.nil? ? 'Unknown' : step_map[step] - else - "Unknown" - end - end - - def config_vars - @config_vars ||= api.get_config_vars(app).body - end - - def pgbackup_client - pgbackups_url = config_vars["PGBACKUPS_URL"] - error("Please add the pgbackups addon first via:\nheroku addons:add pgbackups") unless pgbackups_url - @pgbackup_client ||= Heroku::Client::Pgbackups.new(pgbackups_url) - end - - def backup_name(to_url) - # translate s3://bucket/email/foo/bar.dump => foo/bar - parts = to_url.split('/') - parts.slice(4..-1).join('/').gsub(/\.dump$/, '') - end - - def transfer!(from_url, from_name, to_url, to_name, opts={}) - pgbackup_client.create_transfer(from_url, from_name, to_url, to_name, opts) - end - - def poll_error(app) - error <<-EOM -Failed to query the PGBackups status API. Your backup may still be running. -Verify the status of your backup with `heroku pgbackups -a #{app}` -You can also watch progress with `heroku logs --tail --ps pgbackups -a #{app}` - EOM - end - - def poll_transfer!(transfer) - display "\n" - - if transfer["errors"] - transfer["errors"].values.flatten.each { |e| - output_with_bang "#{e}" - } - abort - end - - transfer_id = transfer["id"] - - while true - unless transfer.nil? - update_display(transfer) - break if transfer["finished_at"] - end - - sleep_time = 1 - begin - sleep(sleep_time) - transfer = pgbackup_client.get_transfer(transfer_id) - rescue - if sleep_time > 300 - poll_error(app) - else - sleep_time *= 2 - retry - end - end - end - - display "\n" - - return transfer - end - - def step_map - @step_map ||= { - "dump" => "Capturing", - "upload" => "Storing", - "download" => "Retrieving", - "restore" => "Restoring", - "gunzip" => "Uncompressing", - "load" => "Restoring", - } - end - - def update_display(transfer) - @ticks ||= 0 - @last_updated_at ||= 0 - @last_logs ||= [] - @last_progress ||= ["", 0] - - @ticks += 1 - - if !transfer["log"] - @last_progress = ['pending', nil] - redisplay "Pending... #{spinner(@ticks)}" - else - logs = transfer["log"].split("\n") - new_logs = logs - @last_logs - @last_logs = logs - - new_logs.each do |line| - matches = line.scan /^([a-z_]+)_progress:\s+([^ ]+)/ - next if matches.empty? - - step, amount = matches[0] - - if ['done', 'error'].include? amount - # step is done, explicitly print result and newline - redisplay "#{@last_progress[0].capitalize}... #{amount}\n" - end - - # store progress, last one in the logs will get displayed - step = step_map[step] || step - @last_progress = [step, amount] - end - - step, amount = @last_progress - unless ['done', 'error'].include? amount - redisplay "#{step.capitalize}... #{amount} #{spinner(@ticks)}" - end - end - end - - private - - TransferEndpoint = Struct.new(:url, :name, :app) - - # - # resolve the given database identifier - def resolve_transfer(db) - if /^postgres:/ =~ db - uri = URI.parse(db) - TransferEndpoint.new(uri, "Database on #{uri.host}:#{uri.port || 5432}#{uri.path}") - else - attachment = resolve(db) - TransferEndpoint.new(attachment.url, db.upcase, attachment.app) - end - end - - def resolve(identifer, default=nil) - Resolver.new(app, api).resolve(identifer, default) - end - - def no_backups_error! - error("No backups. Capture one with `heroku pgbackups:capture`.") - end - - # lists all types of backups ('to_name' attribute) - # - # Useful when one doesn't care if a backup is of a particular - # kind, but wants to know what backups of any kind exist. - # - def backup_types - %w[BACKUP DAILY_SCHEDULED_BACKUP HOURLY_SCHEDULED_BACKUP AUTO_SCHEDULED_BACKUP] + error("'heroku pgbackups:transfer' has been removed. +Please see 'heroku pg:copy' instead. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end end end diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 086f68113..720a8b3c4 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -1,4 +1,6 @@ require "heroku/command/base" +require "heroku/jsplugin" +require "csv" module Heroku::Command @@ -13,12 +15,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]} #{p[:extra]}" } + plugins.concat(::Heroku::Plugin.list) if plugins.length > 0 styled_header("Installed Plugins") @@ -34,22 +37,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 +58,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 + else + Heroku::JSPlugin.uninstall(plugin.name) end end @@ -78,12 +80,13 @@ 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 + Heroku::JSPlugin.update plugins = if plugin = shift_argument [plugin] else @@ -106,5 +109,101 @@ 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 + + # HIDDEN: plugins:commands + # + # Prints a table of commands and location + # + # -c, --csv # Show with csv formatting + def commands + validate_arguments! + + ruby_cmd = Heroku::Command.commands.inject({}) {|h, (cmd, command)| h[cmd] = command_to_hash('ruby', cmd, command) ; h} + commands = Heroku::JSPlugin.commands_info['commands'] + node_cmd = command_list_to_hash(commands.select {|command| command['plugin'] != ''}, 'node') + go_cmd = command_list_to_hash(commands.select {|command| command['plugin'] == ''}, 'go') + + all_cmd = {} + all_cmd.merge!(ruby_cmd) + all_cmd.merge!(node_cmd) + all_cmd.merge!(go_cmd) + + sorted_cmd = all_cmd.sort { |a,b| a[0] <=> b[0] }.map{|cmd| cmd[1]} + + attrs = [:command, :type, :plugin] + header = attrs.map{|attr| attr.to_s.capitalize} + + count_attrs = [:type, :count] + count_header = count_attrs.map{|attr| attr.to_s.capitalize} + + counts = all_cmd.inject(Hash.new(0)) {|h, (_, cmd)| h[cmd[:type]] += 1; h} + type_and_percentage = counts.keys.sort.map{|type| {:type => type, :count => counts[type]}} + + if options[:csv] + csv_str = CSV.generate do |csv| + csv << header + sorted_cmd.each {|cmd| csv << attrs.map{|attr| cmd[attr]}} + + csv << [] + csv << count_header + type_and_percentage.each {|type| csv << count_attrs.map{|attr| type[attr]}} + end + display(csv_str) + else + display_table(sorted_cmd, attrs, header) + display("") + display_table(type_and_percentage, count_attrs, count_header) + end + end + + private + + def command_to_hash(type, cmd, command) + command_hash = {:type => type, :command => cmd} + command_hash[:plugin] = command['plugin'] if command['plugin'] && command['plugin'] != '' + command_hash + end + + def command_list_to_hash(commands, type) + commands.inject({}) do |h, command| + cmd = command['command'] ? "#{command['topic']}:#{command['command']}" : command['topic'] + h[cmd] = command_to_hash(type, cmd, command) + if command['default'] + cmd = command['topic'] + h[cmd] = command_to_hash(type, cmd, command) + end + h + end + end + + 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..77df4be61 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -3,10 +3,7 @@ # manage dynos (dynos, workers) # class Heroku::Command::Ps < Heroku::Command::Base - PRICES = { - "P" => 0.8, - "PX" => 0.8, - } + COSTS = {"Free"=>0, "Hobby"=>7, "Standard-1X"=>25, "Standard-2X"=>50, "Performance-M"=>250, "Performance"=>500, "Performance-L"=>500, "1X"=>36, "2X"=>72, "PX"=>576} # ps:dynos [QTY] # @@ -97,7 +94,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 +128,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| @@ -179,50 +201,42 @@ def restart alias_command "restart", "ps:restart" - # ps:scale DYNO1=AMOUNT1 [DYNO2=AMOUNT2 ...] + # ps:scale [DYNO1=AMOUNT1 [DYNO2=AMOUNT2 ...]] # # scale dynos by the given amount # # appending a size (eg. web=2:2X) allows simultaneous scaling and resizing # + # omitting any arguments will display the app's current dyno formation, in a + # format suitable for passing back into ps:scale + # #Examples: # # $ heroku ps:scale web=3:2X worker+1 # Scaling dynos... done, now running web at 3:2X, worker at 1:1X. # + # $ heroku ps:scale + # web=3:2X worker=1:1X + # def scale - change_map = {} + requires_preauth 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] - { "process" => formation, "quantity" => quantity, "size" => size} + quantity = quantity[1..-1].to_i if quantity[0] == "=" + { "type" => formation, "quantity" => quantity, "size" => size} end end.compact if changes.empty? - error("Usage: heroku ps:scale DYNO1=AMOUNT1[:SIZE] [DYNO2=AMOUNT2 ...]\nMust specify DYNO and AMOUNT to scale.") - end - - action("Scaling dynos") do - # The V3 API supports atomic scale+resize, so we make a raw request here - # since the heroku-api gem still only supports V2. - resp = api.request( - :expects => 200, - :method => :patch, - :path => "/apps/#{app}/formation", - :body => json_encode("updates" => changes), - :headers => { - "Accept" => "application/vnd.heroku+json; version=3", - "Content-Type" => "application/json" - } - ) - new_scales = resp.body. - select {|p| change_map[p['type']] }. - map {|p| "#{p["type"]} at #{p["quantity"]}:#{p["size"]}" } - status("now running " + new_scales.join(", ") + ".") + display_dyno_formation(get_formation) + else + action("Scaling dynos") do + new_scales = scale_dynos(get_formation, changes) + .map {|p| "#{p["type"]} at #{p["quantity"]}:#{p["size"]}" } + status("now running " + new_scales.join(", ") + ".") + end end end @@ -262,60 +276,121 @@ 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 free|hobby|standard-1x|standard-2x|performance # - def resize + # called with 1..n DYNO=TYPE arguments sets the type per dyno + # + def type + requires_preauth app - change_map = {} + formation = get_formation + changes = if args.any?{|arg| arg =~ /=/} + args.map do |arg| + if arg =~ /^([a-zA-Z0-9_]+)=([\w-]+)$/ + type, new_size = $1, $2 + current_p = formation.find{|f| f["type"] == type} + if current_p.nil? + error("Type '#{type}' not found in process formation.") + end + p = current_p.clone + p["size"] = new_size + p + end + end.compact + elsif args.any? + size = shift_argument.downcase + validate_arguments! + formation.map{|p| p["size"] = size; p} + end + scale_dynos(formation, changes) if changes + display_dyno_type_and_costs(get_formation) + end - changes = args.map do |arg| - if arg =~ /^([a-zA-Z0-9_]+)=(\w+)$/ - change_map[$1] = $2 - { "process" => $1, "size" => $2 } - end - end.compact + alias_method :resize, :type + alias_command "resize", "ps:type" - if changes.empty? - message = [ - "Usage: heroku ps:resize DYNO1=1X|2X|PX [DYNO2=1X|2X|PX ...]", - "Must specify DYNO and SIZE to resize." - ] - error(message.join("\n")) + private + + 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 error_no_process_types + error "No process types on #{app}.\nUpload a Procfile to add process types.\nhttps://devcenter.heroku.com/articles/procfile" + end + + def display_dyno_type_and_costs(formation) + annotated = formation.sort_by{|d| d['type']}.map do |dyno| + cost = COSTS[dyno["size"]] + { + 'dyno' => dyno['type'], + 'type' => dyno['size'].rjust(4), + 'qty' => dyno['quantity'].to_s.rjust(3), + 'cost/mo' => cost ? (cost * dyno["quantity"]).to_s.rjust(7) : '' + } end - resp = nil - action("Resizing and restarting the specified dynos") do - resp = api.request( - :expects => 200, - :method => :patch, - :path => "/apps/#{app}/formation", - :body => json_encode("updates" => changes), - :headers => { - "Accept" => "application/vnd.heroku+json; version=3", - "Content-Type" => "application/json" - } - ) + if annotated.empty? + error_no_process_types + else + display_table(annotated, annotated.first.keys, annotated.first.keys) end + end - 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)" + def display_dyno_formation(formation) + dynos = formation.map{|d| "#{d['type']}=#{d['quantity']}:#{d['size']}"}.sort + + if dynos.empty? + error_no_process_types + else + display dynos.join(" ") end end - alias_command "resize", "ps:resize" + def get_formation + api.request( + :expects => 200, + :method => :get, + :path => "/apps/#{app}/formation", + :headers => { + "Accept" => "application/vnd.heroku+json; version=3", + "Content-Type" => "application/json" + } + ).body + end + + def scale_dynos(formation, changes) + # The V3 API supports atomic scale+resize, so we make a raw request here + # since the heroku-api gem still only supports V2. + resp = api.request( + :expects => 200, + :method => :patch, + :path => "/apps/#{app}/formation", + :body => json_encode("updates" => changes), + :headers => { + "Accept" => "application/vnd.heroku+json; version=3", + "Content-Type" => "application/json" + } + ) + resp.body.select {|p| changes.any?{|c| c["type"] == p["type"]} } + end +end + +%w[type restart scale stop].each do |cmd| + Heroku::Command::Base.alias_command "dyno:#{cmd}", "ps:#{cmd}" end diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index ae4b2f43a..83d939420 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -1,4 +1,3 @@ -require "readline" require "heroku/command/base" require "heroku/helpers/log_displayer" @@ -6,24 +5,6 @@ # class Heroku::Command::Run < Heroku::Command::Base - # run COMMAND - # - # run an attached dyno - # - # -s, --size SIZE # specify dyno size - # - #Example: - # - # $ heroku run bash - # Running `bash` attached to terminal... up, run.1 - # ~ $ - # - def index - command = args.join(" ") - error("Usage: heroku run COMMAND") if command.empty? - run_attached(command) - end - # run:detached COMMAND # # run a detached dyno, where output is sent to your logs @@ -56,7 +37,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 +62,7 @@ def rake alias_command "rake", "run:rake" - # run:console [COMMAND] + # HIDDEN: run:console [COMMAND] # # open a remote console session # @@ -131,59 +112,15 @@ 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) end end - def console_history_dir - FileUtils.mkdir_p(path = "#{home_directory}/.heroku/console_history") - path - end - - def console_session(app) - heroku.console(app) do |console| - console_history_read(app) - - display "Ruby console for #{app}.#{heroku.host}" - while cmd = Readline.readline('>> ') - unless cmd.nil? || cmd.strip.empty? - console_history_add(app, cmd) - break if cmd.downcase.strip == 'exit' - display console.run(cmd) - end - end - end - end - - def console_history_file(app) - "#{console_history_dir}/#{app}" - end - - def console_history_read(app) - history = File.read(console_history_file(app)).split("\n") - if history.size > 50 - history = history[(history.size - 51),(history.size - 1)] - File.open(console_history_file(app), "w") { |f| f.puts history.join("\n") } - end - history.each { |cmd| Readline::HISTORY.push(cmd) } - rescue Errno::ENOENT - rescue Exception => 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 - end - end - - 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 deleted file mode 100644 index 0b8f93538..000000000 --- a/lib/heroku/command/status.rb +++ /dev/null @@ -1,51 +0,0 @@ -require "heroku/command/base" - -# check status of heroku platform -# -class Heroku::Command::Status < Heroku::Command::Base - - # status - # - # display current status of heroku platform - # - #Example: - # - # $ heroku status - # === Heroku Status - # Development: No known issues at this time. - # 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 - end - -end diff --git a/lib/heroku/command/two_factor.rb b/lib/heroku/command/two_factor.rb deleted file mode 100644 index c2c1a6efd..000000000 --- a/lib/heroku/command/two_factor.rb +++ /dev/null @@ -1,73 +0,0 @@ -require "heroku/command/base" - -module Heroku::Command - class TwoFactor < BaseWithApp - # 2fa - # - # Display whether two-factor is enabled or not - # - def index - account = api.request( - :expects => 200, - :headers => { "Accept" => "application/vnd.heroku+json; version=3" }, - :method => :get, - :path => "/account").body - - if account["two_factor_authentication"] - display "Two-factor auth is enabled." - else - display "Two-factor is not enabled." - end - end - - alias_command "2fa", "twofactor" - - # 2fa:disable - # - # Disable 2fa on your account - # - def disable - print "Password (typing will be hidden): " - password = Heroku::Auth.ask_for_password - - update = MultiJson.encode( - :two_factor_authentication => false, - :password => password) - - api.request( - :expects => 200, - :headers => { "Accept" => "application/vnd.heroku+json; version=3" }, - :method => :patch, - :path => "/account", - :body => update) - display "Disabled two-factor authentication." - rescue Heroku::API::Errors::RequestFailed => e - error Heroku::Command.extract_error(e.response.body) - end - - alias_command "2fa:disable", "twofactor:disable" - - - # 2fa:generate-recovery-codes - # - # Generates (and replaces) recovery codes - # - def generate_recovery_codes - code = Heroku::Auth.ask_for_second_factor - - recovery_codes = api.request( - :expects => 200, - :method => :post, - :path => "/account/two-factor/recovery-codes", - :headers => { "Heroku-Two-Factor-Code" => code } - ).body - - display "Recovery codes:" - recovery_codes.each { |c| display c } - rescue RestClient::Unauthorized => e - error Heroku::Command.extract_error(e.http_body) - end - - alias_command "2fa:generate-recovery-codes", "twofactor:generate_recovery_codes" - end -end diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index 67b977a15..eaf7e74b6 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -12,11 +12,12 @@ 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") + Heroku::JSPlugin.update + update_from_url(false) end # update:beta @@ -24,24 +25,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, true) end - end diff --git a/lib/heroku/command/version.rb b/lib/heroku/command/version.rb index e87e320c8..12fdd9653 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) + 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..33e4cb0ec 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -1,4 +1,6 @@ -require "vendor/heroku/okjson" +# encoding: utf-8 + +require 'heroku/helpers/env' module Heroku module Helpers @@ -6,7 +8,32 @@ module Helpers extend self def home_directory - running_on_windows? ? ENV['USERPROFILE'].gsub("\\","/") : ENV['HOME'] + if running_on_windows? + # This used to be File.expand_path("~"), which should have worked but there was a bug + # when a user has a cryllic character in their username. Their username gets mangled + # by a C code operation that does not respect multibyte characters + # + # see: https://github.com/ruby/ruby/blob/v2_2_3/win32/file.c#L47 + home = Heroku::Helpers::Env["HOME"] + homedrive = Heroku::Helpers::Env["HOMEDRIVE"] + homepath = Heroku::Helpers::Env["HOMEPATH"] + userprofile = Heroku::Helpers::Env["USERPROFILE"] + + home_dir = if home + home + elsif homedrive && homepath + homedrive + homepath + elsif userprofile + userprofile + else + # The expanding `~' error here does not make much sense + # just made it match File.expand_path when no env set + raise ArgumentError.new("couldn't find HOME environment -- expanding `~'") + end + home_dir.gsub(/\\/, '/') + else + Dir.home + end end def running_on_windows? @@ -34,6 +61,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 +161,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 +195,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 +234,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 @@ -221,8 +275,7 @@ def fail(message) ## DISPLAY HELPERS def action(message, options={}) - message = "#{message} in organization #{org}" if options[:org] - display("#{message}... ", false) + display("#{in_message(message, options)}... ", false) Heroku::Helpers.error_with_failure = true ret = yield Heroku::Helpers.error_with_failure = false @@ -235,6 +288,12 @@ def action(message, options={}) ret end + def in_message(message, options={}) + message = "#{message} in space #{options[:space]}" if options[:space] + message = "#{message} in organization #{org}" if options[:org] + message + end + def status(message) @status = message end @@ -249,12 +308,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 +423,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 +460,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 +472,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 +592,17 @@ 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 + + # cheap deep clone + def deep_clone(obj) + json_decode(json_encode(obj)) + 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..e5366e83a --- /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!(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..e6666e2ff --- /dev/null +++ b/lib/heroku/helpers/addons/resolve.rb @@ -0,0 +1,33 @@ +require "heroku/helpers/addons/api" + +module Heroku::Helpers + module Addons + module Resolve + include Heroku::Helpers::Addons::API + + def resolve_addon!(identifier, app=maybe_app) + if identifier !~ /::/ && app + get_addon(identifier, app: app) + end || get_addon!(identifier) + end + + def resolve_attachment!(identifier, app=maybe_app) + if identifier !~ /::/ && app + get_attachment(identifier, app: app) + end || get_attachment!(identifier) + end + + private + + def maybe_app + app + rescue Heroku::Command::CommandFailed + nil + end + end + + class Resolver < Struct.new(:api) + include Resolve + end + end +end diff --git a/lib/heroku/helpers/env.rb b/lib/heroku/helpers/env.rb new file mode 100644 index 000000000..3596506b2 --- /dev/null +++ b/lib/heroku/helpers/env.rb @@ -0,0 +1,15 @@ +module Heroku + module Helpers + class Env + def self.[](key) + val = ENV[key] + + if val && Heroku::Helpers.running_on_windows? && val.encoding == Encoding::ASCII_8BIT + val = val.dup.force_encoding('utf-8') + end + + val + end + end + end +end diff --git a/lib/heroku/helpers/heroku_postgresql.rb b/lib/heroku/helpers/heroku_postgresql.rb index bd1f21866..9a5d2ebc2 100644 --- a/lib/heroku/helpers/heroku_postgresql.rb +++ b/lib/heroku/helpers/heroku_postgresql.rb @@ -1,4 +1,5 @@ require "heroku/helpers" +require "heroku/helpers/addons/resolve" module Heroku::Helpers::HerokuPostgresql @@ -6,7 +7,10 @@ module Heroku::Helpers::HerokuPostgresql extend Heroku::Helpers class Attachment - attr_reader :app, :name, :config_var, :resource_name, :url, :addon, :plan + attr_reader :app, :name, :config_var, :resource_name, + :url, :addon, :plan, :billing_app + attr_reader :bastions, :bastion_key + def initialize(raw) @raw = raw @app = raw['app']['name'] @@ -15,6 +19,15 @@ def initialize(raw) @resource_name = raw['resource']['name'] @url = raw['resource']['value'] @addon, @plan = raw['resource']['type'].split(':') + @billing_app = raw['resource']['billing_app']['name'] + + # Optional Bastion information for tunneling. + if config = raw['config'] + @bastions = if maybe_hosts = config[@name + '_BASTIONS'] + maybe_hosts.split(',') + end + @bastion_key = config[@name + '_BASTION_KEY'] + end end def starter_plan? @@ -32,6 +45,38 @@ def primary_attachment! def primary_attachment? @primary_attachment end + + def uses_bastion? + !!(bastions && bastion_key) + end + + def maybe_tunnel + require "net/ssh/gateway" + + uri = URI.parse(url) + if uses_bastion? + bastion_host = bastions.sample + gateway = Net::SSH::Gateway.new(bastion_host, 'bastion', + paranoid: false, timeout: 15, key_data: [bastion_key]) + begin + local_port = rand(65535 - 49152) + 49152 + gateway.open(uri.host, uri.port, local_port) do |actual_local_port| + uri.host = 'localhost' + uri.port = actual_local_port + yield uri + end + rescue Errno::EADDRINUSE + # Get a new random port if a local binding was not possible. + gateway && gateway.shutdown! + gateway = nil + retry + ensure + gateway && gateway.shutdown! + end + else + yield uri + end + end end def hpg_resolve(identifier, default=nil) @@ -80,6 +125,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 +139,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 +153,7 @@ def hpg_databases } @hpg_databases = Hash[ pairs ] - if find_database_url_real_attachment + if !@hpg_databases.key?('DATABASE_URL') && find_database_url_real_attachment @hpg_databases['DATABASE_URL'] = find_database_url_real_attachment end @@ -146,10 +191,17 @@ def find_database_url_real_attachment end end + def api_resolver + Heroku::Helpers::Addons::Resolver.new(@api) + end + def match_attachments_by_name(name) return [] if name.empty? return [name] if hpg_databases[name] - hpg_databases.keys.grep(%r{#{ name }}i) + att = api_resolver.resolve_attachment!(name, @app_name) + ["#{att['name']}_URL"] + rescue Heroku::API::Errors::NotFound + [] end def hpg_resolve(name, default=nil) @@ -169,7 +221,11 @@ def hpg_resolve(name, default=nil) end if found_attachment.nil? - error("Unknown database#{': ' + name unless name.empty?}. Valid options are: #{hpg_databases.keys.sort.join(", ")}") + if name.empty? + error("No default database configured in DATABASE_URL. Valid alternatives are: #{hpg_databases.keys.sort.join(", ")}") + else + error("Unknown database#{': ' + name unless name.empty?}. Valid options are: #{hpg_databases.keys.sort.join(", ")}") + end end return found_attachment @@ -197,7 +253,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/helpers/pg_diagnose.rb b/lib/heroku/helpers/pg_diagnose.rb index 7fd6822f0..bf6acc128 100644 --- a/lib/heroku/helpers/pg_diagnose.rb +++ b/lib/heroku/helpers/pg_diagnose.rb @@ -42,8 +42,6 @@ def generate_report(db_id) attachment = generate_resolver.resolve(db_id, "DATABASE_URL") validate_arguments! - warn_old_databases(attachment) - metrics = get_metrics(attachment) params = { @@ -60,13 +58,6 @@ def generate_report(db_id) :headers => {"Content-Type" => "application/json"}) end - def warn_old_databases(attachment) - @uri = URI.parse(attachment.url) # for #nine_two? - if !nine_two? - warn "WARNING: pg:diagnose is only fully supported on Postgres version >= 9.2. Some checks will be skipped.\n\n" - end - end - def get_metrics(attachment) unless attachment.starter_plan? hpg_client(attachment).metrics diff --git a/lib/heroku/http_instrumentor.rb b/lib/heroku/http_instrumentor.rb new file mode 100644 index 000000000..9aad26036 --- /dev/null +++ b/lib/heroku/http_instrumentor.rb @@ -0,0 +1,53 @@ +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]) + $stderr.puts headers if headers? + $stderr.puts "--> #{params[:body]}" if params[:body] + when "excon.response" + $stderr.puts "<-- #{params[:status]} #{params[:reason_phrase]}" + $stderr.puts "<-- request-id: #{headers['Request-id']}" if headers['Request-Id'] + $stderr.puts headers if headers? + 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 + + def headers? + !!ENV['HEROKU_DEBUG_HEADERS'] + end + end +end diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb new file mode 100644 index 000000000..106e153a9 --- /dev/null +++ b/lib/heroku/jsplugin.rb @@ -0,0 +1,240 @@ +require 'rbconfig' +require 'heroku/helpers/env' + +class Heroku::JSPlugin + extend Heroku::Helpers + + def self.try_takeover(command, args) + if command == 'help' && args.length > 0 + return + elsif args.include?('--help') || args.include?('-h') + return + end + command = find_command(command) + return if !command || command[:hidden] + run(ARGV[0], nil, ARGV[1..-1]) + end + + def self.load! + 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 |command| + Heroku::Command.register_command(command) + if command[:default] + Heroku::Command.register_command( + :command => command[:namespace], + :namespace => command[:namespace], + :klass => command[:klass], + :method => :run, + :banner => command[:banner], + :summary => command[:summary], + :help => command[:help], + :hidden => command[:hidden], + ) + end + end + end + + def self.plugins + @plugins ||= `"#{bin}" plugins`.lines.map do |line| + name, version, extra = line.split + { :name => name, :version => version, :extra => extra } + end + end + + def self.is_plugin_installed?(name) + plugins.any? { |p| p[:name] == name } + end + + def self.topics + commands_info['topics'] + end + + def self.commands + @commands ||= begin + this = self + commands_info['commands'].map do |command| + help = "\n\n#{command['fullHelp']}" + klass = Class.new do + def initialize(args, opts) + @args = args + @opts = opts + end + end + klass.send(:define_method, :run) do + this.run(command['topic'], command['command'], ARGV[1..-1]) + end + { + :command => command['command'] ? "#{command['topic']}:#{command['command']}" : command['topic'], + :namespace => command['topic'], + :klass => klass, + :method => :run, + :banner => command['usage'], + :summary => " #{command['description']}", + :help => help, + :hidden => command['hidden'], + :default => command['default'], + } + end + end + end + + def self.commands_info + @commands_info ||= begin + info = json_decode(`"#{bin}" commands --json`) + error "error getting commands #{$?}" if $? != 0 + info + end + end + + def self.install(name, opts={}) + system "\"#{bin}\" plugins:install #{name}" if opts[:force] || !self.is_plugin_installed?(name) + error "error installing plugin #{name}" if $? != 0 + 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.app_dir + localappdata = Heroku::Helpers::Env['LOCALAPPDATA'] + xdg_data_home = Heroku::Helpers::Env['XDG_DATA_HOME'] + + if windows? && localappdata + File.join(localappdata, 'heroku') + elsif xdg_data_home + File.join(xdg_data_home, 'heroku') + else + File.join(Heroku::Helpers.home_directory, '.heroku') + end + end + + def self.bin + File.join(app_dir, windows? ? 'heroku-cli.exe' : 'heroku-cli') + end + + def self.setup + check_if_old + return if setup? + require 'excon' + $stderr.print "heroku-cli: Installing Toolbelt v4..." + FileUtils.mkdir_p File.dirname(bin) + copy_ca_cert + opts = excon_opts.merge( + :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress], + :read_timeout => 300, + ) + 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\nFor more information on Toolbelt v4: https://github.com/heroku/heroku-cli" + version + end + + def self.setup? + File.exist? bin + end + + def self.copy_ca_cert + to = File.join(app_dir, "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 + bin = self.bin + + if windows? && [bin, cmd, *args].any? {|arg| ! arg.ascii_only?} + system bin, cmd, *args + exit $?.exitstatus + else + exec bin, cmd, *args + end + end + + def self.spawn(topic, command, args) + cmd = command ? "#{topic}:#{command}" : topic + system 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://cli-assets.heroku.com/master/manifest.json", excon_opts).body) + end + + def self.excon_opts + if 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 + + def self.find_command(s) + commands.find { |c| c[:command] == s } + end + + # check if release is one that isn't able to update on windows + def self.check_if_old + File.delete(bin) if windows? && setup? && version.start_with?("heroku-cli/4.24") + rescue => e + Rollbar.error(e) + rescue + end + + def self.windows? + os == 'windows' + 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..3c039876a 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('http://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,88 @@ def self.wait_for_lock(path, wait_for=5, check_every=0.5) Heroku::Helpers.error "Unable to acquire update lock" end end + FileUtils.mkdir_p File.dirname(path) + FileUtils.touch path + yield + ensure + FileUtils.rm_f path + end + + def self.autoupdate + # if we've updated in the last 4 hours, 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*4 + end + FileUtils.mkdir_p File.dirname(last_autoupdate_path) + FileUtils.touch last_autoupdate_path + return warn_if_out_of_date if disable begin - FileUtils.touch path - ret = yield - ensure - FileUtils.rm_f path + fork { update(false, false) } + rescue NotImplementedError + # cannot fork on windows + update(false, true) end - ret end - def self.autoupdate? - true + def self.warn_if_out_of_date + $stderr.puts "WARNING: Toolbelt v#{latest_version} update available." if needs_minor_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.update(prerelease=false, message=true) + return unless prerelease || needs_update? - latest_version = Excon.get_with_redirect("http://assets.heroku.com/heroku-client/VERSION", :nonblock => false).body.chomp + $stderr.print 'heroku-cli: Updating...' if message + 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 - 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 + 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 - 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 + extract_zip(zip_filename, download_dir) + FileUtils.rm_f zip_filename - error "Update hash signature mismatch" unless hash == official_hash + version = client_version_from_path(download_dir) - 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 + # do not replace beta version if it is old + return if compare_versions(version, latest_local_version) < 0 - FileUtils.rm "#{download_dir}/heroku.zip" + FileUtils.rm_rf updated_client_path + FileUtils.mkdir_p File.dirname(updated_client_path) + FileUtils.cp_r download_dir, updated_client_path - old_version = latest_local_version - new_version = client_version_from_path(download_dir) + $stderr.puts ' done.' if message - 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 + 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 +183,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..7455373be 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.9.4" + VERSION = "3.42.25" 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..99dd40e80 --- /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, 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..62f9fe5ca --- /dev/null +++ b/resources/deb/heroku/control @@ -0,0 +1,8 @@ +Package: heroku +Version: <%= version %> +Section: main +Priority: standard +Architecture: all +Depends: ruby +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/heroku b/resources/exe/heroku new file mode 100755 index 000000000..77eddbf68 --- /dev/null +++ b/resources/exe/heroku @@ -0,0 +1,29 @@ +#!/bin/sh +# find embedded ruby relative to script +bindir=`cd -P "${0%/*}/../ruby-2.1.7/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.pfx.gpg b/resources/exe/heroku-codesign-cert.pfx.gpg new file mode 100644 index 000000000..3b4a67de9 Binary files /dev/null and b/resources/exe/heroku-codesign-cert.pfx.gpg differ diff --git a/resources/exe/heroku.bat b/resources/exe/heroku.bat new file mode 100644 index 000000000..df6d00692 --- /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-2.1.7\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..6686e9eff --- /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 + +; 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/git"; Description: "Git and SSH"; Types: "client custom"; Check: "not IsProgramInstalled('git-2.6.3.exe')" +Name: "toolbelt/git"; Description: "Git and SSH"; Check: "IsProgramInstalled('git-2.6.3.exe')" + +[Files] +Source: "heroku\*.*"; DestDir: "{app}"; Flags: recursesubdirs; Components: "toolbelt/client" +Source: "installers\rubyinstaller-2.1.7.exe"; DestDir: "{tmp}"; Components: "toolbelt/client" +Source: "installers\git-2.6.3.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-2.1.7.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-2.1.7"""; \ + Flags: shellexec waituntilterminated; StatusMsg: "Installing Ruby"; Components: "toolbelt/client" +Filename: "{tmp}\git-2.6.3.exe"; Parameters: "/silent /nocancel /noicons"; \ + Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git" + +[UninstallDelete] +Type: filesandordirs; Name: "{localappdata}\heroku" +Type: filesandordirs; Name: "{%UserProfile}\.heroku" + +[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 87% rename from dist/resources/pkg/Distribution.erb rename to resources/pkg/Distribution.erb index 795215b31..7c792fdbb 100644 --- a/dist/resources/pkg/Distribution.erb +++ b/resources/pkg/Distribution.erb @@ -1,7 +1,7 @@ - Heroku Client - + Heroku Toolbelt + 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/resources/pkg/certificate.p12.gpg b/resources/pkg/certificate.p12.gpg new file mode 100644 index 000000000..baa78349e Binary files /dev/null and b/resources/pkg/certificate.p12.gpg differ 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/resources/pkg/postinstall b/resources/pkg/postinstall new file mode 100755 index 000000000..62fe6377f --- /dev/null +++ b/resources/pkg/postinstall @@ -0,0 +1,4 @@ +#!/bin/sh + +sudo mkdir -p /usr/local/bin +sudo ln -sf /usr/local/heroku/bin/heroku /usr/local/bin/heroku 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..bf495fe3e 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -10,24 +10,25 @@ 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 + allow(Heroku::Auth).to receive(:home_directory).and_return(Heroku::Helpers.home_directory) FileUtils.mkdir_p(@cli.netrc_path.split("/")[0..-2].join("/")) 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 +49,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 +70,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 +106,47 @@ 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' - 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 +154,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 +171,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 deleted file mode 100644 index b6b7cdfb2..000000000 --- a/spec/heroku/client/pgbackups_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -require "spec_helper" -require "heroku/client/pgbackups" -require "heroku/helpers" - -describe Heroku::Client::Pgbackups do - - include Heroku::Helpers - - let(:path) { "http://id:password@pgbackups.heroku.com" } - let(:client) { Heroku::Client::Pgbackups.new path+'/api' } - let(:transfer_path) { path + '/client/transfers' } - - describe "api" do - let(:version) { Heroku::Client.version } - - it 'still has a heroku gem version' do - version.should be - version.split(/\./).first.to_i.should >= 2 - end - - it 'includes the heroku gem version' do - stub_request(:get, transfer_path) - client.get_transfers - a_request(:get, transfer_path).with( - :headers => {'X-Heroku-Gem-Version' => version} - ).should have_been_made.once - end - end - - describe "create transfers" do - it "sends a request to the client" do - stub_request(:post, transfer_path).to_return( - :body => json_encode({"message" => "success"}), - :status => 200 - ) - - client.create_transfer("postgres://from", "postgres://to", "FROMNAME", "TO_NAME") - - a_request(:post, transfer_path).should have_been_made.once - end - end - -end diff --git a/spec/heroku/client/rendezvous_spec.rb b/spec/heroku/client/rendezvous_spec.rb index f450ec98a..dd9f9f896 100644 --- a/spec/heroku/client/rendezvous_spec.rb +++ b/spec/heroku/client/rendezvous_spec.rb @@ -12,51 +12,48 @@ 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 "" - end - it "hash" do - @rendezvous.send(:fixup, { :x => :y }).should eq({ :x => :y }) + expect(@rendezvous.send(:fixup, "")).to eq "" 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..16b8cea01 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,433 +22,676 @@ module Heroku::Command end it "should display no addons when none are configured" do + Excon.stub(method: :get, path: '/apps/example/addons') do + { body: "[]", status: 200 } + end + + Excon.stub(method: :get, path: '/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: '/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: '/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, '/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: '/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: '/apps/example/releases/current') do + { body: MultiJson.dump({ 'name' => 'v99' }), status: 200 } + end + + Excon.stub(method: :post, path: '/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: '/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, :method => :get, - :path => %r{^/apps/example/releases/current} + :path => '/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: '/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: '/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: '/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 - 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) + Excon.stub(method: :post, path: '/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 + + 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, :method => :get, - :path => %r{^/apps/example/releases/current} + :path => '/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. + 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" }) + + Excon.stub(method: :get, path: '/apps/example/addons') do + { body: MultiJson.encode([my_addon]), status: 200 } + end + + Excon.stub(method: :get, path: '/apps/example/addons/my_service') do + { body: MultiJson.encode(my_addon), status: 200 } + end + + Excon.stub(method: :patch, path: '/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 - 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. + it "adds an addon with a price and multiline message" 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" } + ).merge(provision_message: "foo\nbar") + + Excon.stub(method: :get, path: '/apps/example/addons') do + { body: MultiJson.encode([my_addon]), status: 200 } + end + + Excon.stub(method: :get, path: '/apps/example/addons/my_service') do + { body: MultiJson.encode(my_addon), status: 200 } + end + + Excon.stub(method: :patch, path: '/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) +foo +bar 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 => '/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: '/apps/example/addons') do + { body: MultiJson.encode([my_addon]), status: 200 } + end + + Excon.stub(method: :patch, path: '/apps/example/addons/my_service') do + { body: MultiJson.encode(my_addon), status: 200 } + end + + Excon.stub(method: :patch, path: '/apps/example/addons/my_addon') 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 +699,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 +709,49 @@ 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" })) - 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.should == '' - end + Excon.stub(method: :get, path: '/addon-services/thing') do + { status: 404, body:'{}' } + end + Excon.stub(method: :get, path: '/apps/example/addons/thing') do + { status: 404, body:'{}' } + 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 + Excon.stub(method: :get, path: '/addons/thing') do + { + status: 422, + body: MultiJson.encode( + id: "multiple_matches", + message: "Ambiguous identifier; multiple matching add-ons found: my_addon1 (my_service), my_addon2 (my_service_2)." + ) + } + 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 == '' + expect { execute('addons:docs thing') }.to raise_error(Heroku::API::Errors::RequestFailed) 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 +763,84 @@ 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') + Excon.stub(method: :get, path: %r'(/apps/example)?/addon-attachments/redistogo:nano') do + {status: 404} + end + 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$} - }, + it "opens the addon using the attachment URL if a single unique attachment can be determined" do + addon.merge!(addon_service: { name: "redistogo:nano" }) + attachment = build_attachment( + name: "REDISTOGO", + addon: { name: "redistogo-angular", app: { name: "example" }}, + app: { name: "example" }) + + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon!).and_return(stringify(addon)) + allow_any_instance_of(Heroku::Command::Addons).to receive(:get_attachment).and_return(stringify(attachment)) + require("launchy") + expect(Launchy).to receive(:open).with("https://attachment-sso").and_return(Thread.new {}) + stderr, stdout = execute('addons:open redistogo:nano') + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT +Opening redistogo:nano (my_addon) for example... done +STDOUT + end + + it "opens the add-on using the add-on URL if a single unique attachment can not be determined" do + Excon.stub(method: :get, path: '/apps/example/addon-attachments/redistogo:nano') do { - :body => Heroku::OkJson.encode([ - { 'name' => 'deployhooks:email' }, - { 'name' => 'deployhooks:http' } - ]), - :status => 200, + status: 422, + body: MultiJson.encode( + id: "multiple_matches", + message: "Ambiguous identifier; ..." + ) } - ) - stderr, stdout = execute('addons:open deployhooks') - stderr.should == <<-STDERR - ! Ambiguous addon name: deployhooks - ! Perhaps you meant `deployhooks:email` or `deployhooks:http`. -STDERR - stdout.should == '' - Excon.stubs.shift - end + end - it "complains if no such addon exists" do - 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 == '' + addon.merge!(addon_service: { name: "redistogo:nano" }) + + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon!).and_return(stringify(addon)) + require("launchy") + 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') + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT +Opening redistogo:nano (my_addon) for example... done +STDOUT 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. -STDERR - stdout.should == '' + it "complains if no such addon exists" do + Excon.stub(method: :get, path: %r'(/apps/example)?/addons/unknown') do + { status: 404, body:'{"error": "hi"}' } + end + expect { execute('addons:open unknown') }.to raise_error(Heroku::API::Errors::NotFound) 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/deployhooks:http') do + { status: 404, body:'{"error": "hi"}' } + end + expect { execute('addons:open deployhooks:http') }.to raise_error(Heroku::API::Errors::NotFound) end end + end end diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index ac8f5689e..795419a12 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -6,64 +6,9 @@ module Heroku::Command before(:each) do stub_core + stub_get_space_v3 stub_organizations - end - - context("info") do - - before(:each) do - api.post_app("name" => "example", "stack" => "cedar") - end - - after(:each) do - api.delete_app("example") - end - - it "displays impicit app info" do - stderr, stdout = execute("apps:info") - stderr.should == "" - stdout.should == <<-STDOUT -=== example -Git URL: git@heroku.com:example.git -Owner Email: email@example.com -Stack: cedar -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 -=== example -Git URL: git@heroku.com:example.git -Owner Email: email@example.com -Stack: cedar -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) -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 -id=\\d{1,5} -name=example -owner_email=email@example.com -repo_migrate_status=complete -repo_size= -requested_stack= -slug_size= -stack=cedar -web_url=http://example.herokuapp.com/ -workers=0 -STDOUT - end - + ENV.delete('HEROKU_ORGANIZATION') end context("create") do @@ -73,10 +18,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 +31,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 +44,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 +57,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 +68,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 +81,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,16 +98,64 @@ 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 api.delete_app("alternate-remote") end + context "with a space" do + shared_examples "create in a space" do + Excon.stub( + :headers => {'Accept' => 'application/vnd.heroku+json; version=3'}, + :method => :post, + :path => '/organizations/apps') do + { + :status => 201, + :body => { + :name => 'spaceapp', + :space => { + :name => 'example-space' + }, + :stack => 'cedar-14', + :web_url => 'http://spaceapp.herokuapp.com/' + }.to_json, + } + end + + it "creates app in space" do + with_blank_git_repository do + stderr, stdout = execute("apps:create spaceapp --space test-space") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Creating spaceapp in space test-space... done, stack is cedar-14 +http://spaceapp.herokuapp.com/ | https://git.heroku.com/spaceapp.git +Git remote heroku added + STDOUT + end + end + end + + context "with default org" do + before(:each) do + ENV['HEROKU_ORGANIZATION'] = 'test-org' + end + + it_behaves_like "create in a space" + end + + context "without default org" do + before(:each) do + ENV.delete('HEROKU_ORGANIZATION') + end + + it_behaves_like "create in a space" + end + end end context("index") do @@ -178,8 +171,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 +183,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 +197,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"} ]), @@ -223,30 +206,108 @@ module Heroku::Command ) end - it "lists joined apps in an organization" do - stderr, stdout = execute("apps") - stderr.should == "" - stdout.should == <<-STDOUT -=== Apps joined in organization test-org + it "list all in an organization" do + stderr, stdout = execute("apps -o test-org") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== Apps in organization test-org org-app-1 +org-app-2 STDOUT end + end + end + end - it "list all apps in an organization with the --all flag" do - stderr, stdout = execute("apps --all") - stderr.should == "" - stdout.should == <<-STDOUT -=== Apps joined in organization test-org -org-app-1 + context("index with space") do + shared_examples "index with space" do + context("and the space has no apps") do + before(:each) do + Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }) do + { + :body => MultiJson.dump([]), + :status => 200 + } + end + end -=== Apps available to join in organization test-org -org-app-2 + it "displays a message when the space has no apps" do + stderr, stdout = execute("apps --space test-space") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +There are no apps in space test-space. +STDOUT + end + end + + context("and the space has apps") do + before(:each) do + Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }) do + { + :body => MultiJson.dump([ + { :name => 'space-app-1', :space => {:id => 'test-space-id', :name => 'test-space'}, :joined => true }, + { :name => 'space-app-2', :space => {:id => 'test-space-id', :name => 'test-space'}, :joined => false }, + { :name => 'non-space-app-2', :space => nil, :joined => true } + ]), + :status => 200 + } + end + end + + it "lists only apps in spaces by name" do + stderr, stdout = execute("apps --space test-space") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== Apps in space test-space +space-app-1 +space-app-2 STDOUT end end end + + context "with default org" do + before(:each) do + ENV['HEROKU_ORGANIZATION'] = 'test-org' + end + + it_behaves_like "index with space" + end + + context "without default org" do + before(:each) do + ENV.delete('HEROKU_ORGANIZATION') + end + + it_behaves_like "index with space" + end + end + + context("index with space and org") do + before(:each) do + Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }) do + { + :body => MultiJson.dump([]), + :status => 200 + } + end + end + + it "displays error to not specify both" do + stderr, stdout = execute("apps --space test-space --org test-org") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDERR + ! Specify option for space or org, but not both. +STDERR + end + + it "does not display error if org specified via env" do + ENV['HEROKU_ORGANIZATION'] = 'test-org' + stderr, stdout = execute("apps --space test-space") + expect(stderr).to eq("") + end end context("rename") do @@ -264,10 +325,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 +338,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 +355,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 +369,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 +399,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 +413,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 +428,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,35 +440,55 @@ 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 + + def stub_get_space_v3 + Excon.stub( + :headers => {'Accept' => 'application/vnd.heroku+json; version=3'}, + :method => :get, + :path => '/spaces/test-space') do + { + :body => { + :created_at => '2015-08-12T19:37:02Z', + :id => '6989c417-304f-4394-b958-f42bc6e1fa4e', + :name => 'test1', + :organization => { + :name => 'test-org' + }, + :state => 'allocated', + :updated_at => '2015-08-12T19:48:07Z' + }.to_json, + } + end + 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..2fb532358 --- /dev/null +++ b/spec/heroku/command/buildpacks_spec.rb @@ -0,0 +1,631 @@ +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 buildpack URNs" do + before(:each) do + Excon.stubs.shift + stub_get("urn:buildpack:heroku/nodejs", "urn:buildpack:heroku/ruby") + end + + it "displays the short-hand name" do + stderr, stdout = execute("buildpacks") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Buildpack URLs +1. heroku/nodejs +2. heroku/ruby + STDOUT + end + end + + context "with offical buildpack URLs" do + before(:each) do + Excon.stubs.shift + stub_get("https://codon-buildpacks.s3.amazonaws.com/buildpacks/heroku/nodejs.tgz", "https://codon-buildpacks.s3.amazonaws.com/buildpacks/heroku/ruby.tgz") + end + + it "displays the short-hand name" do + stderr, stdout = execute("buildpacks") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Buildpack URLs +1. heroku/nodejs +2. heroku/ruby + STDOUT + end + 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 3 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 + + it "adds buildpack URL to the very 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 buildpack URNs" do + before(:each) do + Excon.stubs.shift + stub_get("urn:buildpack:heroku/nodejs") + stub_put("heroku/nodejs", "heroku/ruby") + end + + it "displays the short-hand name" do + stderr, stdout = execute("buildpacks:add heroku/ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use: + 1. heroku/nodejs + 2. heroku/ruby +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + end + + 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..4414f964e 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,21 +61,40 @@ 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 + + it "shows a single config for get not found" do + api.put_config_vars("example", { 'LONG' => 'A' * 60 }) + stderr, stdout = execute("config:get BLAH") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT + +STDOUT + end + + it "shows a single config for get --shell when missing" do + api.put_config_vars("example", { 'LONG' => 'A' * 60 }) + stderr, stdout = execute("config:get --shell BLAH") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT + STDOUT end @@ -78,8 +102,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 +112,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 +121,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 +134,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 +155,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..9ec32b2ac 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 @@ -17,42 +17,112 @@ module Heroku::Command stub_core end + def stub_get_domains_v3(*custom_hostnames) + Excon.stub( + :headers => { "Accept" => "application/vnd.heroku+json; version=3" }, + :method => :get, + :path => '/apps/example/domains') do + { + :body => ( + [ + { + 'kind' => 'heroku', + 'hostname' => 'example.herokuapp.com', + 'cname' => nil + } + ] + custom_hostnames.map { |hostname| + { 'kind' => 'custom', + 'hostname' => hostname, + 'cname' => 'example-2121.herokussl.com' + } + } + ).to_json, + } + end + end + + def stub_post_domains_v3(custom_hostname) + Excon.stub( + :headers => { + "Accept" => "application/vnd.heroku+json; version=3", + "Content-Type" => "application/json" + }, + :method => :post, + :path => '/apps/example/domains') do + { + :status => 201, + :body => { + 'kind' => 'custom', + 'hostname' => custom_hostname, + 'cname' => 'example-2121.herokussl.com' + }.to_json, + } + end + end + context("index") do it "lists message with no domains" do + Excon.stub(:path => '/apps/example/domains') {{ :body => [].to_json }} + stderr, stdout = execute("domains") - stderr.should == "" - stdout.should == <<-STDOUT -example has no domain names. + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Heroku Domain + ! Not found + +=== example Custom Domains +example has no custom domains. +Use `heroku domains:add DOMAIN` to add one. +STDOUT + end + + it "lists message with development domain but no custom domains" do + stub_get_domains_v3() + stderr, stdout = execute("domains") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Heroku Domain +example.herokuapp.com + +=== example Custom Domains +example has no custom domains. +Use `heroku domains:add DOMAIN` to add one. STDOUT end - it "lists domains when some exist" do - api.post_domain("example", "example.com") + it "lists development and custom domains when some exist" do + stub_get_domains_v3('example1.com', 'example2.com') stderr, stdout = execute("domains") - stderr.should == "" - stdout.should == <<-STDOUT -=== example Domain Names -example.com + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Heroku Domain +example.herokuapp.com +=== example Custom Domains +Domain Name DNS Target +------------ -------------------------- +example1.com example-2121.herokussl.com +example2.com example-2121.herokussl.com STDOUT - api.delete_domain("example", "example.com") end end it "adds domain names" do + stub_post_domains_v3('example.com') 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 + ! Configure your app's DNS provider to point to the DNS Target example-2121.herokussl.com + ! For help, see https://devcenter.heroku.com/articles/custom-domains STDOUT - api.delete_domain("example", "example.com") end 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 +131,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 +148,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..d6f3632d7 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 + id_rsa_pub = File.read("#{Heroku::Auth.home_directory}/.ssh/id_rsa.pub") + api.delete_key(id_rsa_pub.split(' ')[2]) 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 + 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..32c0d1a21 --- /dev/null +++ b/spec/heroku/command/pg_backups_spec.rb @@ -0,0 +1,664 @@ +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', + 'billing_app' => { 'name' => 'example' } }}), + 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', + 'billing_app' => { 'name' => 'example' } }}), + 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', + 'billing_app' => { 'name' => 'example' } }}) + ] + 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', + 'billing_app' => { 'name' => 'aux-example' } }}) + ] + 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 + } + + Excon.stub(method: :get, path: %r|^(/apps/example)?/addon-attachments/(example::)?|) do |req| + vars = %w[DATABASE_URL HEROKU_POSTGRESQL_GREEN_URL HEROKU_POSTGRESQL_IVORY_URL HEROKU_POSTGRESQL_RED_URL] + identifier = req[:path].scan(%r|[^/]+$|)[0] + matches = vars.grep(Regexp.new(identifier, "i")) + + case matches.size + when 1 + {status: 200, body: MultiJson.encode({ + name: matches[0].gsub(/_URL$/,''), + app: {name: 'example'} + })} + when 0 + {status: 404, body: '{}'} + else + {status: 422, body: '{}' } + end + end + 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 + Excon.stub(method: :get, path: %r|^(/apps/aux-example)?/addon-attachments/(aux-example::)?teal|) do + {status: 200, body: MultiJson.encode({name: 'HEROKU_POSTGRESQL_TEAL'})} + end + + 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 + + it "ignores attached databases that belong to other billing apps" do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments) + .and_return([ 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', + 'billing_app' => { 'name' => 'sushi' } }}) + ]) + 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 schedule" do + before do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return(example_attachments) + end + + it "schedules the requested database at the specified time" do + stub_pg.schedule({ hour: '07', timezone: 'UTC', + schedule_name: 'HEROKU_POSTGRESQL_RED_URL' }) + stderr, stdout = execute("pg:backups schedule RED --at '07:00 UTC' --app example") + expect(stderr).to be_empty + expect(stdout).to match(/Scheduled automatic daily backups/) + end + + it "finds the right database when there are similarly-named databases" do + additional_attachment = Heroku::Helpers::HerokuPostgresql::Attachment + .new({ + 'app' => {'name' => 'example'}, + 'name' => 'ALSO_HEROKU_POSTGRESQL_IVORY', + 'config_var' => 'ALSO_HEROKU_POSTGRESQL_IVORY_URL', + 'resource' => {'name' => 'loudly-yelling-1239', + 'value' => 'postgres:///not-actually-ivory', + 'type' => 'heroku-postgresql:standard-0', + 'billing_app' => { 'name' => 'example' } }}) + example_attachments << additional_attachment + stub_pg.schedule({ hour: '07', timezone: 'UTC', + schedule_name: 'HEROKU_POSTGRESQL_IVORY_URL' }) + stderr, stdout = execute("pg:backups schedule HEROKU_POSTGRESQL_IVORY_URL --at '07:00 UTC' --app example") + expect(stderr).to be_empty + expect(stdout).to match(/Scheduled automatic daily backups/) + end + + context "demonstrating cultural imperialism" do + { + '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', + 'Z' => 'UTC', + 'GMT' => 'Europe/London', + 'BST' => 'Europe/London', + }.each do |common_but_ambiguous_abbreviation, official_tz_db_name| + it "translates #{common_but_ambiguous_abbreviation} to #{official_tz_db_name}" do + stub_pg.schedule({ hour: '07', timezone: official_tz_db_name, + schedule_name: 'HEROKU_POSTGRESQL_RED_URL' }) + specified_time = "07:00 #{common_but_ambiguous_abbreviation}" + stderr, stdout = execute("pg:backups schedule RED --at '#{specified_time}' --app example") + expect(stderr).to be_empty + expect(stdout).to match(/Scheduled automatic daily backups/) + end + end + 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 }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'gof3r', :to_type => 'pg_restore', num: 8, + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => true, :warnings => 4}, + ] + 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*Completed/) + 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*Completed/) + end + + it "lists successful restores" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/r008\s*Finished with 4 warnings/) + end + + it "lists completed restores with warnings" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/r004\s*Failed/) + 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*Completed/) + 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 +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 +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 +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 +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 +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 +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..e6b693571 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -3,43 +3,57 @@ module Heroku::Command describe Pg do - before do - stub_core - - api.post_app "name" => "example" - api.put_config_vars "example", { - "DATABASE_URL" => "postgres://database_url", - "HEROKU_POSTGRESQL_IVORY_URL" => "postgres://database_url", - "HEROKU_POSTGRESQL_RONIN_URL" => "postgres://ronin_database_url" - } - + def stub_attachments(extra_attachments=[]) any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) do |pg| stub(pg).app_attachments.returns([ Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, + 'app' => {'name' => 'example'}, 'name' => 'HEROKU_POSTGRESQL_IVORY', 'config_var' => 'HEROKU_POSTGRESQL_IVORY_URL', 'resource' => {'name' => 'loudly-yelling-1232', 'value' => 'postgres://database_url', - 'type' => 'heroku-postgresql:ronin' }}), + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'example' } }}), Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, + 'app' => {'name' => 'example'}, 'name' => 'HEROKU_POSTGRESQL_RONIN', 'config_var' => 'HEROKU_POSTGRESQL_RONIN_URL', 'resource' => {'name' => 'softly-mocking-123', 'value' => 'postgres://ronin_database_url', - 'type' => 'heroku-postgresql:ronin' }}), + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'example' } }}), Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, + 'app' => {'name' => 'example'}, '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' }}) - ]) + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'example' } }}) + ].concat(extra_attachments)) + end + + Excon.stub(method: :get, path: %r|^(/apps/example)?/addon-attachments/(example::)?RONIN$|i) do + {status: 200, body: MultiJson.encode({ + name: 'HEROKU_POSTGRESQL_RONIN', + app: {name: 'example'} + })} end end + before do + stub_core + + api.post_app "name" => "example" + api.put_config_vars "example", { + "DATABASE_URL" => "postgres://database_url", + "HEROKU_POSTGRESQL_IVORY_URL" => "postgres://database_url", + "HEROKU_POSTGRESQL_RONIN_URL" => "postgres://ronin_database_url" + } + + stub_attachments + end + after do api.delete_app "example" end @@ -48,8 +62,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 +72,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 +97,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 @@ -94,8 +108,9 @@ module Heroku::Command Fork/Follow: Available Created: 2011-12-13 00:00 UTC Maintenance: not required +Add-on: whatever-something-2323 -=== HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL) +=== HEROKU_POSTGRESQL_IVORY_URL, DATABASE_URL Plan: Ronin Status: available Data Size: 1 MB @@ -104,6 +119,7 @@ module Heroku::Command Fork/Follow: Available Created: 2011-12-13 00:00 UTC Maintenance: not required +Add-on: loudly-yelling-1232 === HEROKU_POSTGRESQL_RONIN_URL Plan: Ronin @@ -114,6 +130,7 @@ module Heroku::Command Fork/Follow: Available Created: 2011-12-13 00:00 UTC Maintenance: not required +Add-on: softly-mocking-123 STDOUT end @@ -134,8 +151,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 @@ -146,51 +163,120 @@ module Heroku::Command Forked From: Database on postgreshost.com:5432/database_name Created: 2011-12-13 00:00 UTC Maintenance: not required +Add-on: softly-mocking-123 STDOUT end 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: %r"(/apps/example)?/addons/#{resource[:name]}") do + { body: MultiJson.encode(resource), status: 200 } + end + + Excon.stub(method: :get, path: %r"/apps/example/addons/([a-zA-Z0-9_]+)") do |request| + url = ronin[:name] + '_URL' + identifier = request[:captures][:path][0].upcase + if url[identifier] + { body: MultiJson.encode(resource), status: 200 } + else + { body: MultiJson.encode(resource), status: 404 } + end + 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 context "credential resets" do it "resets credentials and promotes to DATABASE_URL if it's the main DB" do + Excon.stub(method: :get, path: %r|^(/apps/example)?/addon-attachments/(example::)?iv$|) do + {status: 200, body: MultiJson.encode({ + name: 'HEROKU_POSTGRESQL_IVORY', + app: {name: 'example'} + })} + end 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 end it "does not update DATABASE_URL if it's not the main db" do + Excon.stub(method: :get, path: %r|^(/apps/example)?/addon-attachments/(example::)?follo$|) do + {status: 200, body: MultiJson.encode({ + name: 'HEROKU_POSTGRESQL_FOLLOW', + app: {name: 'example'} + })} + end stub_pg.rotate_credentials api.put_config_vars "example", { "DATABASE_URL" => "postgres://to_reset_credentials", "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 +284,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 +301,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 +318,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 +338,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 +362,132 @@ 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_attachment = + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'example'}, + 'name' => remote, + 'config_var' => remote + '_URL', + 'resource' => {'name' => 'loudly-yelling-1232', + 'value' => "postgres://someurl.test/#{remote}", + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'example' }}}) + local_url = "postgres:///#{local}" + + dump_restore = double() + expect(pg).to receive(:resolve_heroku_attachment).and_return( + remote_attachment) + 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_attachment.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_attachment = + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'example'}, + 'name' => remote, + 'config_var' => remote + '_URL', + 'resource' => {'name' => 'loudly-yelling-1232', + 'value' => "postgres://someurl.test/#{remote}", + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'example' }}}) + local_url = "postgres:///#{local}" + dump_restore = double() + expect(pg).to receive(:resolve_heroku_attachment).and_return( + remote_attachment) + expect(dump_restore).to receive(:execute) + expect(Heroku::Command).to receive(:shift_argument).and_return(remote, local) + expect(PgDumpRestore).to receive(:new).with( + remote_attachment.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 + + describe "#links" do + it "returns attachments consolidated by resource" do + stub_pg.link_list.returns([]) + + # API now returns DATABASE as a regular, old, attachment. + # The test setup in this file does not account for that. + # + stub_attachments([ + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'sushi'}, + 'name' => 'DATABASE', + 'config_var' => 'DATABASE_URL', + 'resource' => {'name' => 'loudly-yelling-1232', + 'value' => 'postgres://database_url', + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'sushi' } }}) + ]) + + stderr, stdout = execute("pg:links") + expect(stdout).to eq <<-OUTPUT +=== HEROKU_POSTGRESQL_IVORY_URL, DATABASE_URL (loudly-yelling-1232) +No data sources are linked into this database. + +=== HEROKU_POSTGRESQL_RONIN_URL (softly-mocking-123) +No data sources are linked into this database. + +=== HEROKU_POSTGRESQL_FOLLOW_URL (whatever-something-2323) +No data sources are linked into this database. + OUTPUT + end + end end end diff --git a/spec/heroku/command/pgbackups_spec.rb b/spec/heroku/command/pgbackups_spec.rb index ed44be4ea..bfbdc8f62 100644 --- a/spec/heroku/command/pgbackups_spec.rb +++ b/spec/heroku/command/pgbackups_spec.rb @@ -2,315 +2,15 @@ require "heroku/command/pgbackups" module Heroku::Command - describe Pgbackups, 'with no databases' do - it "aborts if no database addon is present" do - api.post_app("name" => "example") - stub_core - stderr, stdout = execute("pgbackups:capture") - stderr.should == <<-STDERR - ! Your app has no databases. + describe Pgbackups, 'is removed' do + it "does not list" do + stderr, stdout = execute("pgbackups") + expect(stderr).to eq <<-STDERR + ! 'heroku pgbackups' has been removed. + ! Please see 'heroku pg:backups' instead. + ! More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups STDERR - stdout.should == "" - api.delete_app("example") - end - end - - describe Pgbackups do - before do - @pgbackups = prepare_command(Pgbackups) - @pgbackups.heroku.stub!(:info).and_return({}) - - api.post_app("name" => "example") - api.put_config_vars( - "example", - { - "DATABASE_URL" => "postgres://database", - "HEROKU_POSTGRESQL_IVORY" => "postgres://database", - "PGBACKUPS_URL" => "https://ip:password@pgbackups.heroku.com/client" - } - ) - any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) do |pg| - stub(pg).app_attachments.returns(mock_attachments) - stub(pg).api.returns(api) - end - end - - let(:mock_attachments) { - [ - Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, - 'name' => 'HEROKU_POSTGRESQL_IVORY', - 'config_var' => 'HEROKU_POSTGRESQL_IVORY', - 'resource' => {'name' => 'softly-mocking-123', - 'value' => 'postgres://database', - 'type' => 'heroku-postgresql:baku' }}) - ] - } - - after do - api.delete_app("example") - end - - it "requests a pgbackups transfer list for the index command" do - stub_core - stub_pgbackups.get_transfers.returns([{ - "created_at" => "2012-01-01 12:00:00 +0000", - "started_at" => "2012-01-01 12:00:01 +0000", - "from_name" => "DATABASE", - "size" => "1024", - "progress" => "dump 2048", - "to_name" => "BACKUP", - "to_url" => "s3://bucket/userid/b001.dump" - }]) - - stderr, stdout = execute("pgbackups") - stderr.should == "" - stdout.should == <<-STDOUT -ID Backup Time Status Size Database ----- ------------------------- --------- ---- -------- -b001 2012-01-01 12:00:01 +0000 Capturing 1024 DATABASE -STDOUT - end - - describe "single backup" do - let(:from_name) { "FROM_NAME" } - 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) - end - - it "gets the url for the latest backup if nothing is specified" do - stub_core - stub_pgbackups.get_latest_backup.returns({"public_url" => "http://latest/backup.dump"}) - - old_stdout_isatty = STDOUT.isatty - $stdout.stub!(:isatty).and_return(true) - stderr, stdout = execute("pgbackups:url") - stderr.should == "" - stdout.should == <<-STDOUT -http://latest/backup.dump -STDOUT - $stdout.stub!(: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) - stderr, stdout = execute("pgbackups:url b001") - stderr.should == "" - stdout.should == <<-STDOUT -http://latest/backup.dump -STDOUT - $stdout.stub!(: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) - - @pgbackups.capture - end - - 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) - - @pgbackups.capture - end - - it "destroys no backup without a name" do - stub_core - stderr, stdout = execute("pgbackups:destroy") - stderr.should == <<-STDERR - ! Usage: heroku pgbackups:destroy BACKUP_ID - ! Must specify BACKUP_ID to destroy. -STDERR - stdout.should == "" - end - - it "destroys a backup" do - stub_core - stub_pgbackups.get_backup("b001").returns({}) - stub_pgbackups.delete_backup("b001").returns({}) - - stderr, stdout = execute("pgbackups:destroy b001") - stderr.should == "" - stdout.should == <<-STDOUT -Destroying b001... done -STDOUT - end - - - context "on errors" do - def stub_failed_capture(log) - @backup_obj = { - "error_at" => Time.now.to_s, - "finished_at" => Time.now.to_s, - "log" => log, - 'to_url' => 'postgres://from/bar' - } - stub_core - stub_pgbackups.create_transfer.returns(@backup_obj) - stub_pgbackups.get_transfer.returns(@backup_obj) - - any_instance_of(Heroku::Command::Pgbackups) do |pgbackups| - stub(pgbackups).app_attachments.returns( - mock_attachments - ) - end - end - - it 'aborts on a generic error' do - stub_failed_capture "something generic" - stderr, stdout = execute("pgbackups:capture") - stderr.should == <<-STDERR - ! An error occurred and your backup did not finish. - ! Please run `heroku logs --ps pgbackups` for details. -STDERR - stdout.should == <<-STDOUT - -HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar - -\r\e[0K... 0 - -STDOUT - end - - 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 - ! 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 - -HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar - -\r\e[0K... 0 - -STDOUT - end - - 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 - ! 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 - -HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar - -\r\e[0K... 0 - -STDOUT - end - end - end - - 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) - end - - it "should receive a confirm_command on restore" do - @pgbackups_client.stub!(:get_latest_backup) { {"to_url" => "s3://bucket/user/bXXX.dump"} } - - @pgbackups.should_receive(:confirm_command).and_return(false) - @pgbackups_client.should_not_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) - end - - context "for commands which perform restores" do - before do - @backup_obj = { - "to_name" => "TO_NAME", - "to_url" => "s3://bucket/userid/bXXX.dump", - "from_url" => "FROM_NAME", - "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) - end - - it "should default to the latest backup" do - @pgbackups.stub(:args).and_return([]) - mock(@pgbackups_client).get_latest_backup.returns(@backup_obj) - @pgbackups.restore - end - - - 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) - 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) - @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) - end - - def stub_error_backup_with_log(log) - @backup_obj = { - "error_at" => Time.now.to_s, - "log" => log - } - - @pgbackups_client.should_receive(:create_transfer) { @backup_obj } - @pgbackups.stub!(: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.") - @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/ } - @pgbackups.restore - end - end + expect(stdout).to eq("") end 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..ec24aecb9 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 @@ -213,54 +272,69 @@ describe "ps:scale" do + it "displays existing dyno formation" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [ + {"quantity" => 1, "size" => "2X", "type" => "web"}, + {"quantity" => 2, "size" => "1X", "type" => "worker"}], status: 200}) + stderr, stdout = execute("ps:scale") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +web=1:2X worker=2:1X +STDOUT + end + it "can scale using key/value format" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [], status: 200}) Excon.stub({ :method => :patch, :path => "/apps/example/formation" }, { :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 it "can scale relative amounts" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [], status: 200}) Excon.stub({ :method => :patch, :path => "/apps/example/formation" }, { :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 it "can resize while scaling" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [], status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { - "updates" => [{"process" => "web", "quantity" => "4", "size" => "2X"}] + "updates" => [{"type" => "web", "quantity" => 4, "size" => "2X"}] }.to_json }, :body => [{"quantity" => 4, "size" => "2X", "type" => "web"}], :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 it "can scale multiple types in one call" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [], status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { "updates" => [ - {"process" => "web", "quantity" => "4", "size" => "1X"}, - {"process" => "worker", "quantity" => "2", "size" => "2x"}, + {"type" => "web", "quantity" => 4, "size" => "1X"}, + {"type" => "worker", "quantity" => 2, "size" => "2x"}, ] }.to_json }, @@ -272,26 +346,27 @@ :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 it "accepts PX as a valid size" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [], status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { - "updates" => [{"process" => "web", "quantity" => "4", "size" => "PX"}] + "updates" => [{"type" => "web", "quantity" => 4, "size" => "PX"}] }.to_json }, :body => [{"quantity" => 4, "size" => "PX", "type" => "web"}], :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 @@ -300,58 +375,71 @@ describe "ps:resize" do it "can resize using a key/value format" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [{"type" => "web", "size" => "1X", "quantity" => 1}], status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { - "updates" => [{"process" => "web", "size" => "2X"}] + "updates" => [{"type" => "web", "size" => "2X", "quantity" => 1}] }.to_json }, :body => [{"quantity" => 2, "size" => "2X", "type" => "web"}], :status => 200 ) stderr, stdout = execute("ps:resize web=2X") - stderr.should == "" - stdout.should == <<-STDOUT -Resizing and restarting the specified dynos... done -web dynos now 2X ($0.10/dyno-hour) + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +dyno type qty cost/mo +---- ---- --- ------- +web 1X 1 36 STDOUT end it "can resize multiple types in one call" do + formation = [ + {"type" => "web", "size" => "1X", "quantity" => 1}, + {"type" => "worker", "size" => "1X", "quantity" => 1}, + ] + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: formation, status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { "updates" => [ - {"process" => "web", "size" => "4x"}, - {"process" => "worker", "size" => "2X"} + {"type" => "web", "size" => "4x", "quantity" => 1}, + {"type" => "worker", "size" => "2X", "quantity" => 1} ] }.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 -Resizing and restarting the specified dynos... done -web dynos now 4X ($0.20/dyno-hour) -worker dynos now 2X ($0.10/dyno-hour) + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +dyno type qty cost/mo +------ ---- --- ------- +web 1X 1 36 +worker 1X 1 36 STDOUT end - it "accepts P as a valid size, with a price of $0.80/hour" do + it "accepts PX as a valid size, with a price of $0.80/hour" do + formation = [ + {"type" => "web", "size" => "1X", "quantity" => 1}, + {"type" => "worker", "size" => "1X", "quantity" => 1}, + ] + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: formation, status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { "updates" => [ - {"process" => "web", "size" => "PX"}, - {"process" => "worker", "size" => "Px"} + {"type" => "web", "size" => "PX", "quantity" => 1}, + {"type" => "worker", "size" => "Px", "quantity" => 1} ] }.to_json }, @@ -362,11 +450,12 @@ :status => 200 ) stderr, stdout = execute("ps:resize web=PX worker=Px") - stderr.should == "" - stdout.should == <<-STDOUT -Resizing and restarting the specified dynos... done -web dynos now PX ($0.80/dyno-hour) -worker dynos now PX ($0.80/dyno-hour) + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +dyno type qty cost/mo +------ ---- --- ------- +web 1X 1 36 +worker 1X 1 36 STDOUT end @@ -376,16 +465,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 +497,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 +506,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 +519,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 +528,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..71b942e0f 100644 --- a/spec/heroku/command/run_spec.rb +++ b/spec/heroku/command/run_spec.rb @@ -15,26 +15,13 @@ api.delete_app("example") end - describe "run" do - it "runs a command" do - stub_rendezvous.start { $stdout.puts "output" } - - stderr, stdout = execute("run bin/foo") - stderr.should == "" - stdout.should == <<-STDOUT -Running `bin/foo` attached to terminal... up, run.1 -output -STDOUT - end - end - 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 +39,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 +51,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 +63,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..b3efa909a 100644 --- a/spec/heroku/command/version_spec.rb +++ b/spec/heroku/command/version_spec.rb @@ -6,9 +6,11 @@ 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} +heroku-cli/4.0.0-4f2c5c5 (amd64-darwin) go1.5 +You have no installed plugins. STDOUT end diff --git a/spec/heroku/command_spec.rb b/spec/heroku/command_spec.rb index ff3e277ab..fd413769e 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,30 @@ 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} +heroku-cli/4.0.0-4f2c5c5 (amd64-darwin) go1.5 +You have no installed plugins. STDOUT end @@ -182,12 +211,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 +228,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/env_spec.rb b/spec/heroku/helpers/env_spec.rb new file mode 100644 index 000000000..06e6a326b --- /dev/null +++ b/spec/heroku/helpers/env_spec.rb @@ -0,0 +1,45 @@ +# encoding: utf-8 +# +require "spec_helper" +require "heroku/helpers/env" + +module Heroku::Helpers + describe Env do + context "[]" do + + before do + allow(ENV).to receive(:[]).and_return(nil) + allow(Heroku::Helpers).to receive(:running_on_windows?).and_return(true) + end + + after do + allow(ENV).to receive(:[]).and_call_original + allow(Heroku::Helpers).to receive(:running_on_windows?).and_call_original + end + + it "Passes through non ASCII-8BIT strings without re-encoding" do + allow(ENV).to receive(:[]).with('foo').and_return("foo".encode("ISO-8859-1")) + + actual = Heroku::Helpers::Env['foo'] + + expect(actual).to eq("foo") + expect(actual.encoding).to eq(Encoding::ISO_8859_1) + end + + it "Passes through nil without failing" do + allow(ENV).to receive(:[]).with('foo').and_return(nil) + expect(Heroku::Helpers::Env['foo']).to be_nil + end + + it "Attempts to convert ASCII_8BIT" do + bad_encoding = "\u0412".force_encoding("ASCII-8BIT").freeze # verify we work with frozen values + allow(ENV).to receive(:[]).with('foo').and_return(bad_encoding) + + actual = Heroku::Helpers::Env['foo'] + + expect(actual).to eq("\u0412") + expect(actual.encoding).to eq(Encoding::UTF_8) + end + end + end +end diff --git a/spec/heroku/helpers/heroku_postgresql_spec.rb b/spec/heroku/helpers/heroku_postgresql_spec.rb index 5f0d285a9..92a27abf8 100644 --- a/spec/heroku/helpers/heroku_postgresql_spec.rb +++ b/spec/heroku/helpers/heroku_postgresql_spec.rb @@ -6,11 +6,32 @@ 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', api) + allow(@resolver).to receive(:app_config_vars) { app_config_vars } + allow(@resolver).to receive(:app_attachments) { app_attachments } + + # loosely emulate the API resolution + allow(api).to receive(:request).with(hash_including(method: :get, path: %r|^(/apps/appname)?/addon-attachments/|)) do |req| + identifier = req[:path].scan(%r|[^/]+$|)[0] + + matches = app_config_vars.keys.grep(Regexp.new(identifier, "i")) + + case matches.size + when 1 + Struct.new(:body).new({ + 'name' => matches.first.gsub(/_URL$/,''), + 'app' => { 'name' => 'appname' } + }) + when 0 + raise Heroku::API::Errors::NotFound.new('not found', Struct.new(:body).new({})) + else + raise Heroku::API::Errors::RequestFailed.new('ambiguous', Struct.new(:body).new({})) + end + end end + let(:api) { double(:api) } + let(:app_config_vars) do { "DATABASE_URL" => "postgres://default", @@ -19,21 +40,23 @@ } end - let(:app_attachments) { - [ Attachment.new({ 'name' => 'HEROKU_POSTGRESQL_IVORY', - 'config_var' => 'HEROKU_POSTGRESQL_IVORY_URL', - 'app' => {'name' => 'sushi' }, - 'resource' => {'name' => 'softly-mocking-123', - 'value' => 'postgres://default', - 'type' => 'heroku-postgresql:baku' }}), - Attachment.new({ 'name' => 'HEROKU_POSTGRESQL_BLACK', - 'config_var' => 'HEROKU_POSTGRESQL_BLACK_URL', - 'app' => {'name' => 'sushi' }, - 'resource' => {'name' => 'quickly-yelling-2421', - 'value' => 'postgres://black', - 'type' => 'heroku-postgresql:zilla' }}) - ] - } + let(:app_attachments) { + [ Attachment.new({ 'name' => 'HEROKU_POSTGRESQL_IVORY', + 'config_var' => 'HEROKU_POSTGRESQL_IVORY_URL', + 'app' => {'name' => 'sushi' }, + 'resource' => {'name' => 'softly-mocking-123', + 'value' => 'postgres://default', + 'type' => 'heroku-postgresql:baku', + 'billing_app' => { 'name' => 'sushi' }}}), + Attachment.new({ 'name' => 'HEROKU_POSTGRESQL_BLACK', + 'config_var' => 'HEROKU_POSTGRESQL_BLACK_URL', + 'app' => {'name' => 'sushi' }, + 'resource' => {'name' => 'quickly-yelling-2421', + 'value' => 'postgres://black', + 'type' => 'heroku-postgresql:zilla', + 'billing_app' => { 'name' => 'sushi' } }}) + ] + } context "when the DATABASE_URL has query options" do let(:app_config_vars) do @@ -47,37 +70,42 @@ 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") + 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 + before do + allow(api).to receive(:request).with(hash_including(method: :get, path: %r|^/apps/app2/addon-attachments/black|)) do + Struct.new(:body).new({ 'name' => 'HEROKU_POSTGRESQL_BLACK' }) + end + end + 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,89 +121,87 @@ 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("No default database configured in DATABASE_URL. Valid alternatives 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("No default database configured in DATABASE_URL. Valid alternatives 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("No default database configured in DATABASE_URL. Valid alternatives are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") att = @resolver.resolve('', "DATABASE_URL") end - - end end diff --git a/spec/heroku/helpers_spec.rb b/spec/heroku/helpers_spec.rb index fc7925ec0..ac48c16ee 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,14 +55,123 @@ 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 end + context "home_directory" do + before do + allow(Heroku::Helpers).to receive(:running_on_windows?).and_return(true) + + # I would much rather have removed / set ENV variables here, + # but things get manged by the []= operator in windows. + # + # ENV['f'] = "\u0412" ; ENV['f'].encoding == ASCII-8BIT + allow(ENV).to receive(:[]).and_return(nil) + + @tmp_dir = Dir.mktmpdir.encode('utf-8') + @home_dir = File.join(@tmp_dir, "\u0412") + @windows_home_dir = @home_dir.gsub(/\//, "\\") + Dir.mkdir(@home_dir) + end + + after do + allow(Heroku::Helpers).to receive(:running_on_windows?).and_call_original + allow(ENV).to receive(:[]).and_call_original + + Dir.rmdir(@home_dir) + Dir.rmdir(@tmp_dir) + end + + it "should throw ArgumentError when nothing is defined" do + expect{ Heroku::Helpers.orig_home_directory }.to raise_error(ArgumentError) + end + + it "should handle crillic characters properly in HOME" do + allow(ENV).to receive(:[]).with("HOME").and_return(@windows_home_dir) + allow(ENV).to receive(:[]).with("HOMEPATH").and_return("foo") + allow(ENV).to receive(:[]).with("HOMEDRIVE").and_return("bar") + allow(ENV).to receive(:[]).with("USERPROFILE").and_return("biz") + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + + it "should not use HOMEDRIVE when HOMEPATH is not defined" do + allow(ENV).to receive(:[]).with("HOMEDRIVE").and_return(@windows_home_dir[0..1]) + expect{ Heroku::Helpers.orig_home_directory }.to raise_error(ArgumentError) + end + + it "should handle crillic characters properly in HOMEDRIVE / HOMEPATH" do + allow(ENV).to receive(:[]).with("HOMEDRIVE").and_return(@windows_home_dir[0..1]) + allow(ENV).to receive(:[]).with("HOMEPATH").and_return(@windows_home_dir[2..-1]) + allow(ENV).to receive(:[]).with("USERPROFILE").and_return("biz") + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + + it "should handle crillic characters properly in USERPROFILE" do + allow(ENV).to receive(:[]).with("USERPROFILE").and_return(@windows_home_dir) + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + end + + context "home_directory (compatibility)" do + before do + allow(Heroku::Helpers).to receive(:running_on_windows?).and_return(true) + + # I would much rather have removed / set ENV variables here, + # but things get manged by the []= operator in windows. + # + # ENV['f'] = "\u0412" ; ENV['f'].encoding == ASCII-8BIT + @home = ENV.delete('HOME') + @home_drive = ENV.delete('HOMEDRIVE') + @home_path = ENV.delete('HOMEPATH') + @user_profile = ENV.delete('USERPROFILE') + + @home_dir = Heroku::Helpers.home_directory + @windows_home_dir = @home_dir.gsub(/\//, "\\") + end + + after do + allow(Heroku::Helpers).to receive(:running_on_windows?).and_call_original + + ENV['HOME'] = @home + ENV['HOMEDRIVE'] = @home_drive + ENV['HOMEPATH'] = @home_path + ENV['USERPROFILE'] = @user_profile + end + + it "should throw ArgumentError when nothing is defined" do + expect{ Heroku::Helpers.orig_home_directory }.to raise_error(ArgumentError) + end + + it "should use HOME" do + ENV["HOME"] = @windows_home_dir + ENV["HOMEPATH"] = "foo" + ENV["HOMEDRIVE"] = "bar" + ENV["USERPROFILE"] = "biz" + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + + it "should not use HOMEDRIVE when HOMEPATH is not defined" do + ENV["HOMEDRIVE"] = @windows_home_dir[0..1] + expect{ Heroku::Helpers.orig_home_directory }.to raise_error(ArgumentError) + end + + it "should use HOMEDRIVE / HOMEPATH" do + ENV["HOMEDRIVE"] = @windows_home_dir[0..1] + ENV["HOMEPATH"] = @windows_home_dir[2..-1] + ENV["USERPROFILE"] = "biz" + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + + it "should use USERPROFILE" do + ENV["USERPROFILE"] = @windows_home_dir + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + end end end diff --git a/spec/heroku/jsplugin_spec.rb b/spec/heroku/jsplugin_spec.rb new file mode 100644 index 000000000..6e99bb605 --- /dev/null +++ b/spec/heroku/jsplugin_spec.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +require "spec_helper" +require "heroku/jsplugin" + +module Heroku + describe JSPlugin do + context "app_dir" do + before do + allow(Heroku::JSPlugin).to receive(:windows?).and_return(true) + allow(ENV).to receive(:[]).and_return(nil) + end + + it "should use LOCALAPPDATA only in windows" do + allow(ENV).to receive(:[]).with("LOCALAPPDATA").and_return("foo") + allow(ENV).to receive(:[]).with("XDG_DATA_HOME").and_return("bar") + expect(Heroku::JSPlugin.app_dir).to eq(File.join("foo", "heroku")) + + allow(Heroku::JSPlugin).to receive(:windows?).and_return(false) + expect(Heroku::JSPlugin.app_dir).to eq(File.join("bar", "heroku")) + end + + it "should not use XDG_DATA_HOME if defined" do + allow(ENV).to receive(:[]).with("XDG_DATA_HOME").and_return("bar") + expect(Heroku::JSPlugin.app_dir).to eq(File.join("bar", "heroku")) + end + + it "should default to home directory" do + expect(Heroku::JSPlugin.app_dir).to eq(File.join(Heroku::Helpers.home_directory, ".heroku")) + end + + after do + allow(Heroku::JSPlugin).to receive(:windows?).and_call_original + allow(ENV).to receive(:[]).and_call_original + end + end + end +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..09b1ca89d 100644 --- a/spec/heroku/plugin_spec.rb +++ b/spec/heroku/plugin_spec.rb @@ -1,90 +1,81 @@ require "spec_helper" require "heroku/plugin" +require "tmpdir" module Heroku describe Plugin do 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(File.expand_path('/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) + @sandbox = Dir.mktmpdir + @plugin_folder = File.join(Dir.mktmpdir, 'heroku_plugin') + FileUtils.mkdir_p(@plugin_folder) + allow(Dir).to receive(:pwd).and_return(@sandbox) + allow(Plugin).to receive(:directory).and_return(@sandbox) end after(:each) do FileUtils.rm_rf(@sandbox) + FileUtils.rm_rf(@plugin_folder) end 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 - plugin_folder = "/tmp/heroku_plugin" - 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" + `cd #{@plugin_folder} && git init && echo test > README && git add README && git commit -m my_plugin` + Plugin.new(@plugin_folder).install + expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq(File.read("#{@plugin_folder}/README")) end it "reinstalls over old copies" do - plugin_folder = "/tmp/heroku_plugin" - 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 - Plugin.new(plugin_folder).install - File.directory?("#{@sandbox}/heroku_plugin").should be_true - File.read("#{@sandbox}/heroku_plugin/README").should == "test\n" + `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 + expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq(File.read("#{@plugin_folder}/README")) end context "update" do before(:each) do - plugin_folder = "/tmp/heroku_plugin" - 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 - `cd #{plugin_folder} && echo 'updated' > README && git add . && git commit -m 'my plugin update'` + @plugin_folder = File.join(Dir.mktmpdir, 'heroku_plugin') + 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 + `cd #{@plugin_folder} && echo updated > README && git add . && git commit -m my_plugin_update` end 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" + expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq(File.read("#{@plugin_folder}/README")) 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 + if Heroku::Helpers.running_on_windows? + xit "raises exception on symlinked plugins" do + # using mklink is problematic & buggy + end + else + it "raises exception on symlinked plugins" do + `cd #{@sandbox} && ln -s heroku_plugin heroku_plugin_symlink` + expect { Plugin.new('heroku_plugin_symlink').update }.to raise_error Heroku::Plugin::ErrorUpdatingSymlinkPlugin end - stderr.should == <<-STDERR - ! heroku_plugin is a legacy plugin installation. - ! Enable updating by reinstalling with `heroku plugins:install`. -STDERR - 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 end end @@ -93,21 +84,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 +109,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 +117,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 +126,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 +142,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..16a4002ff 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 = IO.binread(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 = IO.binread(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..fbb73c2e2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,20 +1,26 @@ -$stdin = File.new("/dev/null") +require "heroku/helpers" + +if (Heroku::Helpers.running_on_windows?) + $stdin = File.new("nul") +else + $stdin = File.new("/dev/null") +end 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" require 'tmpdir' require "webmock/rspec" +require "shellwords" include WebMock::API @@ -35,19 +41,19 @@ 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(" ") + args = command_line.shellsplit command = args.shift Heroku::Command.load @@ -62,15 +68,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 +141,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 +156,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 @@ -201,17 +220,47 @@ def bash(cmd) require "heroku/helpers" module Heroku::Helpers @home_directory = Dir.mktmpdir - undef_method :home_directory + alias_method :orig_home_directory, :home_directory 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 "heroku/jsplugin" +class Heroku::JSPlugin + def self.topics; [] end + def self.commands; [] end + def self.setup; end + def self.run; end + def self.plugins; [] end + def self.version; 'heroku-cli/4.0.0-4f2c5c5 (amd64-darwin) go1.5' 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..0bb9ca108 --- /dev/null +++ b/spec/support/addons_helper.rb @@ -0,0 +1,56 @@ +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, + web_url: "https://attachment-sso" + } + 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..a163d62d4 --- /dev/null +++ b/tasks/deb.rake @@ -0,0 +1,54 @@ +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/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/heroku-#{version}.deb") => distribution_files("deb") do |t| + mkdir_p File.dirname(t.name) + 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..d11cd2b0c --- /dev/null +++ b/tasks/exe.rake @@ -0,0 +1,124 @@ +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-2.1.7.exe", "git-2.6.3.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/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)) + + # compile installer under wine! + setup_wine_env + system 'wine', 'C:\inno\ISCC.exe', 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 + + # sign executable + system "osslsigncode -pkcs12 #{resource('exe/heroku-codesign-cert.pfx')} \ + -pass '#{ENV['HEROKU_WINDOWS_SIGNING_PASS']}' \ + -n 'Heroku Toolbelt' \ + -i https://toolbelt.heroku.com/ \ + -in #{exe_task.name} \ + -out #{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 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..1cde72bdb --- /dev/null +++ b/tasks/pkg.rake @@ -0,0 +1,57 @@ +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('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