
Oh no! It seems as though we've encountered a problem! Please try your request again.
57 |├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── CHANGELOG.md ├── CHANGELOG_old.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── bin └── tugboat ├── config └── license_finder.yml ├── features ├── cassettes │ └── config │ │ ├── Array_of_SSH_Keys_in_Config.yml │ │ └── Single_SSH_key_as_number_in_config.yml ├── step_definitions │ └── steps.rb ├── support │ └── env.rb └── tugboat │ ├── config_array_of_ssh_keys.feature │ ├── config_current_directory.feature │ └── config_number_key.feature ├── lib ├── tugboat.rb └── tugboat │ ├── cli.rb │ ├── config.rb │ ├── middleware.rb │ ├── middleware │ ├── add_key.rb │ ├── ask_for_credentials.rb │ ├── backup_setting.rb │ ├── base.rb │ ├── check_configuration.rb │ ├── check_credentials.rb │ ├── check_droplet_active.rb │ ├── check_droplet_inactive.rb │ ├── check_snapshot_parameters.rb │ ├── config.rb │ ├── confirm_action.rb │ ├── create_droplet.rb │ ├── custom_logger.rb │ ├── destroy_droplet.rb │ ├── destroy_image.rb │ ├── find_droplet.rb │ ├── find_image.rb │ ├── halt_droplet.rb │ ├── info_droplet.rb │ ├── info_image.rb │ ├── inject_client.rb │ ├── inject_configuration.rb │ ├── list_droplets.rb │ ├── list_images.rb │ ├── list_regions.rb │ ├── list_sizes.rb │ ├── list_snapshots.rb │ ├── list_ssh_keys.rb │ ├── password_reset.rb │ ├── rebuild_droplet.rb │ ├── resize_droplet.rb │ ├── restart_droplet.rb │ ├── scp_droplet.rb │ ├── snapshot_droplet.rb │ ├── ssh_droplet.rb │ ├── start_droplet.rb │ └── wait_for_state.rb │ └── version.rb ├── license └── dependency_decisions.yml ├── spec ├── cli │ ├── add_key_spec.rb │ ├── authorize_cli_spec.rb │ ├── backup_setting_spec.rb │ ├── config_cli_spec.rb │ ├── create_cli_spec.rb │ ├── debug_cli_spec.rb │ ├── destroy_cli_spec.rb │ ├── destroy_image_cli_spec.rb │ ├── droplets_cli_spec.rb │ ├── env_variable_spec.rb │ ├── halt_cli_spec.rb │ ├── help_cli_spec.rb │ ├── images_cli_spec.rb │ ├── info_cli_spec.rb │ ├── info_image_cli_spec.rb │ ├── keys_cli_spec.rb │ ├── password_reset_cli_spec.rb │ ├── rebuild_cli_spec.rb │ ├── regions_cli_spec.rb │ ├── resize_cli_spec.rb │ ├── restart_cli_spec.rb │ ├── scp_cli_spec.rb │ ├── sizes_cli_spec.rb │ ├── snapshot_cli_spec.rb │ ├── snapshots_cli_spec.rb │ ├── ssh_cli_spec.rb │ ├── start_cli_spec.rb │ ├── verify_cli_spec.rb │ ├── version_cli_spec.rb │ └── wait_cli_spec.rb ├── config_spec.rb ├── fixtures │ ├── 401.json │ ├── 500.html │ ├── create_droplet.json │ ├── create_ssh_key.json │ ├── create_ssh_key_from_file.json │ ├── disable_backups_response.json │ ├── droplet_no_network.json │ ├── droplet_start_response.json │ ├── enable_backups_response.json │ ├── not_found.json │ ├── password_reset_response.json │ ├── power_cycle_response.json │ ├── resize_droplet.json │ ├── restart_response.json │ ├── show_coreos_image.json │ ├── show_droplet.json │ ├── show_droplet_inactive.json │ ├── show_droplet_user_image.json │ ├── show_droplets.json │ ├── show_droplets_empty.json │ ├── show_droplets_paginated_first.json │ ├── show_droplets_paginated_last.json │ ├── show_droplets_private_ip.json │ ├── show_image.json │ ├── show_images.json │ ├── show_images_empty.json │ ├── show_images_global.json │ ├── show_keys.json │ ├── show_redmine_image.json │ ├── show_regions.json │ ├── show_sizes.json │ ├── show_snapshots.json │ ├── show_snapshots_empty.json │ ├── shutdown_response.json │ ├── snapshot_response.json │ ├── ubuntu_image_9801951.json │ └── user_data.sh ├── middleware │ ├── base_spec.rb │ ├── check_configuration_spec.rb │ ├── check_credentials_spec.rb │ ├── check_droplet_active_spec.rb │ ├── check_droplet_inactive_spec.rb │ ├── find_droplet_spec.rb │ ├── find_image_spec.rb │ ├── inject_client_spec.rb │ ├── inject_configuration_spec.rb │ └── ssh_droplet_spec.rb ├── shared │ └── environment.rb └── spec_helper.rb ├── tmp └── .gitkeep └── tugboat.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | *~ 4 | *.swp 5 | *.swo 6 | .bundle 7 | .rvmrc 8 | Gemfile.lock 9 | coverage 10 | doc/* 11 | log/* 12 | pkg/* 13 | tmp/* 14 | # Added file to not force other to use it. 15 | .overcommit.yml 16 | .yardoc/ 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.1.10 5 | - 2.2.7 6 | - 2.3.4 7 | - 2.4.1 8 | script: 9 | - "bundle exec rake spec" 10 | - "bundle exec rake features" 11 | - "bundle exec license_finder" 12 | -------------------------------------------------------------------------------- /CHANGELOG_old.md: -------------------------------------------------------------------------------- 1 | ## 0.2.1 (UNRELEASED) 2 | 3 | 4 | ## 0.2.0 (Feburary 15, 2014) 5 | 6 | FEATURES: 7 | 8 | - [Pierre](https://github.com/spearway) added an `info-image` and `destroy-image` 9 | command, letting you deal with your images from Tugboat. It's great. [GH-91] 10 | - [Pierre](https://github.com/spearway) also added a `rebuild` command, 11 | letting you take an existing droplet and recreate it from scratch. [GH-90] 12 | 13 | IMPROVEMENTS: 14 | 15 | - [Dale](https://github.com/Vel0x) made it so fuzzy name searching 16 | is case insensitive. We wonder why we didn't do this earlier, really. [GH-88] 17 | - There is now a `--quiet/-q` flag, which makes Tugboat be silent, as it 18 | can get a little obnoxious. [GH-87] 19 | - [Andrew](https://github.com/4n3w) hooked up a `backups_enabled` option 20 | for creating droplets. [GH-82] 21 | 22 | 23 | ## 0.0.9 (December 24, 2013) 24 | 25 | FEATURES: 26 | 27 | - [Pete](https://github.com/petems) added the ability to add an 28 | ssh key to your account. [GH-64] 29 | - [Caleb](https://github.com/calebreach) gave us an easy way 30 | to pass a command through to a machine with the `-c` command. [GH-73] 31 | 32 | IMPROVEMENTS: 33 | 34 | - [Andrew](https://github.com/4n3w) added a private networking option. [GH-75] 35 | 36 | BUG FIXES: 37 | 38 | - [Zo](https://github.com/obradovic) made our default image 13.04 [GH-76] 39 | - Issues with the JSON dependency in 2.0.0 were resolved. [GH-80] 40 | 41 | 42 | ## 0.0.8 (September 7, 2013) 43 | 44 | FEATURES: 45 | 46 | - [Pete](https://github.com/petems) added configuration defaults 47 | that you can set. [GH-61] 48 | - [Pete](https://github.com/petems) added log filtering to debug output. 49 | `DEBUG=1` now gives you filtered logs, `DEBUG=2`, raw. [GH-58] 50 | 51 | IMPROVEMENTS: 52 | 53 | - Error messages are now caught at the Faraday level and displayed 54 | back to the user. 55 | 56 | BUG FIXES: 57 | 58 | - [Ørjan](https://github.com/blom) added a color reset on the `list` 59 | command. [GH-57] 60 | 61 | ## 0.0.7 (August 2, 2013) 62 | 63 | IMPROVEMENTS: 64 | 65 | - [Pete](https://github.com/petems) made it clearer to the user 66 | if they don't have any droplets or images. [GH-48], [GH-49] 67 | 68 | BUG FIXES: 69 | 70 | - Fix the initial check for authorization after `authorize` [GH-41] 71 | 72 | ## 0.0.6 (June 25, 2013) 73 | 74 | FEATURES: 75 | 76 | - [Ørjan](https://github.com/blom) added a `start` command, which 77 | let's you start a droplet. [GH-30] 78 | - [Ørjan](https://github.com/blom) added a `resize` command, which 79 | let's you resize a droplet. [GH-40] 80 | - [Ørjan](https://github.com/blom) added a `password-reset` command 81 | [GH-45] 82 | - Added a the `wait` command, allowing you to "wait" for a droplet 83 | to enter a state. [GH-46] 84 | 85 | IMPROVEMENTS: 86 | 87 | - [Ørjan](https://github.com/blom) added an `--ssh-opts` flag, for the 88 | `ssh` command. [GH-38] 89 | - Droplet state is checked for some commands. For example, a droplet 90 | can't be started if it's active. [GH-31] 91 | 92 | BUG FIXES: 93 | 94 | - DigitalOcean changed their `image_id`'s, so the defaults for `create` 95 | were updated. [GH-39] 96 | 97 | ## 0.0.5 (May 4, 2013) 98 | 99 | FEATURES: 100 | 101 | - [Ørjan](https://github.com/blom) added a `regions` command, which 102 | returns a list of available DigitalOcean regions. You can specify 103 | which region to use while creating: `tugboat create foobar -r 2`. [GH-18] 104 | - [Ørjan](https://github.com/blom) added an ssh_user option to the 105 | `ssh` command. This lets you specify the user to connect as on 106 | a per-command basis, as well as in your `.tugboat`. 107 | - [Ørjan](https://github.com/blom) added a `sizes` command, which 108 | returns a list of available sizes. You can specify which size to 109 | use while creating: `tugboat create foobar -s 66` [GH-19] 110 | - [Ørjan](https://github.com/blom) added a `hard` flag to 111 | `halt` and `restart`. This cycles the Droplet's power. `tugboat restart --hard` [GH-27] 112 | 113 | IMPROVEMENTS: 114 | 115 | - Tugboat now returns proper status codes for successes and failures. 116 | [GH-21] 117 | - Support for MRI 1.8.7 118 | - CTRL+C's, SIG-INT's are now caught and quietly kill Tugboat without 119 | a stacktrace. 120 | 121 | ## 0.0.4 (April 23, 2013) 122 | 123 | BUG FIXES: 124 | 125 | - Fix a syntax error caused by the order of arguments on `snapshot`. 126 | This changes the argument order and is a breaking change [GH-10]. 127 | - Fix an issue with looking up a droplet by it's `--name`. A variable 128 | was changed, and because it was shadowed passed inspection. 129 | 130 | IMPROVEMENTS: 131 | 132 | - Added a warning for snapshotting a droplet in a non-powered off 133 | state. DigitalOcean currently doesn't return an error from their API. 134 | - Added a `--confirm` or `-c` to confirmed actions, like destroy. [GH-7] 135 | - [Ørjan](https://github.com/blom) added a `--version` command to see 136 | what version of Tugboat you're using. 137 | - Substantially more test coverage - all of the commands (except `ssh` are 138 | now integration tested. [GH-15] 139 | 140 | FEATURES: 141 | 142 | - Optionally add a list of ssh_key_ids when creating a droplet. These 143 | SSH keys will automatically be added to your droplet. 144 | - Show a list of SSH keys on your account with `tugboat keys` 145 | - [Phil](https://github.com/PhilETaylor) added the ability to specify 146 | an `--ssh-port` on `tugboat ssh`, as well as set a default in your `.tugboat` [GH-13] 147 | 148 | ## 0.0.3 (April 15, 2013) 149 | 150 | Initial release. 151 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at webmaster@petersouter.co.uk. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Fork it 4 | 2. Create your feature branch (`git checkout -b my-new-feature`) 5 | 3. Commit your changes (`git commit -am 'Add some feature'`) 6 | 4. Push to the branch (`git push origin my-new-feature`) 7 | 5. Create new Pull Request 8 | 9 | 10 | ## Development Environment 11 | 12 | To add a feature, fix a bug, or to run a development build of Tugboat 13 | on your machine, clone down the repo and run: 14 | 15 | $ bundle install --path vendor/bundle 16 | 17 | You can then execute tugboat: 18 | 19 | $ bundle exec tugboat [command] 20 | 21 | As well as run the tests: 22 | 23 | $ bundle exec rspec 24 | Or 25 | $ rake spec 26 | 27 | To install the gem on your system from source: 28 | 29 | $ bundle exec rake install 30 | 31 | If you need help with your environment, feel free to open an issue. 32 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in tugboat.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jack Pearkes 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.require(:development) 3 | 4 | require 'bundler/gem_tasks' 5 | require 'rspec/core/rake_task' 6 | require 'cucumber/rake/task' 7 | require 'rubocop/rake_task' 8 | 9 | RSpec::Core::RakeTask.new(:spec) 10 | 11 | Cucumber::Rake::Task.new(:features) do |t| 12 | t.cucumber_opts = %w(--format pretty --order random) 13 | end 14 | 15 | RuboCop::RakeTask.new(:rubocop) do |t| 16 | t.options = ['--display-cop-names'] 17 | end 18 | 19 | task default: [:spec, :features] 20 | 21 | begin 22 | require 'github_changelog_generator/task' 23 | GitHubChangelogGenerator::RakeTask.new :changelog do |config| 24 | require 'tugboat/version' 25 | version = Tugboat::VERSION 26 | config.user = 'petems' 27 | config.project = 'tugboat' 28 | config.future_release = "v#{version}" 29 | config.header = "# Change log\n\nAll notable changes to this project will be documented in this file." 30 | config.exclude_labels = %w{duplicate question invalid wontfix} 31 | end 32 | rescue LoadError 33 | end 34 | -------------------------------------------------------------------------------- /bin/tugboat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # https://github.com/mitchellh/vagrant/blob/8cc4910fa9ca6059697459d0cdee1557af8d0507/bin/vagrant#L3-L6 4 | # Catch any ctrl+c's to avoid stack traces. Thanks Mitchell. ^^ 5 | 6 | Signal.trap('INT') { exit 1 } 7 | 8 | require 'tugboat' 9 | 10 | Tugboat::CLI.start(ARGV) 11 | -------------------------------------------------------------------------------- /config/license_finder.yml: -------------------------------------------------------------------------------- 1 | --- 2 | decisions_file: './license/dependency_decisions.yml' 3 | -------------------------------------------------------------------------------- /features/cassettes/config/Array_of_SSH_Keys_in_Config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.digitalocean.com/v2/droplets 6 | body: 7 | encoding: UTF-8 8 | string: '{"name":"droplet-with-array-of-keys","size":"512mb","image":"ubuntu-14-04-x64","region":"nyc2","ssh_keys":["1234","5678"],"private_networking":"false","backups_enabled":"false","ipv6":"false","user_data":null}' 9 | headers: 10 | Authorization: 11 | - Bearer f8sazukxeh729ggxh9gjavvzw5cabdpq95txpzhz6ep6jvtquxztfkf2chyejcsg5 12 | Content-Type: 13 | - application/json 14 | User-Agent: 15 | - Faraday v0.9.2 16 | Accept-Encoding: 17 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 18 | Accept: 19 | - "*/*" 20 | response: 21 | status: 22 | code: 202 23 | message: Accepted 24 | headers: 25 | Server: 26 | - cloudflare-nginx 27 | Date: 28 | - Tue, 16 Feb 2016 10:10:51 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - keep-alive 35 | Set-Cookie: 36 | - __cfduid=hm6ha3pvo7hm6ha3pvo7hm6ha3pvo7; expires=Wed, 15-Feb-17 37 | 10:10:50 GMT; path=/; domain=.digitalocean.com; HttpOnly 38 | Status: 39 | - 202 Accepted 40 | X-Frame-Options: 41 | - SAMEORIGIN 42 | X-Xss-Protection: 43 | - 1; mode=block 44 | X-Content-Type-Options: 45 | - nosniff 46 | Ratelimit-Limit: 47 | - '5000' 48 | Ratelimit-Remaining: 49 | - '4994' 50 | Ratelimit-Reset: 51 | - '1455617507' 52 | Cache-Control: 53 | - no-cache 54 | X-Request-Id: 55 | - 8d869cf8-bbeb-48fa-ae84-32f1b01ebab6 56 | X-Runtime: 57 | - '0.246402' 58 | Cf-Ray: 59 | - 27587709cabc1377-LHR 60 | body: 61 | encoding: UTF-8 62 | string: '{"droplet":{"id":276961,"name":"droplet-with-array-of-keys","memory":512,"vcpus":1,"disk":20,"locked":true,"status":"new","kernel":{"id":6297,"name":"Ubuntu 63 | 14.04 x64 vmlinuz-3.13.0-71-generic","version":"3.13.0-71-generic"},"created_at":"2016-02-16T10:10:50Z","features":["virtio"],"backup_ids":[],"next_backup_window":null,"snapshot_ids":[],"image":{"id":14782728,"name":"14.04.3 64 | x64","distribution":"Ubuntu","slug":"ubuntu-14-04-x64","public":true,"regions":["nyc1","sfo1","nyc2","ams2","sgp1","lon1","nyc3","ams3","fra1","tor1"],"created_at":"2015-12-10T16:42:21Z","min_disk_size":20,"type":"snapshot"},"size":{"slug":"512mb","memory":512,"vcpus":1,"disk":20,"transfer":1.0,"price_monthly":5.0,"price_hourly":0.00744,"regions":["ams1","ams2","ams3","fra1","lon1","nyc1","nyc2","nyc3","sfo1","sgp1","tor1"],"available":true},"size_slug":"512mb","networks":{"v4":[],"v6":[]},"region":{"name":"New 65 | York 2","slug":"nyc2","sizes":["1gb","2gb","4gb","8gb","32gb","64gb","512mb","48gb","16gb"],"features":["private_networking","backups","ipv6","metadata"],"available":true},"tags":[]},"links":{"actions":[{"id":84162954,"rel":"create","href":"https://api.digitalocean.com/v2/actions/27624451"}]}}' 66 | http_version: 67 | recorded_at: Tue, 16 Feb 2016 10:10:51 GMT 68 | recorded_with: VCR 2.9.3 69 | -------------------------------------------------------------------------------- /features/cassettes/config/Single_SSH_key_as_number_in_config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.digitalocean.com/v2/droplets 6 | body: 7 | encoding: UTF-8 8 | string: '{"name":"droplet-with-array-of-keys","size":"512mb","image":"ubuntu-14-04-x64","region":"nyc2","ssh_keys":["1234","5678"],"private_networking":"false","backups_enabled":"false","ipv6":"false","user_data":null}' 9 | headers: 10 | Authorization: 11 | - Bearer faketokenazukxeh729ggxh9gjavvzw5cabdpq95txpzhz6ep6jvtquxztfkf2chyejcsg5 12 | Content-Type: 13 | - application/json 14 | User-Agent: 15 | - Faraday v0.9.2 16 | Accept-Encoding: 17 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 18 | Accept: 19 | - "*/*" 20 | response: 21 | status: 22 | code: 202 23 | message: Accepted 24 | headers: 25 | Server: 26 | - cloudflare-nginx 27 | Date: 28 | - Tue, 16 Feb 2016 10:10:51 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - keep-alive 35 | Set-Cookie: 36 | - __cfduid=ajsdfjk2hbnfjb3jbj; expires=Wed, 15-Feb-17 37 | 10:10:50 GMT; path=/; domain=.digitalocean.com; HttpOnly 38 | Status: 39 | - 202 Accepted 40 | X-Frame-Options: 41 | - SAMEORIGIN 42 | X-Xss-Protection: 43 | - 1; mode=block 44 | X-Content-Type-Options: 45 | - nosniff 46 | Ratelimit-Limit: 47 | - '5000' 48 | Ratelimit-Remaining: 49 | - '4994' 50 | Ratelimit-Reset: 51 | - '1455617507' 52 | Cache-Control: 53 | - no-cache 54 | X-Request-Id: 55 | - 8d869cf8-bbeb-48fa-ae84-32f1b01ebab6 56 | X-Runtime: 57 | - '0.246402' 58 | Cf-Ray: 59 | - 27587709cabc1377-LHR 60 | body: 61 | encoding: UTF-8 62 | string: '{"droplet":{"id":276961,"name":"droplet-with-array-of-keys","memory":512,"vcpus":1,"disk":20,"locked":true,"status":"new","kernel":{"id":6297,"name":"Ubuntu 63 | 14.04 x64 vmlinuz-3.13.0-71-generic","version":"3.13.0-71-generic"},"created_at":"2016-02-16T10:10:50Z","features":["virtio"],"backup_ids":[],"next_backup_window":null,"snapshot_ids":[],"image":{"id":14782728,"name":"14.04.3 64 | x64","distribution":"Ubuntu","slug":"ubuntu-14-04-x64","public":true,"regions":["nyc1","sfo1","nyc2","ams2","sgp1","lon1","nyc3","ams3","fra1","tor1"],"created_at":"2015-12-10T16:42:21Z","min_disk_size":20,"type":"snapshot"},"size":{"slug":"512mb","memory":512,"vcpus":1,"disk":20,"transfer":1.0,"price_monthly":5.0,"price_hourly":0.00744,"regions":["ams1","ams2","ams3","fra1","lon1","nyc1","nyc2","nyc3","sfo1","sgp1","tor1"],"available":true},"size_slug":"512mb","networks":{"v4":[],"v6":[]},"region":{"name":"New 65 | York 2","slug":"nyc2","sizes":["1gb","2gb","4gb","8gb","32gb","64gb","512mb","48gb","16gb"],"features":["private_networking","backups","ipv6","metadata"],"available":true},"tags":[]},"links":{"actions":[{"id":84162954,"rel":"create","href":"https://api.digitalocean.com/v2/actions/27624451"}]}}' 66 | http_version: 67 | recorded_at: Tue, 16 Feb 2016 10:10:51 GMT 68 | recorded_with: VCR 2.9.3 69 | -------------------------------------------------------------------------------- /features/step_definitions/steps.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | Given(%r{^a '\.tugboat' config with data:$}) do |data_str| 4 | data = ERB.new(data_str).result(binding) 5 | File.write("#{Dir.pwd}/tmp/aruba/.tugboat", data) 6 | end 7 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'aruba/cucumber' 2 | require 'aruba/in_process' 3 | require 'aruba/spawn_process' 4 | require 'vcr' 5 | require 'webmock' 6 | require 'tugboat' 7 | 8 | VCR.configure do |c| 9 | c.hook_into :webmock 10 | c.cassette_library_dir = 'features/cassettes' 11 | c.default_cassette_options = { record: :new_episodes } 12 | end 13 | 14 | VCR.cucumber_tags do |t| 15 | t.tag '@vcr', use_scenario_name: true 16 | end 17 | 18 | class VcrFriendlyMain 19 | def initialize(argv, stdin, stdout, stderr, kernel) 20 | @argv = argv 21 | @stdin = stdin 22 | @stdout = stdout 23 | @stderr = stderr 24 | @kernel = kernel 25 | end 26 | 27 | def execute! 28 | $stdin = @stdin 29 | $stdout = @stdout 30 | Tugboat::CLI.start(@argv) 31 | end 32 | end 33 | 34 | Before('@vcr') do 35 | Aruba::InProcess.main_class = VcrFriendlyMain 36 | Aruba.process = Aruba::InProcess 37 | end 38 | 39 | After('@vcr') do 40 | Aruba.process = Aruba::SpawnProcess 41 | $stdin = STDIN 42 | $stdout = STDOUT 43 | VCR.eject_cassette 44 | end 45 | -------------------------------------------------------------------------------- /features/tugboat/config_array_of_ssh_keys.feature: -------------------------------------------------------------------------------- 1 | Feature: config 2 | In order to have an easier time connecting to droplets 3 | As a user 4 | I should be able to supply an array of ssh keys for a machine 5 | 6 | @vcr 7 | Scenario: Array of SSH Keys in Config 8 | Given a '.tugboat' config with data: 9 | """ 10 | --- 11 | authentication: 12 | access_token: f8sazukxeh729ggxh9gjavvzw5cabdpq95txpzhz6ep6jvtquxztfkf2chyejcsg5 13 | ssh: 14 | ssh_user: bobby_sousa 15 | ssh_key_path: "~/.ssh/id_rsa" 16 | ssh_port: '22' 17 | defaults: 18 | region: nyc2 19 | image: ubuntu-14-04-x64 20 | size: 512mb 21 | ssh_key: ['1234','5678'] 22 | private_networking: 'false' 23 | backups_enabled: 'false' 24 | ip6: 'false' 25 | """ 26 | When I run `tugboat create droplet-with-array-of-keys` 27 | Then the exit status should not be 1 28 | And the output should contain "Queueing creation of droplet 'droplet-with-array-of-keys'...Droplet created!" 29 | -------------------------------------------------------------------------------- /features/tugboat/config_current_directory.feature: -------------------------------------------------------------------------------- 1 | Feature: config 2 | In order to easily load DigitalOcean config 3 | As a user 4 | I should be able to load tugboat config from a .tugboat in the current directory 5 | 6 | Scenario: Read config from current directory 7 | Given a '.tugboat' config with data: 8 | """ 9 | --- 10 | authentication: 11 | access_token: FOO 12 | ssh: 13 | ssh_user: janedoe 14 | ssh_key_path: "/Users/janedoe/.ssh/id_rsa" 15 | ssh_port: '22' 16 | defaults: 17 | region: nyc2 18 | image: ubuntu-14-04-x64 19 | size: 512mb 20 | ssh_key: ['1234','5678'] 21 | private_networking: 'false' 22 | backups_enabled: 'false' 23 | ip6: 'false' 24 | """ 25 | When I run `tugboat config` 26 | Then the exit status should not be 1 27 | And the output should contain "ssh_user: janedoe" 28 | And the output should contain "'1234'" 29 | And the output should contain "'5678'" 30 | -------------------------------------------------------------------------------- /features/tugboat/config_number_key.feature: -------------------------------------------------------------------------------- 1 | Feature: config 2 | In order to easily load DigitalOcean config 3 | As a user 4 | I should be able to supply an ssh key as a number 5 | 6 | @vcr 7 | Scenario: Single SSH key as number in config 8 | Given a '.tugboat' config with data: 9 | """ 10 | --- 11 | authentication: 12 | access_token: faketokenazukxeh729ggxh9gjavvzw5cabdpq95txpzhz6ep6jvtquxztfkf2chyejcsg5 13 | ssh: 14 | ssh_user: root 15 | ssh_key_path: "~/.ssh/id_rsa" 16 | ssh_port: '22' 17 | defaults: 18 | region: nyc2 19 | image: ubuntu-14-04-x64 20 | size: 512mb 21 | ssh_key: 27100 22 | private_networking: 'false' 23 | backups_enabled: 'false' 24 | ip6: 'false' 25 | """ 26 | When I run `tugboat create number-based-key` 27 | Then the exit status should not be 1 28 | And the output should contain "Queueing creation of droplet 'number-based-key'...Droplet created!" 29 | -------------------------------------------------------------------------------- /lib/tugboat.rb: -------------------------------------------------------------------------------- 1 | require 'tugboat/cli' 2 | require 'tugboat/config' 3 | require 'tugboat/version' 4 | require 'json' 5 | require 'hashie' 6 | require 'hashie/logger' 7 | 8 | Hashie.logger = Logger.new(nil) 9 | 10 | # TODO: do this properly 11 | # See: https://github.com/intridea/hashie/issues/394 12 | require 'hashie' 13 | require 'hashie/logger' 14 | Hashie.logger = Logger.new(nil) 15 | 16 | module Tugboat 17 | end 18 | -------------------------------------------------------------------------------- /lib/tugboat/config.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module Tugboat 4 | # This is the configuration object. It reads in configuration 5 | # from a .tugboat file located in the user's home directory 6 | 7 | class Configuration 8 | include Singleton 9 | attr_reader :data 10 | attr_reader :path 11 | 12 | FILE_NAME = '.tugboat'.freeze 13 | DEFAULT_SSH_KEY_PATH = '.ssh/id_rsa'.freeze 14 | DEFAULT_SSH_PORT = '22'.freeze 15 | DEFAULT_REGION = 'nyc2'.freeze 16 | DEFAULT_IMAGE = 'ubuntu-14-04-x64'.freeze 17 | DEFAULT_SIZE = '512mb'.freeze 18 | DEFAULT_SSH_KEY = ''.freeze 19 | DEFAULT_IP6 = 'false'.freeze 20 | DEFAULT_PRIVATE_NETWORKING = 'false'.freeze 21 | DEFAULT_BACKUPS_ENABLED = 'false'.freeze 22 | DEFAULT_USER_DATA = nil 23 | DEFAULT_TIMEOUT = 10 24 | 25 | # Load config file from current directory, if not exit load from user's home directory 26 | def initialize 27 | @path = File.join(File.expand_path('.'), FILE_NAME) 28 | unless File.exist?(@path) 29 | @path = (ENV['TUGBOAT_CONFIG_PATH'] || File.join(File.expand_path('~'), FILE_NAME)) 30 | end 31 | @data = load_config_file 32 | end 33 | 34 | # If we can't load the config file, self.data is nil, which we can 35 | # check for in CheckConfiguration 36 | def load_config_file 37 | require 'yaml' 38 | YAML.load_file(@path) 39 | rescue Errno::ENOENT 40 | return 41 | end 42 | 43 | def access_token 44 | env_access_token || @data['authentication']['access_token'] 45 | end 46 | 47 | def ssh_key_path 48 | @data['ssh']['ssh_key_path'] 49 | end 50 | 51 | def ssh_user 52 | @data['ssh']['ssh_user'] 53 | end 54 | 55 | def ssh_port 56 | @data['ssh']['ssh_port'] 57 | end 58 | 59 | def use_public_ip 60 | @data['use_public_ip'] 61 | end 62 | 63 | def default_region 64 | @data['defaults'].nil? ? DEFAULT_REGION : @data['defaults']['region'] 65 | end 66 | 67 | def default_image 68 | @data['defaults'].nil? ? DEFAULT_IMAGE : @data['defaults']['image'] 69 | end 70 | 71 | def default_size 72 | @data['defaults'].nil? ? DEFAULT_SIZE : @data['defaults']['size'] 73 | end 74 | 75 | def default_ssh_key 76 | @data['defaults'].nil? ? DEFAULT_SSH_KEY : @data['defaults']['ssh_key'] 77 | end 78 | 79 | def default_ip6 80 | @data['defaults'].nil? ? DEFAULT_IP6 : @data['defaults']['ip6'] 81 | end 82 | 83 | def default_user_data 84 | @data['defaults'].nil? ? DEFAULT_USER_DATA : @data['defaults']['user_data'] 85 | end 86 | 87 | def default_private_networking 88 | @data['defaults'].nil? ? DEFAULT_PRIVATE_NETWORKING : @data['defaults']['private_networking'] 89 | end 90 | 91 | def default_backups_enabled 92 | @data['defaults'].nil? ? DEFAULT_BACKUPS_ENABLED : @data['defaults']['backups_enabled'] 93 | end 94 | 95 | def env_access_token 96 | ENV['DO_API_TOKEN'] unless ENV['DO_API_TOKEN'].to_s.empty? 97 | end 98 | 99 | def timeout 100 | @data['connection'].nil? || @data['connection']['timeout'].nil? ? DEFAULT_TIMEOUT : @data['connection']['timeout'] 101 | end 102 | 103 | # Re-runs initialize 104 | def reset! 105 | send(:initialize) 106 | end 107 | 108 | # Re-loads the config 109 | def reload! 110 | @data = load_config_file 111 | end 112 | 113 | # Writes a config file 114 | def create_config_file(access_token, ssh_key_path, ssh_user, ssh_port, region, image, size, ssh_key, private_networking, backups_enabled, ip6, timeout) 115 | # Default SSH Key path 116 | ssh_key_path = File.join('~', DEFAULT_SSH_KEY_PATH) if ssh_key_path.empty? 117 | 118 | ssh_user = 'root' if ssh_user.empty? 119 | 120 | ssh_port = DEFAULT_SSH_PORT if ssh_port.empty? 121 | 122 | region = DEFAULT_REGION if region.empty? 123 | 124 | image = DEFAULT_IMAGE if image.empty? 125 | 126 | size = DEFAULT_SIZE if size.empty? 127 | 128 | default_ssh_key = DEFAULT_SSH_KEY if ssh_key.empty? 129 | 130 | if private_networking.empty? 131 | private_networking = DEFAULT_PRIVATE_NETWORKING 132 | end 133 | 134 | backups_enabled = DEFAULT_BACKUPS_ENABLED if backups_enabled.empty? 135 | 136 | ip6 = DEFAULT_IP6 if ip6.empty? 137 | 138 | timeout = DEFAULT_TIMEOUT if timeout.empty? 139 | 140 | require 'yaml' 141 | File.open(@path, File::RDWR | File::TRUNC | File::CREAT, 0o600) do |file| 142 | data = { 143 | 'authentication' => { 144 | 'access_token' => access_token 145 | }, 146 | 'connection' => { 147 | 'timeout' => timeout 148 | }, 149 | 'ssh' => { 150 | 'ssh_user' => ssh_user, 151 | 'ssh_key_path' => ssh_key_path, 152 | 'ssh_port' => ssh_port 153 | }, 154 | 'defaults' => { 155 | 'region' => region, 156 | 'image' => image, 157 | 'size' => size, 158 | 'ssh_key' => ssh_key, 159 | 'private_networking' => private_networking, 160 | 'backups_enabled' => backups_enabled, 161 | 'ip6' => ip6 162 | } 163 | } 164 | file.write data.to_yaml 165 | end 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/add_key.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class AddKey < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | if env['add_key_pub_key'] 8 | pub_key_string = env['add_key_pub_key'] 9 | else 10 | if env['add_key_file_path'] 11 | pub_key_string = File.read(env['add_key_file_path']) 12 | else 13 | possible_keys = Dir.glob("#{ENV['HOME']}/.ssh/*.pub") 14 | 15 | # Only show hinted keys if the user has any 16 | unless possible_keys.empty? 17 | say "Possible public key paths from #{ENV['HOME']}/.ssh:" 18 | say 19 | possible_keys.each do |key_file| 20 | say key_file.to_s 21 | end 22 | say 23 | end 24 | 25 | ssh_key_file = ask 'Enter the path to your SSH key:' 26 | pub_key_string = File.read(ssh_key_file.to_s) 27 | end 28 | end 29 | 30 | say "Queueing upload of SSH key '#{env['add_key_name']}'...", nil, false 31 | 32 | response = ocean.key.create name: env['add_key_name'], 33 | public_key: pub_key_string 34 | 35 | unless response.success? 36 | say "Failed to create key: #{response.message}", :red 37 | exit 1 38 | end 39 | 40 | say 'SSH Key uploaded', :green 41 | say 42 | say "Name: #{response.ssh_key.name}" 43 | say "ID: #{response.ssh_key.id}" 44 | 45 | @app.call(env) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/ask_for_credentials.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # Ask for user credentials from the command line, then write them out. 4 | class AskForCredentials < Base 5 | def call(env) 6 | say 'Note: You can get your Access Token from https://cloud.digitalocean.com/settings/tokens/new', :yellow 7 | say 8 | access_token = ask 'Enter your access token:' 9 | timeout = ask 'Enter your default timeout for connections in seconds (optional, defaults to 10):' 10 | access_token.strip! 11 | ssh_key_path = ask 'Enter your SSH key path (optional, defaults to ~/.ssh/id_rsa):' 12 | ssh_user = ask 'Enter your SSH user (optional, defaults to root):' 13 | ssh_port = ask 'Enter your SSH port number (optional, defaults to 22):' 14 | say 15 | say "To retrieve region, image, size and key ID's, you can use the corresponding tugboat command, such as `tugboat images`." 16 | say 'Defaults can be changed at any time in your ~/.tugboat configuration file.' 17 | say 18 | region = ask 'Enter your default region (optional, defaults to nyc1):' 19 | image = ask 'Enter your default image ID or image slug (optional, defaults to ubuntu-14-04-x64):' 20 | size = ask 'Enter your default size (optional, defaults to 512mb)):' 21 | ssh_key = ask "Enter your default ssh key IDs (optional, defaults to none, array of IDs of ssh keys eg. ['1234']):" 22 | private_networking = ask 'Enter your default for private networking (optional, defaults to false):' 23 | backups_enabled = ask 'Enter your default for enabling backups (optional, defaults to false):' 24 | ip6 = ask 'Enter your default for IPv6 (optional, defaults to false):' 25 | 26 | # Write the config file. 27 | env['config'].create_config_file(access_token, ssh_key_path, ssh_user, ssh_port, region, image, size, ssh_key, private_networking, backups_enabled, ip6, timeout) 28 | env['config'].reload! 29 | 30 | @app.call(env) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/backup_setting.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class BackupConfig < Base 4 | def call(env) 5 | ocean = env['droplet_kit'] 6 | 7 | if env['disable'] && env['enable'] 8 | say 'You cannot use both --disable and --enable for backup_config', :red 9 | exit 1 10 | end 11 | 12 | begin 13 | if env['disable'] 14 | response = ocean.droplet_actions.disable_backups(droplet_id: env['droplet_id']) 15 | end 16 | if env['enable'] 17 | response = ocean.droplet_actions.enable_backups(droplet_id: env['droplet_id']) 18 | end 19 | rescue DropletKit::Error => e 20 | say "Failed to configure backups on droplet. Reason given from API: #{e}", :red 21 | exit 1 22 | end 23 | 24 | say "Backup action #{response_stringify(response)} is #{response.status}" 25 | 26 | @app.call(env) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/base.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # A base middleware class to initalize. 4 | class Base 5 | # Some colors for making things pretty. 6 | CLEAR = "\e[0m".freeze 7 | RED = "\e[31m".freeze 8 | GREEN = "\e[32m".freeze 9 | YELLOW = "\e[33m".freeze 10 | 11 | # We want access to all of the fun thor cli helper methods, 12 | # like say, yes?, ask, etc. 13 | include Thor::Shell 14 | 15 | def initialize(app) 16 | @app = app 17 | # This resets the color to "clear" on the user's terminal. 18 | say '', :clear, false 19 | end 20 | 21 | def check_response_success(task_string, response) 22 | unless response.success? 23 | say "Failed to #{task_string}: #{response.message}", :red 24 | exit 1 25 | end 26 | end 27 | 28 | def response_stringify(response) 29 | response.type.gsub(/_/,' ') 30 | end 31 | 32 | def call(env) 33 | @app.call(env) 34 | end 35 | 36 | def verify_credentials(ocean, say_success = false) 37 | begin 38 | if ocean.is_a?(DropletKit::Client) 39 | response = ocean.droplets.all(per_page: '1', page: '1') 40 | 41 | begin 42 | response.first 43 | rescue DropletKit::Error => e 44 | say "Failed to connect to DigitalOcean. Reason given from API:\n#{e}", :red 45 | exit 1 46 | end 47 | else 48 | response = ocean.droplet.all(per_page: '1', page: '1') 49 | 50 | unless response.success? 51 | say "Failed to connect to DigitalOcean. Reason given from API: #{response.id} - #{response.message}", :red 52 | exit 1 53 | end 54 | end 55 | rescue Faraday::ClientError => e 56 | say 'Authentication with DigitalOcean failed at an early stage' 57 | say "Error was: #{e}" 58 | exit 1 59 | end 60 | 61 | say 'Authentication with DigitalOcean was successful.', :green if say_success 62 | end 63 | 64 | def wait_for_state(droplet_id, desired_state, ocean) 65 | start_time = Time.now 66 | 67 | response = ocean.droplet.show droplet_id 68 | 69 | say '.', nil, false 70 | 71 | unless response.success? 72 | say "Failed to get status of Droplet: #{response.message}", :red 73 | exit 1 74 | end 75 | 76 | while response.droplet.status != desired_state 77 | sleep 2 78 | response = ocean.droplet.show droplet_id 79 | say '.', nil, false 80 | end 81 | 82 | total_time = (Time.now - start_time).to_i 83 | 84 | say "done#{CLEAR} (#{total_time}s)", :green 85 | end 86 | 87 | def restart_droplet(hard_restart, ocean, droplet_id = '', droplet_name = '') 88 | if hard_restart 89 | say "Queuing hard restart for #{droplet_id} #{droplet_name}...", nil, false 90 | ocean.droplet.power_cycle droplet_id 91 | else 92 | say "Queuing restart for #{droplet_id} #{droplet_name}...", nil, false 93 | ocean.droplet.reboot droplet_id 94 | end 95 | end 96 | 97 | def print_droplet_info(droplet, attribute, porcelain, include_urls, include_name) 98 | droplet_ip4_public = droplet.networks.v4.find { |address| address.type == 'public' }.ip_address unless droplet.networks.v4.empty? 99 | droplet_ip6_public = droplet.networks.v6.find { |address| address.type == 'public' }.ip_address unless droplet.networks.v6.empty? 100 | check_private_ip = droplet.networks.v4.find { |address| address.type == 'private' } 101 | droplet_private_ip = check_private_ip.ip_address if check_private_ip 102 | 103 | attributes_list = [ 104 | ['name', droplet.name], 105 | ['id', droplet.id], 106 | ['status', droplet.status], 107 | ['ip4', droplet_ip4_public], 108 | ['ip6', droplet_ip6_public], 109 | ['private_ip', droplet_private_ip], 110 | ['region', droplet.region.slug], 111 | ['image', droplet.image.id], 112 | ['size', droplet.size_slug], 113 | ['backups_active', !droplet.backup_ids.empty?] 114 | ] 115 | attributes = Hash[*attributes_list.flatten(1)] 116 | 117 | if attribute 118 | if attributes.key? attribute 119 | if include_name 120 | say "#{attributes['name']},#{attributes[attribute]}" 121 | else 122 | say attributes[attribute] 123 | end 124 | else 125 | say "Invalid attribute \"#{attribute}\"", :red 126 | say 'Provide one of the following:', :red 127 | attributes_list.each { |a| say " #{a[0]}", :red } 128 | exit 1 129 | end 130 | else 131 | if porcelain 132 | attributes_list.select { |a| !a[1].nil? }.each { |a| say "#{a[0]} #{a[1]}" } 133 | say "" 134 | else 135 | print_droplet_info_full(droplet, include_urls) 136 | end 137 | end 138 | end 139 | 140 | def print_droplet_info_full(droplet, include_urls) 141 | private_addr = droplet.networks.v4.find { |address| address.type == 'private' } 142 | if private_addr 143 | private_ip = ", private_ip: #{private_addr.ip_address}" 144 | end 145 | 146 | status_color = if droplet.status == 'active' 147 | GREEN 148 | else 149 | RED 150 | end 151 | 152 | public_addr = droplet.networks.v4.find { |address| address.type == 'public' } 153 | 154 | say "#{droplet.name} (ip: #{public_addr.ip_address}#{private_ip}, status: #{status_color}#{droplet.status}#{CLEAR}, region: #{droplet.region.slug}, size: #{droplet.size_slug}, id: #{droplet.id}#{include_urls ? droplet_id_to_url(droplet.id) : ''})" 155 | end 156 | 157 | # Get all pages of droplets 158 | def get_droplet_list(ocean, per_page = 20) 159 | verify_credentials(ocean) 160 | 161 | # Allow both Barge and DropletKit usage 162 | if ocean.is_a?(DropletKit::Client) 163 | # DropletKit self-paginates 164 | ocean.droplets.all(per_page: per_page) 165 | else 166 | page = ocean.droplet.all(per_page: 200, page: 1) 167 | return page.droplets unless page.paginated? 168 | 169 | Enumerator.new do |enum| 170 | page.droplets.each { |drop| enum.yield drop } 171 | for page_num in 2..page.last_page 172 | page = ocean.droplet.all(per_page: 200, page: page_num) 173 | page.droplets.each { |drop| enum.yield drop } 174 | end 175 | end 176 | end 177 | 178 | end 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/check_configuration.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # Check if the client has set-up configuration yet. 4 | class CheckConfiguration < Base 5 | def call(env) 6 | config = env['config'] 7 | 8 | if !config || !config.data || !config.access_token 9 | say 'You must run `tugboat authorize` in order to connect to DigitalOcean', :red 10 | exit 1 11 | end 12 | 13 | # If the user passes the global `-q/--quiet` flag, redirect 14 | # stdout 15 | $stdout = File.new('/dev/null', 'w') if env['user_quiet'] 16 | 17 | @app.call(env) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/check_credentials.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | 3 | module Tugboat 4 | module Middleware 5 | # Check if the client can connect to the ocean 6 | class CheckCredentials < Base 7 | def call(env) 8 | # We use a harmless API call to check if the authentication will 9 | # work. 10 | verify_credentials(env['barge'], true) 11 | 12 | @app.call(env) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/check_droplet_active.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # Check if the droplet in the environment is active 4 | class CheckDropletActive < Base 5 | def call(env) 6 | unless env['user_droplet_ssh_wait'] 7 | if env['droplet_status'] != 'active' 8 | say 'Droplet must be on for this operation to be successful.', :red 9 | exit 1 10 | end 11 | end 12 | 13 | @app.call(env) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/check_droplet_inactive.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # Check if the droplet in the environment is inactive, or "off" 4 | class CheckDropletInactive < Base 5 | def call(env) 6 | if env['droplet_status'] != 'off' 7 | say 'Droplet must be off for this operation to be successful.', :red 8 | exit 1 9 | end 10 | 11 | @app.call(env) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/check_snapshot_parameters.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # Check if the droplet in the environment is active 4 | class CheckSnapshotParameters < Base 5 | def call(env) 6 | unless env['user_droplet_id'] || env['user_droplet_name'] || env['user_droplet_fuzzy_name'] 7 | say 'You must provide a snapshot name followed by the droplet\'s name.', :red 8 | say "For example: `tugboat snapshot #{env['user_snapshot_name']} example-node.com`", :green 9 | exit 1 10 | end 11 | 12 | @app.call(env) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/config.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # Check if the droplet in the environment is inactive, or "off" 4 | class Config < Base 5 | def call(env) 6 | config = Tugboat::Configuration.instance 7 | 8 | keys_retracted = '' 9 | 10 | config_data = config.data.to_yaml.delete('"') 11 | 12 | if env['user_hide_keys'] 13 | keys_retracted = '(Keys Redacted)' 14 | config_data = config_data.gsub(%r{(access_token: )([a-zA-Z0-9]+)}, '\1 [REDACTED]') 15 | end 16 | 17 | say "Current Config #{keys_retracted}\n", :green 18 | 19 | say "Path: #{config.path}" 20 | say config_data 21 | 22 | @app.call(env) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/confirm_action.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class ConfirmAction < Base 4 | def call(env) 5 | unless env['user_confirm_action'] 6 | response = yes? 'Warning! Potentially destructive action. Please confirm [y/n]:' 7 | 8 | unless response 9 | say 'Aborted due to user request.', :red 10 | # Quit 11 | exit 1 12 | end 13 | 14 | end 15 | 16 | @app.call(env) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/create_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class CreateDroplet < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | say "Queueing creation of droplet '#{env['create_droplet_name']}'...", nil, false 8 | 9 | droplet_region_slug = env['create_droplet_region_slug'] ? env['create_droplet_region_slug'] : env['config'].default_region 10 | 11 | droplet_image_slug = env['create_droplet_image_slug'] ? env['create_droplet_image_slug'] : env['config'].default_image 12 | 13 | droplet_size_slug = env['create_droplet_size_slug'] ? env['create_droplet_size_slug'] : env['config'].default_size 14 | 15 | droplet_ssh_key_ids = env['create_droplet_ssh_key_ids'] ? env['create_droplet_ssh_key_ids'] : env['config'].default_ssh_key 16 | 17 | droplet_private_networking = env['create_droplet_private_networking'] ? env['create_droplet_private_networking'] : env['config'].default_private_networking 18 | 19 | droplet_ip6 = env['create_droplet_ip6'] ? env['create_droplet_ip6'] : env['config'].default_ip6 20 | 21 | droplet_user_data = env['create_droplet_user_data'] ? env['create_droplet_user_data'] : env['config'].default_user_data 22 | 23 | if droplet_user_data 24 | if File.file?(droplet_user_data) 25 | user_data_string = File.open(droplet_user_data, 'rb', &:read) 26 | else 27 | say "Could not find file: #{droplet_user_data}, check your user_data setting" 28 | exit 1 29 | end 30 | end 31 | 32 | droplet_backups_enabled = env['create_droplet_backups_enabled'] ? env['create_droplet_backups_enabled'] : env['config'].default_backups_enabled 33 | 34 | droplet_key_array = if droplet_ssh_key_ids.is_a?(Array) 35 | droplet_ssh_key_ids 36 | else 37 | droplet_ssh_key_ids.to_s.split(',') 38 | end 39 | 40 | create_opts = { 41 | name: env['create_droplet_name'], 42 | size: droplet_size_slug, 43 | image: droplet_image_slug.to_s, 44 | region: droplet_region_slug, 45 | ssh_keys: droplet_key_array, 46 | private_networking: droplet_private_networking, 47 | backups_enabled: droplet_backups_enabled, 48 | ipv6: droplet_ip6, 49 | user_data: user_data_string 50 | } 51 | 52 | response = ocean.droplet.create(create_opts) 53 | 54 | unless response.success? 55 | say "Failed to create Droplet: #{response.message}", :red 56 | exit 1 57 | end 58 | 59 | say "Droplet created! Droplet ID is #{response.droplet[:id]}" 60 | 61 | @app.call(env) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/custom_logger.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | 3 | module Tugboat 4 | class CustomLogger < ::Faraday::Middleware 5 | extend Forwardable 6 | def_delegators :@logger, :debug, :info, :warn, :error, :fatal 7 | 8 | def initialize(app, options = {}) 9 | @app = app 10 | @logger = options.fetch(:logger) do 11 | require 'logger' 12 | ::Logger.new($stdout) 13 | end 14 | end 15 | 16 | def call(env) 17 | start_time = Time.now 18 | info { request_info(env) } 19 | debug { request_debug(env) } 20 | @app.call(env).on_complete do 21 | end_time = Time.now 22 | response_time = end_time - start_time 23 | info { response_info(env, response_time) } 24 | debug { response_debug(env) } 25 | end 26 | end 27 | 28 | private 29 | 30 | def filter(output) 31 | if ENV['DEBUG'].to_i == 2 32 | output = output.to_s.gsub(%r{Bearer [a-zA-Z0-9]*}, 'Bearer [TOKEN REDACTED]') 33 | output = output.to_s.gsub(%r{_digitalocean2_session_v2=[a-zA-Z0-9%-]*}, '_digitalocean2_session_v2=[SESSION_COOKIE]') 34 | else 35 | output 36 | end 37 | end 38 | 39 | def request_info(env) 40 | format('Started %s request to: %s', env[:method].to_s.upcase, filter(env[:url])) 41 | end 42 | 43 | def response_info(env, response_time) 44 | format('Response from %s; Status: %d; Time: %.1fms', filter(env[:url]), env[:status], (response_time * 1_000.0)) 45 | end 46 | 47 | def request_debug(env) 48 | debug_message('Request', env[:request_headers], env[:body]) 49 | end 50 | 51 | def response_debug(env) 52 | debug_message('Response', env[:response_headers], env[:body]) 53 | end 54 | 55 | def debug_message(name, headers, body) 56 | main_message = <<-MESSAGE.gsub(%r{^ +([^ ])}m, '\\1') 57 | #{name} Headers: 58 | ---------------- 59 | #{format_headers(headers)} 60 | 61 | #{name} Body: 62 | ------------- 63 | MESSAGE 64 | main_message + pretty_body(body) 65 | end 66 | 67 | def pretty_body(body) 68 | body_json = JSON.parse(body) 69 | JSON.pretty_generate(body_json) 70 | rescue JSON::ParserError 71 | body 72 | end 73 | 74 | def format_headers(headers) 75 | length = headers.map { |k, _v| k.to_s.size }.max 76 | headers.map { |name, value| "#{name.to_s.ljust(length)} : #{filter(value)}" }.join("\n") 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/destroy_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class DestroyDroplet < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | say "Queuing destroy for #{env['droplet_id']} #{env['droplet_name']}...", nil, false 8 | 9 | response = ocean.droplet.destroy env['droplet_id'] 10 | 11 | if response.success? 12 | say 'Deletion Successful!', :green 13 | else 14 | say "Failed to destroy Droplet: #{response.message}", :red 15 | exit 1 16 | end 17 | 18 | @app.call(env) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/destroy_image.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class DestroyImage < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | say "Queuing destroy image for #{env['image_id']} #{env['image_name']}...", nil, false 8 | 9 | response = ocean.image.destroy env['image_id'] 10 | 11 | if response.success? 12 | say 'Image deletion successful!', :green 13 | else 14 | say "Failed to destroy image: #{response.message}", :red 15 | exit 1 16 | end 17 | 18 | @app.call(env) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/find_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # Check if the client has set-up configuration yet. 4 | class FindDroplet < Base 5 | def get_public_ip(networks) 6 | get_ip_per_network_type networks, 'public' 7 | end 8 | 9 | def get_private_ip(networks) 10 | get_ip_per_network_type networks, 'private' 11 | end 12 | 13 | def get_ip_per_network_type(networks, type) 14 | found_network = networks.find { |n| n.type == type } 15 | found_network.ip_address if found_network 16 | end 17 | 18 | def call(env) 19 | ocean = env['barge'] 20 | user_fuzzy_name = env['user_droplet_fuzzy_name'] 21 | user_droplet_name = env['user_droplet_name'] 22 | user_droplet_id = env['user_droplet_id'] 23 | porcelain = env['user_porcelain'] 24 | 25 | # First, if nothing is provided to us, we should quit and 26 | # let the user know. 27 | if !user_fuzzy_name && !user_droplet_name && !user_droplet_id 28 | 29 | say 'Tugboat attempted to find a droplet with no arguments.', :red 30 | say "Try running `tugboat #{env['tugboat_action']} dropletname`", :green 31 | say "For more help run: `tugboat help #{env['tugboat_action']}`", :blue 32 | exit 1 33 | end 34 | 35 | if porcelain && (!(user_droplet_name || user_droplet_id) || user_fuzzy_name) 36 | say 'Tugboat expects an exact droplet ID or droplet name for porcelain mode.', :red 37 | exit 1 38 | end 39 | 40 | # If you were to `tugboat restart foo -n foo-server-001` then we'd use 41 | # 'foo-server-001' without looking up the fuzzy name. 42 | # 43 | # This is why we check in this order. 44 | 45 | # Easy for us if they provide an id. Just set it to the droplet_id 46 | if user_droplet_id 47 | 48 | unless porcelain 49 | say 'Droplet id provided. Finding Droplet...', nil, false 50 | end 51 | response = ocean.droplet.show user_droplet_id 52 | 53 | unless response.success? 54 | say "Failed to find Droplet: #{response.message}", :red 55 | exit 1 56 | end 57 | 58 | env['droplet_id'] = response.droplet.id 59 | env['droplet_name'] = "(#{response.droplet.name})" 60 | env['droplet_ip'] = get_public_ip response.droplet.networks.v4 61 | env['droplet_ip_private'] = get_private_ip response.droplet.networks.v4 62 | env['droplet_status'] = response.droplet.status 63 | end 64 | 65 | # If they provide a name, we need to get the ID for it. 66 | # This requires a lookup. 67 | if user_droplet_name && !env['droplet_id'] 68 | unless porcelain 69 | say 'Droplet name provided. Finding droplet ID...', nil, false 70 | end 71 | 72 | # Look for the droplet by an exact name match. 73 | (get_droplet_list ocean).each do |d| 74 | next unless d.name == user_droplet_name 75 | env['droplet_id'] = d.id 76 | env['droplet_name'] = "(#{d.name})" 77 | env['droplet_ip'] = get_public_ip d.networks.v4 78 | env['droplet_ip_private'] = get_private_ip d.networks.v4 79 | env['droplet_status'] = d.status 80 | end 81 | 82 | # If we coulnd't find it, tell the user and drop out of the 83 | # sequence. 84 | unless env['droplet_id'] 85 | say "error\nUnable to find a droplet named '#{user_droplet_name}'.", :red 86 | exit 1 87 | end 88 | end 89 | 90 | # We only need to "fuzzy find" a droplet if a fuzzy name is provided, 91 | # and we don't want to fuzzy search if an id or name is provided 92 | # with a flag. 93 | # 94 | # This requires a lookup. 95 | if user_fuzzy_name && !env['droplet_id'] 96 | say 'Droplet fuzzy name provided. Finding droplet ID...', nil, false 97 | 98 | found_droplets = [] 99 | choices = [] 100 | 101 | (get_droplet_list ocean).each do |d| 102 | # Check to see if one of the droplet names have the fuzzy string. 103 | found_droplets << d if d.name.upcase.include? user_fuzzy_name.upcase 104 | end 105 | 106 | # Check to see if we have more then one droplet, and prompt 107 | # a user to choose otherwise. 108 | if found_droplets.length == 1 109 | droplet_return = found_droplets.first 110 | 111 | env['droplet_id'] = droplet_return.id 112 | env['droplet_name'] = "(#{droplet_return.name})" 113 | if droplet_return.networks.v4.empty? 114 | env['droplet_ip'] = '' # No Network Yet 115 | env['droplet_ip_private'] = '' # No Network Yet 116 | else 117 | env['droplet_ip'] = get_public_ip droplet_return.networks.v4 118 | env['droplet_ip_private'] = get_private_ip droplet_return.networks.v4 119 | end 120 | env['droplet_status'] = droplet_return.status 121 | elsif found_droplets.length > 1 122 | # Did we run the multiple questionairre? 123 | did_run_multiple = true 124 | 125 | say 'Multiple droplets found.' 126 | say 127 | found_droplets.each_with_index do |d, i| 128 | say "#{i}) #{d.name} (#{d.id})" 129 | choices << i.to_s 130 | end 131 | say 132 | choice = ask 'Please choose a droplet:', limited_to: choices 133 | env['droplet_id'] = found_droplets[choice.to_i].id 134 | env['droplet_name'] = found_droplets[choice.to_i].name 135 | env['droplet_ip'] = get_public_ip found_droplets[choice.to_i].networks.v4 136 | env['droplet_ip_private'] = get_private_ip found_droplets[choice.to_i].networks.v4 137 | env['droplet_status'] = found_droplets[choice.to_i].status 138 | end 139 | 140 | # If we coulnd't find it, tell the user and drop out of the 141 | # sequence. 142 | unless env['droplet_id'] 143 | say "error\nUnable to find a droplet named '#{user_fuzzy_name}'.", :red 144 | exit 1 145 | end 146 | end 147 | 148 | unless did_run_multiple 149 | unless porcelain 150 | say "done#{CLEAR}, #{env['droplet_id']} #{env['droplet_name']}", :green 151 | end 152 | end 153 | @app.call(env) 154 | end 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/find_image.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # Check if the client has set-up configuration yet. 4 | class FindImage < Base 5 | def call(env) 6 | ocean = env['barge'] 7 | user_fuzzy_name = env['user_image_fuzzy_name'] 8 | user_image_name = env['user_image_name'] 9 | user_image_id = env['user_image_id'] 10 | 11 | # First, if nothing is provided to us, we should quit and 12 | # let the user know. 13 | if !user_fuzzy_name && !user_image_name && !user_image_id 14 | say 'Tugboat attempted to find an image with no arguments.', :red 15 | say "Try running `tugboat #{env['tugboat_action']} imagename`", :green 16 | say "For more help run: `tugboat help #{env['tugboat_action']}`", :blue 17 | exit 1 18 | end 19 | 20 | # If you were to `tugboat restart foo -n foo-server-001` then we'd use 21 | # 'foo-server-001' without looking up the fuzzy name. 22 | # 23 | # This is why we check in this order. 24 | 25 | # Easy for us if they provide an id. Just set it to the image_id 26 | if user_image_id 27 | say 'Image id provided. Finding Image...', nil, false 28 | response = ocean.image.show user_image_id 29 | 30 | unless response.success? 31 | say "Failed to find Image: #{response.message}", :red 32 | exit 1 33 | end 34 | 35 | env['image_id'] = response.image.id 36 | env['image_name'] = "(#{response.image.name})" 37 | end 38 | 39 | # If they provide a name, we need to get the ID for it. 40 | # This requires a lookup. 41 | if user_image_name && !env['image_id'] 42 | say 'Image name provided. Finding Image...', nil, false 43 | 44 | # Look for the image by an exact name match. 45 | ocean.image.all['images'].each do |d| 46 | if d.name == user_image_name 47 | env['image_id'] = d.id 48 | env['image_name'] = "(#{d.name})" 49 | end 50 | end 51 | 52 | # If we coulnd't find it, tell the user and drop out of the 53 | # sequence. 54 | unless env['image_id'] 55 | say "error\nUnable to find an image named '#{user_image_name}'.", :red 56 | exit 1 57 | end 58 | end 59 | 60 | # We only need to "fuzzy find" a image if a fuzzy name is provided, 61 | # and we don't want to fuzzy search if an id or name is provided 62 | # with a flag. 63 | # 64 | # This requires a lookup. 65 | if user_fuzzy_name && !env['image_id'] 66 | say 'Image fuzzy name provided. Finding image ID...', nil, false 67 | 68 | found_images = [] 69 | choices = [] 70 | 71 | ocean.image.all['images'].each_with_index do |d, _i| 72 | # Check to see if one of the image names have the fuzzy string. 73 | found_images << d if d.name.upcase.include? user_fuzzy_name.upcase 74 | 75 | unless d.slug.nil? 76 | found_images << d if d.slug.upcase.include? user_fuzzy_name.upcase 77 | end 78 | end 79 | 80 | # Check to see if we have more then one image, and prompt 81 | # a user to choose otherwise. 82 | if found_images.length == 1 83 | env['image_id'] = found_images.first.id 84 | env['image_name'] = "(#{found_images.first.name})" 85 | elsif found_images.length > 1 86 | # Did we run the multiple questionairre? 87 | did_run_multiple = true 88 | 89 | say 'Multiple images found.' 90 | say 91 | found_images.each_with_index do |d, i| 92 | say "#{i}) #{d.name} (#{d.id})" 93 | choices << i.to_s 94 | end 95 | say 96 | choice = ask 'Please choose a image:', limited_to: choices 97 | env['image_id'] = found_images[choice.to_i].id 98 | env['image_name'] = found_images[choice.to_i].name 99 | end 100 | 101 | # If we coulnd't find it, tell the user and drop out of the 102 | # sequence. 103 | unless env['image_id'] 104 | say "error\nUnable to find an image named '#{user_fuzzy_name}'.", :red 105 | exit 1 106 | end 107 | end 108 | 109 | unless did_run_multiple 110 | say "done#{CLEAR}, #{env['image_id']} #{env['image_name']}", :green 111 | end 112 | @app.call(env) 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/halt_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class HaltDroplet < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | response = if env['user_droplet_hard'] 8 | say "Queuing hard shutdown for #{env['droplet_id']} #{env['droplet_name']}...", nil, false 9 | ocean.droplet.power_off env['droplet_id'] 10 | else 11 | say "Queuing shutdown for #{env['droplet_id']} #{env['droplet_name']}...", nil, false 12 | ocean.droplet.shutdown env['droplet_id'] 13 | end 14 | 15 | if response.success? 16 | say 'Halt successful!', :green 17 | else 18 | say "Failed to halt on Droplet: #{response.message}", :red 19 | exit 1 20 | end 21 | 22 | @app.call(env) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/info_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class InfoDroplet < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | response = ocean.droplet.show env['droplet_id'] 8 | 9 | check_response_success('get info for Droplet', response) 10 | 11 | droplet = response.droplet 12 | 13 | unless response.success? 14 | say "Failed to find droplet: #{response.message}", :red 15 | exit 1 16 | end 17 | 18 | status_color = if droplet.status == 'active' 19 | GREEN 20 | else 21 | RED 22 | end 23 | 24 | attribute = env['user_attribute'] 25 | 26 | droplet_ip4_public = droplet.networks.v4.find { |address| address.type == 'public' }.ip_address unless droplet.networks.v4.empty? 27 | droplet_ip6_public = droplet.networks.v6.find { |address| address.type == 'public' }.ip_address unless droplet.networks.v6.empty? 28 | check_private_ip = droplet.networks.v4.find { |address| address.type == 'private' } 29 | droplet_private_ip = check_private_ip.ip_address if check_private_ip 30 | 31 | attributes_list = [ 32 | ['name', droplet.name], 33 | ['id', droplet.id], 34 | ['status', droplet.status], 35 | ['ip4', droplet_ip4_public], 36 | ['ip6', droplet_ip6_public], 37 | ['private_ip', droplet_private_ip], 38 | ['region', droplet.region.slug], 39 | ['image', droplet.image.id], 40 | ['size', droplet.size_slug], 41 | ['backups_active', !droplet.backup_ids.empty?] 42 | ] 43 | attributes = Hash[*attributes_list.flatten(1)] 44 | 45 | if attribute 46 | if attributes.key? attribute 47 | say attributes[attribute] 48 | else 49 | say "Invalid attribute \"#{attribute}\"", :red 50 | say 'Provide one of the following:', :red 51 | attributes_list.each { |a| say " #{a[0]}", :red } 52 | exit 1 53 | end 54 | else 55 | if env['user_porcelain'] 56 | attributes_list.select { |a| !a[1].nil? }.each { |a| say "#{a[0]} #{a[1]}" } 57 | else 58 | say 59 | say "Name: #{droplet.name}" 60 | say "ID: #{droplet.id}" 61 | say "Status: #{status_color}#{droplet.status}#{CLEAR}" 62 | say "IP4: #{droplet_ip4_public}" unless droplet.networks.v4.empty? 63 | say "IP6: #{droplet_ip6_public}" unless droplet.networks.v6.empty? 64 | 65 | say "Private IP: #{droplet_private_ip}" if droplet_private_ip 66 | 67 | image_description = if droplet.image.slug.nil? 68 | droplet.image.name 69 | else 70 | droplet.image.slug 71 | end 72 | 73 | say "Region: #{droplet.region.name} - #{droplet.region.slug}" 74 | say "Image: #{droplet.image.id} - #{image_description}" 75 | say "Size: #{droplet.size_slug.upcase}" 76 | say "Backups Active: #{!droplet.backup_ids.empty?}" 77 | end 78 | end 79 | 80 | @app.call(env) 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/info_image.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class InfoImage < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | response = ocean.image.show env['image_id'] 8 | 9 | unless response.success? 10 | say "Failed to get info for Image: #{response.message}", :red 11 | exit 1 12 | end 13 | 14 | image = response.image 15 | 16 | say 17 | say "Name: #{image.name}" 18 | say "ID: #{image.id}" 19 | say "Distribution: #{image.distribution}" 20 | say "Min Disk Size: #{image.min_disk_size}GB" 21 | say "Regions: #{image.regions.join(',')}" 22 | 23 | @app.call(env) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/inject_client.rb: -------------------------------------------------------------------------------- 1 | require 'barge' 2 | require 'droplet_kit' 3 | require File.expand_path('../custom_logger', __FILE__) 4 | 5 | module Tugboat 6 | module Middleware 7 | # Inject the digital ocean client into the environment 8 | class InjectClient < Base 9 | def call(env) 10 | # Sets the digital ocean client into the environment for use 11 | # later. 12 | @access_token = env['config'].access_token 13 | config_timeout = env['config'].timeout 14 | 15 | env['barge'] = Barge::Client.new(access_token: @access_token, 16 | timeout: config_timeout, 17 | open_timeout: config_timeout) 18 | 19 | env['droplet_kit'] = DropletKit::Client.new(access_token: @access_token) 20 | 21 | env['droplet_kit'].connection.options.timeout = config_timeout.to_i 22 | env['droplet_kit'].connection.options.open_timeout = config_timeout.to_i 23 | 24 | env['barge'].faraday.use CustomLogger if ENV['DEBUG'] 25 | env['droplet_kit'].connection.use CustomLogger if ENV['DEBUG'] 26 | 27 | env['droplet_kit'].connection 28 | 29 | @app.call(env) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/inject_configuration.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # Check if the client has set-up configuration yet. 4 | class InjectConfiguration < Base 5 | def call(env) 6 | config = Tugboat::Configuration.instance 7 | 8 | env['config'] = config 9 | 10 | @app.call(env) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/list_droplets.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # Check if the client has set-up configuration yet. 4 | class ListDroplets < Base 5 | def call(env) 6 | ocean = env['droplet_kit'] 7 | 8 | verify_credentials(ocean) 9 | 10 | droplet_list = get_droplet_list(ocean, env['per_page']) 11 | 12 | has_one = false 13 | 14 | droplet_list.each do |droplet| 15 | has_one = true 16 | 17 | print_droplet_info(droplet, env['attribute'], env['porcelain'], env['include_urls'], env['include_name']) 18 | end 19 | 20 | unless has_one 21 | say "You don't appear to have any droplets.", :red 22 | say "Try creating one with #{GREEN}\`tugboat create\`#{CLEAR}" 23 | end 24 | 25 | @app.call(env) 26 | end 27 | 28 | private 29 | 30 | def droplet_id_to_url(id) 31 | ", url: 'https://cloud.digitalocean.com/droplets/#{id}'" 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/list_images.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class ListImages < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | my_images = ocean.image.all(private: true) 7 | public_images = ocean.image.all.images - my_images.images 8 | 9 | if env['user_show_just_private_images'] 10 | say 'Showing just private images', :green 11 | say 'Private Images:', :blue 12 | my_images_list = my_images.images 13 | if my_images_list.nil? || my_images_list.empty? 14 | say 'No private images found' 15 | else 16 | my_images_list.each do |image| 17 | say "#{image.name} (id: #{image.id}, distro: #{image.distribution})" 18 | end 19 | end 20 | else 21 | say 'Showing both private and public images' 22 | say 'Private Images:', :blue 23 | my_images_list = my_images.images 24 | if my_images_list.nil? || my_images_list.empty? 25 | say 'No private images found' 26 | else 27 | my_images_list.each do |image| 28 | say "#{image.name} (id: #{image.id}, distro: #{image.distribution})" 29 | end 30 | end 31 | say '' 32 | say 'Public Images:', :blue 33 | public_images.each do |image| 34 | say "#{image.name} (slug: #{image.slug}, id: #{image.id}, distro: #{image.distribution})" 35 | end 36 | end 37 | 38 | @app.call(env) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/list_regions.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class ListRegions < Base 4 | def call(env) 5 | ocean = env['droplet_kit'] 6 | regions = ocean.regions.all.sort_by(&:name) 7 | 8 | say 'Regions:' 9 | regions.each do |region| 10 | say "#{region.name} (slug: #{region.slug})" 11 | end 12 | 13 | @app.call(env) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/list_sizes.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class ListSizes < Base 4 | def call(env) 5 | ocean = env['droplet_kit'] 6 | sizes = ocean.sizes.all 7 | 8 | say 'Sizes:' 9 | sizes.each do |size| 10 | say "Disk: #{size.disk}GB, Memory: #{size.memory.round}MB (slug: #{size.slug})" 11 | end 12 | 13 | @app.call(env) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/list_snapshots.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | # Check if the client has set-up configuration yet. 4 | class ListSnapshots < Base 5 | def call(env) 6 | ocean = env['droplet_kit'] 7 | 8 | verify_credentials(ocean) 9 | 10 | response = ocean.snapshots.all(per_page: env['per_page']) 11 | 12 | has_one = false 13 | 14 | response.each do |snapshot| 15 | has_one = true 16 | 17 | say "#{snapshot.name} (id: #{snapshot.id}, resource_type: #{snapshot.resource_type}, created_at: #{snapshot.created_at})" 18 | end 19 | 20 | unless has_one 21 | say "You don't appear to have any snapshots.", :red 22 | end 23 | 24 | @app.call(env) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/list_ssh_keys.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class ListSSHKeys < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | ssh_keys = ocean.key.all.ssh_keys 7 | 8 | say 'SSH Keys:' 9 | ssh_keys.each do |key| 10 | say "Name: #{key.name}, (id: #{key.id}), fingerprint: #{key.fingerprint}" 11 | end 12 | 13 | @app.call(env) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/password_reset.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class PasswordReset < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | say "Queuing password reset for #{env['droplet_id']} #{env['droplet_name']}...", nil, false 8 | response = ocean.droplet.password_reset env['droplet_id'] 9 | 10 | if response.success? 11 | say 'Password reset successful!', :green 12 | say 'Your new root password will be emailed to you', :green 13 | else 14 | say "Failed to reset password on Droplet: #{response.message}", :red 15 | exit 1 16 | end 17 | 18 | @app.call(env) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/rebuild_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class RebuildDroplet < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | say "Queuing rebuild for droplet #{env['droplet_id']} #{env['droplet_name']} with image #{env['image_id']} #{env['image_name']}...", nil, false 8 | 9 | response = ocean.droplet.rebuild env['droplet_id'], 10 | image: env['image_id'] 11 | 12 | if response.success? 13 | say 'Rebuild complete', :green 14 | else 15 | say "Failed to rebuild Droplet: #{response.message}", :red 16 | exit 1 17 | end 18 | 19 | @app.call(env) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/resize_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class ResizeDroplet < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | say "Queuing resize for #{env['droplet_id']} #{env['droplet_name']}...", nil, false 8 | 9 | response = ocean.droplet.resize env['droplet_id'], 10 | size: env['user_droplet_size'] 11 | 12 | if response.success? 13 | say 'Resize complete!', :green 14 | else 15 | say "Failed to resize Droplet: #{response.message}", :red 16 | exit 1 17 | end 18 | 19 | @app.call(env) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/restart_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class RestartDroplet < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | response = restart_droplet(env['user_droplet_hard'], ocean, env['droplet_id'], env['droplet_name']) 8 | 9 | if response.success? 10 | say 'Restart complete!', :green 11 | else 12 | say "Failed to restart Droplet: #{response.message}", :red 13 | exit 1 14 | end 15 | 16 | @app.call(env) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/scp_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class SCPDroplet < Base 4 | def call(env) 5 | say "Executing SCP on Droplet #{env['droplet_name']}..." 6 | 7 | identity = File.expand_path(env['config'].ssh_key_path.to_s).strip 8 | 9 | ssh_user = env['user_droplet_ssh_user'] || env['config'].ssh_user 10 | 11 | scp_command = env['user_scp_command'] || 'scp' 12 | 13 | host_ip = env['droplet_ip'] 14 | 15 | host_string = "#{ssh_user}@#{host_ip}" 16 | 17 | if env['user_droplet_ssh_wait'] 18 | say 'Wait flag given, waiting for droplet to become active' 19 | wait_for_state(env['droplet_id'], 'active', env['barge']) 20 | end 21 | 22 | identity_string = "-i #{identity}" 23 | 24 | scp_command_string = [scp_command, identity_string, env['user_from_file'], "#{host_string}:#{env['user_to_file']}"].join(' ') 25 | 26 | say "Attempting SCP with `#{scp_command_string}`" 27 | 28 | Kernel.exec(scp_command_string) 29 | 30 | @app.call(env) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/snapshot_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class SnapshotDroplet < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | # Right now, the digital ocean API doesn't return an error 7 | # when your droplet is not powered off and you try to snapshot. 8 | # This is a temporary measure to let the user know. 9 | say 'Warning: Droplet must be in a powered off state for snapshot to be successful', :yellow 10 | 11 | say "Queuing snapshot '#{env['user_snapshot_name']}' for #{env['droplet_id']} #{env['droplet_name']}...", nil, false 12 | 13 | response = ocean.droplet.snapshot env['droplet_id'], 14 | name: env['user_snapshot_name'] 15 | 16 | if response.success? 17 | say 'Snapshot successful!', :green 18 | else 19 | say "Failed to snapshot Droplet: #{response.message}", :red 20 | exit 1 21 | end 22 | 23 | @app.call(env) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/ssh_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class SSHDroplet < Base 4 | def call(env) 5 | say "Executing SSH on Droplet #{env['droplet_name']}..." 6 | 7 | options = [ 8 | '-o', 'LogLevel=ERROR', 9 | '-o', 'StrictHostKeyChecking=no', 10 | '-o', 'UserKnownHostsFile=/dev/null' 11 | ] 12 | 13 | if env['config'].ssh_key_path.nil? || env['config'].ssh_key_path.empty? 14 | options.push('-o', 'IdentitiesOnly=no') 15 | else 16 | options.push('-o', 'IdentitiesOnly=yes') 17 | options.push('-i', File.expand_path(env['config'].ssh_key_path.to_s)) 18 | end 19 | 20 | if env['user_droplet_ssh_port'] 21 | options.push('-p', env['user_droplet_ssh_port'].to_s) 22 | elsif env['config'].ssh_port 23 | options.push('-p', env['config'].ssh_port.to_s) 24 | else 25 | options.push('-p', '22') 26 | end 27 | 28 | if env['user_droplet_ssh_opts'] 29 | options.concat env['user_droplet_ssh_opts'].split 30 | end 31 | 32 | ssh_user = env['user_droplet_ssh_user'] || env['config'].ssh_user 33 | 34 | host_ip = env['droplet_ip'] 35 | 36 | if env['user_droplet_use_private_ip'] && env['droplet_ip_private'].nil? 37 | say 'You asked to ssh to the private IP, but no Private IP found!', :red 38 | exit 1 39 | end 40 | 41 | if env['droplet_ip_private'] 42 | say 'This droplet has a private IP, checking if you asked to use the Private IP...' 43 | if env['user_droplet_use_private_ip'] 44 | say 'You did! Using private IP for ssh...', :yellow 45 | host_ip = env['droplet_ip_private'] 46 | else 47 | say "You didn't! Using public IP for ssh...", :yellow 48 | end 49 | end 50 | 51 | host_string = "#{ssh_user}@#{host_ip}" 52 | 53 | if env['user_droplet_ssh_wait'] 54 | say 'Wait flag given, waiting for droplet to become active' 55 | wait_for_state(env['droplet_id'], 'active', env['barge']) 56 | end 57 | 58 | say "Attempting SSH: #{host_string}" 59 | 60 | options << host_string 61 | 62 | if env['user_droplet_ssh_command'] 63 | options.push(env['user_droplet_ssh_command']) 64 | end 65 | 66 | say "SShing with options: #{options.join(' ')}" 67 | 68 | Kernel.exec('ssh', *options) 69 | 70 | @app.call(env) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/start_droplet.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class StartDroplet < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | say "Queuing start for #{env['droplet_id']} #{env['droplet_name']}...", nil, false 8 | response = ocean.droplet.power_on env['droplet_id'] 9 | 10 | if response.success? 11 | say 'Start complete!', :green 12 | else 13 | say "Failed to start Droplet: #{response.message}", :red 14 | exit 1 15 | end 16 | 17 | @app.call(env) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/tugboat/middleware/wait_for_state.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | module Middleware 3 | class WaitForState < Base 4 | def call(env) 5 | ocean = env['barge'] 6 | 7 | say "Waiting for droplet to become #{env['user_droplet_desired_state']}.", nil, false 8 | 9 | wait_for_state(env['droplet_id'], env['user_droplet_desired_state'], ocean) 10 | 11 | @app.call(env) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/tugboat/version.rb: -------------------------------------------------------------------------------- 1 | module Tugboat 2 | VERSION = '4.1.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /license/dependency_decisions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - - :ignore_group 3 | - development 4 | - :who: Peter Souter 5 | :why: Development gems are not distributed with the final product and are therefore 6 | exempt. 7 | :versions: [] 8 | :when: 2017-03-18 22:26:01.054140000 Z 9 | - - :ignore_group 10 | - test 11 | - :who: Peter Souter 12 | :why: Test gems are not distributed with the final product and are therefore exempt. 13 | :versions: [] 14 | :when: 2017-03-18 22:26:06.250326000 Z 15 | - - :ignore 16 | - bundler 17 | - :who: Peter Souter 18 | :why: Bundler is MIT licensed but will sometimes fail in CI. 19 | :versions: [] 20 | :when: 2017-03-18 22:46:08.045090000 Z 21 | - - :whitelist 22 | - MIT 23 | - :who: Peter Souter 24 | :why: http://choosealicense.com/licenses/mit/ 25 | :versions: [] 26 | :when: 2017-03-18 22:16:24.558441000 Z 27 | - - :whitelist 28 | - Apache-2.0 29 | - :who: Peter Souter 30 | :why: http://choosealicense.com/licenses/apache-2.0/ 31 | :versions: [] 32 | :when: 2017-03-18 22:26:43.762702000 Z 33 | - - :whitelist 34 | - Ruby 35 | - :who: Peter Souter 36 | :why: https://github.com/ruby/ruby/blob/ruby_2_1/COPYING 37 | :versions: [] 38 | :when: 2017-03-18 22:36:54.498490000 Z 39 | - - :whitelist 40 | - LGPL-2.1 41 | - :who: Peter Souter 42 | :why: http://www.gnu.org/licenses/license-list.html#LGPLv2.1 43 | :versions: [] 44 | :when: 2017-03-18 22:36:48.645841000 Z 45 | - - :whitelist 46 | - ISC 47 | - :who: Peter Souter 48 | :why: http://www.gnu.org/licenses/license-list.html#ISC 49 | :versions: [] 50 | :when: 2017-03-18 22:46:01.894452000 Z 51 | - - :whitelist 52 | - BSD-3-Clause 53 | - :who: Peter Souter 54 | :why: https://opensource.org/licenses/BSD-3-Clause 55 | :versions: [] 56 | :when: 2017-03-18 22:46:38.246021000 Z 57 | - - :whitelist 58 | - BSD-2-Clause 59 | - :who: Peter Souter 60 | :why: https://opensource.org/licenses/BSD-2-Clause 61 | :versions: [] 62 | :when: 2017-03-18 22:56:09.796363000 Z 63 | - - :blacklist 64 | - GPL 2.0 65 | - :who: Peter Souter 66 | :why: GPL-licensed libraries cannot be linked to from non-GPL projects. 67 | :versions: [] 68 | :when: 2017-03-18 22:26:27.637336000 Z 69 | - - :blacklist 70 | - GPL-3.0 71 | - :who: Peter Souter 72 | :why: GPL-licensed libraries cannot be linked to from non-GPL projects. 73 | :versions: [] 74 | :when: 2017-03-18 22:26:43.904715000 Z 75 | - - :blacklist 76 | - OSL-3.0 77 | - :who: Peter Souter 78 | :why: The OSL license is a copyleft license 79 | :versions: [] 80 | :when: 2017-03-18 22:06:15.540105000 Z 81 | - - :approve 82 | - minitest 83 | - :who: 84 | :why: MIT licensed but in the README, can remove when this PR merged - https://github.com/seattlerb/minitest/pull/665 85 | :versions: [] 86 | :when: 2017-06-22 15:25:27.212303000 Z 87 | -------------------------------------------------------------------------------- /spec/cli/add_key_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fileutils' 3 | 4 | describe Tugboat::CLI do 5 | include_context 'spec' 6 | 7 | let(:tmp_path) { project_path + '/tmp/tugboat' } 8 | let(:fake_home) { "#{project_path}/tmp" } 9 | 10 | before do 11 | File.open('id_dsa.pub', 'w') { |f| f.write('ssh-dss A456= user@host') } 12 | end 13 | 14 | after do 15 | File.delete('id_dsa.pub') if File.exist?('id_dsa.pub') 16 | File.delete("#{fake_home}/.ssh/id_rsa.pub") if File.exist?("#{fake_home}/.ssh/id_rsa.pub") 17 | end 18 | 19 | describe 'add-key' do 20 | it 'with a name and key string' do 21 | stub_request(:post, 'https://api.digitalocean.com/v2/account/keys'). 22 | with(body: '{"name":"macbook_pro","public_key":"ssh-dss A123= user@host"}'). 23 | to_return(status: 201, body: fixture('create_ssh_key'), headers: {}) 24 | 25 | cli.options = cli.options.merge(key: ssh_public_key.to_s) 26 | 27 | add_key_with_name_and_keystring = <<-eos 28 | Queueing upload of SSH key 'macbook_pro'...SSH Key uploaded 29 | 30 | Name: macbook_pro 31 | ID: 3 32 | eos 33 | 34 | expect { cli.add_key(ssh_key_name) }.to output(add_key_with_name_and_keystring).to_stdout 35 | 36 | expect(a_request(:post, 'https://api.digitalocean.com/v2/account/keys')).to have_been_made 37 | end 38 | 39 | before do 40 | allow(ENV).to receive(:[]).with('HOME').and_return(fake_home) 41 | allow(ENV).to receive(:[]).with('DEBUG').and_return(nil) 42 | allow(ENV).to receive(:[]).with('DO_API_TOKEN').and_return(nil) 43 | allow(ENV).to receive(:[]).with('http_proxy').and_return(nil) 44 | allow(ENV).to receive(:[]).with('THOR_SHELL').and_return(nil) 45 | 46 | FileUtils.mkdir_p "#{fake_home}/.ssh" 47 | File.open("#{fake_home}/.ssh/id_rsa.pub", 'w') { |f| f.write('ssh-dss A456= user@host') } 48 | end 49 | 50 | it 'with name, prompts from file folder' do 51 | stub_request(:post, 'https://api.digitalocean.com/v2/account/keys'). 52 | with(body: '{"name":"macbook_pro","public_key":"ssh-dss A456= user@host"}', 53 | headers: { 'Accept' => '*/*', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json' }). 54 | to_return(status: 201, body: fixture('create_ssh_key_from_file'), headers: {}) 55 | 56 | expect($stdin).to receive(:gets).and_return("#{fake_home}/.ssh/id_rsa.pub") 57 | 58 | with_name_prompts_from_file_folder_stdout = <<-eos 59 | Possible public key paths from #{fake_home}/.ssh: 60 | 61 | #{fake_home}/.ssh/id_rsa.pub 62 | 63 | Enter the path to your SSH key: Queueing upload of SSH key 'macbook_pro'...SSH Key uploaded 64 | 65 | Name: cool_key 66 | ID: 5 67 | eos 68 | 69 | expect { cli.add_key(ssh_key_name) }.to output(with_name_prompts_from_file_folder_stdout).to_stdout 70 | end 71 | 72 | after do 73 | File.delete('id_dsa.pub') if File.exist?('id_dsa.pub') 74 | FileUtils.rm_rf("#{fake_home}/.ssh/") if File.exist?("#{fake_home}/.ssh/") 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/cli/authorize_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | let(:tmp_path) { project_path + '/tmp/tugboat' } 7 | 8 | describe 'authorize' do 9 | before do 10 | end 11 | 12 | it 'asks the right questions and checks credentials' do 13 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 14 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 15 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 16 | 17 | expect($stdin).to receive(:gets).and_return(access_token) 18 | expect($stdin).to receive(:gets).and_return(timeout) 19 | expect($stdin).to receive(:gets).and_return(ssh_key_path) 20 | expect($stdin).to receive(:gets).and_return(ssh_user) 21 | expect($stdin).to receive(:gets).and_return(ssh_port) 22 | expect($stdin).to receive(:gets).and_return(region) 23 | expect($stdin).to receive(:gets).and_return(image) 24 | expect($stdin).to receive(:gets).and_return(size) 25 | expect($stdin).to receive(:gets).and_return(ssh_key_id) 26 | expect($stdin).to receive(:gets).and_return(private_networking) 27 | expect($stdin).to receive(:gets).and_return(backups_enabled) 28 | expect($stdin).to receive(:gets).and_return(ip6) 29 | 30 | expect { cli.authorize }.to output(/Note: You can get your Access Token from https:\/\/cloud.digitalocean.com\/settings\/tokens\/new/).to_stdout 31 | 32 | config = YAML.load_file(tmp_path) 33 | 34 | expect(config['defaults']['image']).to eq image 35 | expect(config['defaults']['region']).to eq region 36 | expect(config['defaults']['size']).to eq size 37 | expect(config['ssh']['ssh_user']).to eq ssh_user 38 | expect(config['ssh']['ssh_key_path']).to eq ssh_key_path 39 | expect(config['ssh']['ssh_port']).to eq ssh_port 40 | expect(config['defaults']['ssh_key']).to eq ssh_key_id 41 | expect(config['defaults']['private_networking']).to eq private_networking 42 | expect(config['defaults']['backups_enabled']).to eq backups_enabled 43 | expect(config['defaults']['ip6']).to eq ip6 44 | end 45 | 46 | it 'sets defaults if no input given' do 47 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 48 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => %r{Bearer}, 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 49 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 50 | 51 | expect($stdin).to receive(:gets).and_return('') 52 | expect($stdin).to receive(:gets).and_return('') 53 | expect($stdin).to receive(:gets).and_return('') 54 | expect($stdin).to receive(:gets).and_return('') 55 | expect($stdin).to receive(:gets).and_return('') 56 | expect($stdin).to receive(:gets).and_return('') 57 | expect($stdin).to receive(:gets).and_return('') 58 | expect($stdin).to receive(:gets).and_return('') 59 | expect($stdin).to receive(:gets).and_return('') 60 | expect($stdin).to receive(:gets).and_return('') 61 | expect($stdin).to receive(:gets).and_return('') 62 | expect($stdin).to receive(:gets).and_return('') 63 | 64 | expect { cli.authorize }.to output(/Note: You can get your Access Token from https:\/\/cloud.digitalocean.com\/settings\/tokens\/new/).to_stdout 65 | 66 | config = YAML.load_file(tmp_path) 67 | 68 | expect(config['defaults']['image']).to eq 'ubuntu-14-04-x64' 69 | expect(config['defaults']['region']).to eq 'nyc2' 70 | expect(config['defaults']['size']).to eq '512mb' 71 | expect(config['ssh']['ssh_user']).to eq 'root' 72 | expect(config['ssh']['ssh_key_path']).to eq '~/.ssh/id_rsa' 73 | expect(config['ssh']['ssh_port']).to eq '22' 74 | expect(config['defaults']['ssh_key']).to eq '' 75 | expect(config['defaults']['private_networking']).to eq 'false' 76 | expect(config['defaults']['backups_enabled']).to eq 'false' 77 | expect(config['defaults']['ip6']).to eq 'false' 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/cli/backup_setting_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'backup_config' do 7 | it 'enables backups on a droplet with the enable flag' do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 11 | 12 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 13 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 14 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 15 | 16 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets/6918990/actions'). 17 | with(body: '{"type":"enable_backups"}', 18 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 19 | to_return(status: 200, body: fixture('enable_backups_response'), headers: {}) 20 | 21 | cli.options = cli.options.merge(enable: true) 22 | 23 | expected_string = <<-eos 24 | Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) 25 | Backup action enable backups is in-progress 26 | eos 27 | 28 | expect { cli.backup_config('example.com') }.to output(expected_string).to_stdout 29 | end 30 | 31 | it 'enables backups on a droplet with the disable flag' do 32 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 33 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 34 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 35 | 36 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 37 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 38 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 39 | 40 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets/6918990/actions'). 41 | with(body: '{"type":"disable_backups"}', 42 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 43 | to_return(status: 200, body: fixture('disable_backups_response'), headers: {}) 44 | 45 | cli.options = cli.options.merge(disable: true) 46 | 47 | expected_string = <<-eos 48 | Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) 49 | Backup action disable backups is in-progress 50 | eos 51 | 52 | expect { cli.backup_config('example.com') }.to output(expected_string).to_stdout 53 | end 54 | 55 | it 'shows error if both enable and disable given' do 56 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 57 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 58 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 59 | 60 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 61 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 62 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 63 | 64 | cli.options = cli.options.merge(disable: true, enable: true) 65 | 66 | expect { cli.backup_config('example.com') }.to raise_error(SystemExit).and output(%r{You cannot use both --disable and --enable for backup_config}).to_stdout 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/cli/config_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'config' do 7 | it 'shows the full config' do 8 | expected_string = <<-eos 9 | Current Config\x20 10 | Path: #{Dir.pwd}/tmp/tugboat 11 | --- 12 | authentication: 13 | access_token: foo 14 | connection: 15 | timeout: '15' 16 | ssh: 17 | ssh_user: baz 18 | ssh_key_path: ~/.ssh/id_rsa2 19 | ssh_port: '33' 20 | defaults: 21 | region: nyc2 22 | image: ubuntu-14-04-x64 23 | size: 512mb 24 | ssh_key: '1234' 25 | private_networking: 'false' 26 | backups_enabled: 'false' 27 | ip6: 'false' 28 | eos 29 | 30 | expect { cli.config }.to output(expected_string).to_stdout 31 | end 32 | 33 | it 'hides sensitive data if option given' do 34 | cli.options = cli.options.merge(hide: true) 35 | 36 | expected_string = <<-eos 37 | Current Config (Keys Redacted) 38 | Path: #{Dir.pwd}/tmp/tugboat 39 | --- 40 | authentication: 41 | access_token:\x20\x20[REDACTED] 42 | connection: 43 | timeout: '15' 44 | ssh: 45 | ssh_user: baz 46 | ssh_key_path: ~/.ssh/id_rsa2 47 | ssh_port: '33' 48 | defaults: 49 | region: nyc2 50 | image: ubuntu-14-04-x64 51 | size: 512mb 52 | ssh_key: '1234' 53 | private_networking: 'false' 54 | backups_enabled: 'false' 55 | ip6: 'false' 56 | eos 57 | 58 | expect { cli.config }.to output(expected_string).to_stdout 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/cli/create_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'create a droplet' do 7 | it 'with a name, uses defaults from configuration' do 8 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets'). 9 | with(body: '{"name":"foo","size":"512mb","image":"ubuntu-14-04-x64","region":"nyc2","ssh_keys":["1234"],"private_networking":"false","backups_enabled":"false","ipv6":"false","user_data":null}', 10 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 11 | to_return(status: 200, body: fixture('create_droplet'), headers: {}) 12 | 13 | expected_string = <<-eos 14 | Queueing creation of droplet '#{droplet_name}'...Droplet created! Droplet ID is 3164494 15 | eos 16 | 17 | expect { cli.create(droplet_name) }.to output(expected_string).to_stdout 18 | end 19 | 20 | it 'with args does not use defaults from configuration' do 21 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets'). 22 | with(body: '{"name":"example.com","size":"1gb","image":"ubuntu-12-04-x64","region":"nyc3","ssh_keys":["foo_bar_key"],"private_networking":"false","backups_enabled":"false","ipv6":"false","user_data":null}', 23 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 24 | to_return(status: 200, body: fixture('create_droplet'), headers: {}) 25 | 26 | cli.options = cli.options.merge(image: 'ubuntu-12-04-x64', size: '1gb', region: 'nyc3', keys: 'foo_bar_key') 27 | 28 | expected_string = <<-eos 29 | Queueing creation of droplet 'example.com'...Droplet created! Droplet ID is 3164494 30 | eos 31 | 32 | expect { cli.create('example.com') }.to output(expected_string).to_stdout 33 | end 34 | 35 | it 'with ip6 enable args' do 36 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets'). 37 | with(body: '{"name":"example.com","size":"512mb","image":"ubuntu-14-04-x64","region":"nyc2","ssh_keys":["1234"],"private_networking":"false","backups_enabled":"false","ipv6":"true","user_data":null}', 38 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 39 | to_return(status: 200, body: fixture('create_droplet'), headers: {}) 40 | 41 | cli.options = cli.options.merge(ip6: 'true') 42 | 43 | expected_string = <<-eos 44 | Queueing creation of droplet 'example.com'...Droplet created! Droplet ID is 3164494 45 | eos 46 | 47 | expect { cli.create('example.com') }.to output(expected_string).to_stdout 48 | end 49 | 50 | it 'with user data args' do 51 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets'). 52 | with(body: '{"name":"example.com","size":"512mb","image":"ubuntu-14-04-x64","region":"nyc2","ssh_keys":["1234"],"private_networking":"false","backups_enabled":"false","ipv6":"false","user_data":"#!/bin/bash\\n\\necho \\"Hello world\\""}', 53 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 54 | to_return(status: 200, body: fixture('create_droplet'), headers: {}) 55 | 56 | cli.options = cli.options.merge(user_data: project_path + '/spec/fixtures/user_data.sh') 57 | 58 | expected_string = <<-eos 59 | Queueing creation of droplet 'example.com'...Droplet created! Droplet ID is 3164494 60 | eos 61 | 62 | expect { cli.create('example.com') }.to output(expected_string).to_stdout 63 | end 64 | 65 | it 'fails when user data file does not exist' do 66 | cli.options = cli.options.merge(user_data: '/foo/bar/baz.sh') 67 | 68 | expected_string = <<-eos 69 | Queueing creation of droplet 'example.com'...Could not find file: /foo/bar/baz.sh, check your user_data setting 70 | eos 71 | 72 | expect { expect { cli.create('example.com') }.to raise_error(SystemExit) }.to output(expected_string).to_stdout 73 | end 74 | 75 | context "doesn't create a droplet when mistyping help command" do 76 | ['help', '--help', '-h'].each do |help_attempt| 77 | it "tugboat create #{help_attempt}" do 78 | expected_string = <<-eos 79 | Usage: 80 | rspec create NAME 81 | 82 | Options: 83 | -s, [--size=SIZE] # The size slug of the droplet 84 | -i, [--image=IMAGE] # The image slug of the droplet 85 | -r, [--region=REGION] # The region slug of the droplet 86 | -k, [--keys=KEYS] # A comma separated list of SSH key ids to add to the droplet 87 | -p, [--private-networking] # Enable private networking on the droplet 88 | -l, [--ip6] # Enable IP6 on the droplet 89 | -u, [--user-data=USER_DATA] # Location of a file to read and use as user data 90 | -b, [--backups-enabled] # Enable backups on the droplet 91 | -q, [--quiet] \x20 92 | 93 | Create a droplet. 94 | eos 95 | 96 | expect { cli.create(help_attempt) }.to output(expected_string).to_stdout 97 | end 98 | end 99 | end 100 | 101 | it 'does not clobber named droplets that contain the word help' do 102 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets'). 103 | with(body: '{"name":"somethingblahblah--help","size":"512mb","image":"ubuntu-14-04-x64","region":"nyc2","ssh_keys":["1234"],"private_networking":"false","backups_enabled":"false","ipv6":"false","user_data":null}', 104 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 105 | to_return(status: 200, body: fixture('create_droplet'), headers: {}) 106 | 107 | expected_string = <<-eos 108 | Queueing creation of droplet 'somethingblahblah--help'...Droplet created! Droplet ID is 3164494 109 | eos 110 | 111 | expect { cli.create('somethingblahblah--help') }.to output(expected_string).to_stdout 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /spec/cli/debug_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'DEBUG=1' do 7 | before do 8 | allow(ENV).to receive(:[]).with('HOME').and_return('/tmp/fake_home') 9 | allow(ENV).to receive(:[]).with('DEBUG').and_return(1) 10 | allow(ENV).to receive(:[]).with('http_proxy').and_return(nil) 11 | allow(ENV).to receive(:[]).with('DO_API_TOKEN').and_return(nil) 12 | allow(ENV).to receive(:[]).with('THOR_SHELL').and_return(nil) 13 | end 14 | 15 | it 'gives full faraday logs' do 16 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 17 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 18 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 19 | 20 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=20'). 21 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 22 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 23 | 24 | debug_droplets_expectation = expect { cli.droplets } 25 | 26 | debug_droplets_expectation.to output(%r{DEBUG -- : Request Headers:}).to_stdout 27 | debug_droplets_expectation.to output(%r{Bearer foo}).to_stdout 28 | debug_droplets_expectation.to output(%r{Started GET request to}).to_stdout 29 | end 30 | end 31 | 32 | describe 'DEBUG=2' do 33 | before do 34 | allow(ENV).to receive(:[]).with('HOME').and_return('/tmp/fake_home') 35 | allow(ENV).to receive(:[]).with('DEBUG').and_return(2) 36 | allow(ENV).to receive(:[]).with('http_proxy').and_return(nil) 37 | allow(ENV).to receive(:[]).with('DO_API_TOKEN').and_return(nil) 38 | allow(ENV).to receive(:[]).with('THOR_SHELL').and_return(nil) 39 | end 40 | 41 | it 'gives full faraday logs with redacted API keys' do 42 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 43 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 44 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 45 | 46 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=20'). 47 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 48 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 49 | 50 | debug_droplets_expectation = expect { cli.droplets } 51 | debug_droplets_expectation.to output(%r{Started GET request to}).to_stdout 52 | debug_droplets_expectation.to output(%r{DEBUG -- : Request Headers:}).to_stdout 53 | debug_droplets_expectation.to output(%r{Bearer \[TOKEN REDACTED\]}).to_stdout 54 | debug_droplets_expectation.not_to output(%r{Bearer foo}).to_stdout 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/cli/destroy_image_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'destroy image' do 7 | it 'destroys an image with a fuzzy name' do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/images?per_page=200'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(status: 200, body: fixture('show_images_global'), headers: {}) 11 | 12 | stub_request(:delete, 'https://api.digitalocean.com/v2/images/6376601'). 13 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 14 | to_return(status: 204, body: '', headers: {}) 15 | 16 | expect($stdin).to receive(:gets).and_return('y') 17 | 18 | expected_string = <<-eos 19 | Image fuzzy name provided. Finding image ID...done\e[0m, 6376601 (My application image)\nWarning! Potentially destructive action. Please confirm [y/n]: Queuing destroy image for 6376601 (My application image)...Image deletion successful! 20 | eos 21 | 22 | expect { cli.destroy_image('My application image') }.to output(expected_string).to_stdout 23 | end 24 | 25 | it 'destroys an image with an id' do 26 | stub_request(:get, 'https://api.digitalocean.com/v2/images/6376601?per_page=200'). 27 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 28 | to_return(status: 200, body: fixture('show_image'), headers: {}) 29 | 30 | stub_request(:delete, 'https://api.digitalocean.com/v2/images/6376601'). 31 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 32 | to_return(status: 204, body: '', headers: {}) 33 | 34 | expect($stdin).to receive(:gets).and_return('y') 35 | 36 | cli.options = cli.options.merge(id: 6_376_601) 37 | 38 | expected_string = <<-eos 39 | Image id provided. Finding Image...done\e[0m, 6376601 (My application image)\nWarning! Potentially destructive action. Please confirm [y/n]: Queuing destroy image for 6376601 (My application image)...Image deletion successful! 40 | eos 41 | 42 | expect { cli.destroy_image }.to output(expected_string).to_stdout 43 | end 44 | 45 | it 'destroys an image with a name' do 46 | stub_request(:get, 'https://api.digitalocean.com/v2/images?per_page=200'). 47 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 48 | to_return(status: 200, body: fixture('show_images_global'), headers: {}) 49 | 50 | stub_request(:delete, 'https://api.digitalocean.com/v2/images/6376601'). 51 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 52 | to_return(status: 204, body: '', headers: {}) 53 | 54 | expect($stdin).to receive(:gets).and_return('y') 55 | 56 | expected_string = "Image name provided. Finding Image...done\e[0m, 6376601 (My application image)\nWarning! Potentially destructive action. Please confirm [y/n]: Queuing destroy image for 6376601 (My application image)...Image deletion successful!\n" 57 | 58 | cli.options = cli.options.merge(name: 'My application image') 59 | expect { cli.destroy_image }.to output(expected_string).to_stdout 60 | end 61 | 62 | it 'destroys an image with confirm flag set' do 63 | stub_request(:get, 'https://api.digitalocean.com/v2/images?per_page=200'). 64 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 65 | to_return(status: 200, body: fixture('show_images_global'), headers: {}) 66 | 67 | stub_request(:delete, 'https://api.digitalocean.com/v2/images/6376601'). 68 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 69 | to_return(status: 204, body: '', headers: {}) 70 | 71 | cli.options = cli.options.merge(name: 'My application image') 72 | cli.options = cli.options.merge(confirm: true) 73 | 74 | expected_string = <<-eos 75 | Image name provided. Finding Image...done\e[0m, 6376601 (My application image)\nQueuing destroy image for 6376601 (My application image)...Image deletion successful! 76 | eos 77 | 78 | expect { cli.destroy_image('NLP Final') }.to output(expected_string).to_stdout 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/cli/env_variable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'DO_API_TOKEN=foobar' do 7 | it 'verifies with the ENV variable DO_API_TOKEN' do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer env_variable', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 11 | 12 | allow(ENV).to receive(:[]).with('HOME').and_return('/tmp/fake_home') 13 | allow(ENV).to receive(:[]).with('DO_API_TOKEN').and_return('env_variable') 14 | allow(ENV).to receive(:[]).with('http_proxy').and_return(nil) 15 | allow(ENV).to receive(:[]).with('DEBUG').and_return(nil) 16 | allow(ENV).to receive(:[]).with('THOR_SHELL').and_return(nil) 17 | 18 | expected_string = "Authentication with DigitalOcean was successful.\n" 19 | expect { cli.verify }.to output(expected_string).to_stdout 20 | expect(a_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1')).to have_been_made 21 | end 22 | 23 | it 'does not use ENV variable DO_API_TOKEN if empty' do 24 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 25 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 26 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 27 | 28 | allow(ENV).to receive(:[]).with('HOME').and_return('/tmp/fake_home') 29 | allow(ENV).to receive(:[]).with('DO_API_TOKEN').and_return('') 30 | allow(ENV).to receive(:[]).with('http_proxy').and_return(nil) 31 | allow(ENV).to receive(:[]).with('DEBUG').and_return(nil) 32 | allow(ENV).to receive(:[]).with('THOR_SHELL').and_return(nil) 33 | 34 | expected_string = "Authentication with DigitalOcean was successful.\n" 35 | expect { cli.verify }.to output(expected_string).to_stdout 36 | expect(a_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1')).to have_been_made 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/cli/help_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'help' do 7 | it 'shows a help message' do 8 | expected_string = %r{To learn more or to contribute, please see} 9 | expect { cli.help }.to output(expected_string).to_stdout 10 | end 11 | 12 | it 'shows a help message for specific commands' do 13 | expected_string = %r{Show available droplet sizes} 14 | expect { cli.help 'sizes' }.to output(expected_string).to_stdout 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/cli/keys_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'keys' do 7 | it 'shows a list' do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/account/keys?per_page=200'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(status: 200, body: fixture('show_keys'), headers: {}) 11 | 12 | expected_string = <<-eos 13 | SSH Keys: 14 | Name: My SSH Public Key, (id: 512189), fingerprint: 3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa 15 | Name: My Other SSH Public Key, (id: 512110), fingerprint: 3b:16:bf:d4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa 16 | eos 17 | 18 | expect { cli.keys }.to output(expected_string).to_stdout 19 | 20 | expect(a_request(:get, 'https://api.digitalocean.com/v2/account/keys?per_page=200')).to have_been_made 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/cli/regions_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'regions' do 7 | it 'shows a list' do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/regions?page=1&per_page=20'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(status: 200, body: fixture('show_regions'), headers: { 'Content-Type' => 'application/json' }) 11 | 12 | expected_string = <<-eos 13 | Regions: 14 | Amsterdam 1 (slug: ams1) 15 | Amsterdam 2 (slug: ams2) 16 | Amsterdam 3 (slug: ams3) 17 | London 1 (slug: lon1) 18 | New York 1 (slug: nyc1) 19 | New York 2 (slug: nyc2) 20 | New York 3 (slug: nyc3) 21 | San Francisco 1 (slug: sfo1) 22 | Singapore 1 (slug: sgp1) 23 | eos 24 | 25 | expect { cli.regions }.to output(expected_string).to_stdout 26 | 27 | expect(a_request(:get, 'https://api.digitalocean.com/v2/regions?page=1&per_page=20')). 28 | to have_been_made 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/cli/resize_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'resize' do 7 | it 'resizes a droplet with a fuzzy name' do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 11 | 12 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 13 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 14 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 15 | 16 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets/6918990/actions'). 17 | with(body: '{"type":"resize","size":"1gb"}', 18 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 19 | to_return(status: 200, body: fixture('resize_droplet'), headers: {}) 20 | 21 | expected_string = <<-eos 22 | Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) 23 | Queuing resize for 6918990 (example.com)...Resize complete! 24 | eos 25 | 26 | cli.options = cli.options.merge(size: '1gb') 27 | expect { cli.resize('example.com') }.to output(expected_string).to_stdout 28 | end 29 | 30 | it 'resizes a droplet with an id' do 31 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 32 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 33 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 34 | 35 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets/6918990?per_page=200'). 36 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 37 | to_return(status: 200, body: fixture('show_droplet'), headers: {}) 38 | 39 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets/6918990/actions'). 40 | with(body: '{"type":"resize","size":"1gb"}', 41 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 42 | to_return(status: 200, body: fixture('resize_droplet'), headers: {}) 43 | 44 | expected_string = <<-eos 45 | Droplet id provided. Finding Droplet...done\e[0m, 6918990 (example.com) 46 | Queuing resize for 6918990 (example.com)...Resize complete! 47 | eos 48 | 49 | cli.options = cli.options.merge(size: '1gb', id: 6_918_990) 50 | expect { cli.resize }.to output(expected_string).to_stdout 51 | end 52 | 53 | it 'resizes a droplet with a name' do 54 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 55 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 56 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 57 | 58 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 59 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 60 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 61 | 62 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets/6918990/actions'). 63 | with(body: '{"type":"resize","size":"1gb"}', 64 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 65 | to_return(status: 200, body: fixture('resize_droplet'), headers: {}) 66 | 67 | expected_string = <<-eos 68 | Droplet name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) 69 | Queuing resize for 6918990 (example.com)...Resize complete! 70 | eos 71 | 72 | cli.options = cli.options.merge(size: '1gb', name: 'example.com') 73 | expect { cli.resize }.to output(expected_string).to_stdout 74 | end 75 | 76 | it 'raises SystemExit when a request fails' do 77 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 78 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 79 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 80 | 81 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 82 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 83 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 84 | 85 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets/6918990/actions'). 86 | with(body: '{"type":"resize","size":"1gb"}', 87 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 88 | to_return(headers: { 'Content-Type' => 'application/json' }, status: 500, body: '{"status":"ERROR","message":"Some error"}') 89 | 90 | expected_string = <<-eos 91 | Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) 92 | Queuing resize for 6918990 (example.com)...Failed to resize Droplet: Some error 93 | eos 94 | 95 | cli.options = cli.options.merge(size: '1gb') 96 | expect { cli.resize('example.com') }.to raise_error(SystemExit).and output(expected_string).to_stdout 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/cli/scp_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'scp' do 7 | it "tries to fetch the droplet's IP from the API" do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 11 | 12 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 13 | to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplets')) 14 | allow(Kernel).to receive(:exec).with("scp -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar") 15 | 16 | expected_string = <<-eos 17 | Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) 18 | Executing SCP on Droplet (example.com)... 19 | Attempting SCP with `scp -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar` 20 | eos 21 | 22 | expect { cli.scp('example.com', '/tmp/foo', '/tmp/bar') }.to output(expected_string).to_stdout 23 | end 24 | 25 | it "runs with rsync if given at the command line" do 26 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 27 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 28 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 29 | 30 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 31 | to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplets')) 32 | allow(Kernel).to receive(:exec).with("rsync -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar") 33 | 34 | expected_string = <<-eos 35 | Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) 36 | Executing SCP on Droplet (example.com)... 37 | Attempting SCP with `rsync -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar` 38 | eos 39 | 40 | cli.options = cli.options.merge(scp_command: 'rsync') 41 | 42 | expect { cli.scp('example.com', '/tmp/foo', '/tmp/bar') }.to output(expected_string).to_stdout 43 | end 44 | 45 | it "wait's until droplet active if -w command is given and droplet eventually active" do 46 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 47 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 48 | to_return(status: 200, body: '', headers: {}) 49 | 50 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets/6918990?per_page=200'). 51 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 52 | to_return( 53 | { status: 200, body: fixture('show_droplet_inactive'), headers: {} }, 54 | status: 200, body: fixture('show_droplet'), headers: {} 55 | ) 56 | 57 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 58 | to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplets')) 59 | allow(Kernel).to receive(:exec).with("scp -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar") 60 | 61 | cli.options = cli.options.merge(wait: true) 62 | 63 | expected_string = <<-eos 64 | Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) 65 | Executing SCP on Droplet (example.com)... 66 | Wait flag given, waiting for droplet to become active 67 | ..done\e[0m (2s) 68 | Attempting SCP with `scp -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar` 69 | eos 70 | 71 | expect { cli.scp('example.com', '/tmp/foo', '/tmp/bar') }.to output(expected_string).to_stdout 72 | end 73 | 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/cli/sizes_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'sizes' do 7 | it 'shows a list' do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/sizes?page=1&per_page=20'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_sizes')) 11 | 12 | cli_sizes_output = <<-eos 13 | Sizes: 14 | Disk: 20GB, Memory: 512MB (slug: 512mb) 15 | Disk: 30GB, Memory: 1024MB (slug: 1gb) 16 | Disk: 40GB, Memory: 2048MB (slug: 2gb) 17 | Disk: 60GB, Memory: 4096MB (slug: 4gb) 18 | Disk: 80GB, Memory: 8192MB (slug: 8gb) 19 | Disk: 160GB, Memory: 16384MB (slug: 16gb) 20 | Disk: 320GB, Memory: 32768MB (slug: 32gb) 21 | Disk: 480GB, Memory: 49152MB (slug: 48gb) 22 | Disk: 640GB, Memory: 65536MB (slug: 64gb) 23 | eos 24 | 25 | expect { cli.sizes }.to output(cli_sizes_output).to_stdout 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/cli/snapshots_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'snapshots' do 7 | it 'shows a list when snapshots exist' do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 11 | 12 | stub_request(:get, "https://api.digitalocean.com/v2/snapshots?page=1&per_page=20"). 13 | with(headers: {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'Bearer foo', 'Content-Type'=>'application/json', 'User-Agent'=>'Faraday v0.9.2'}). 14 | to_return(status: 200, body: fixture('show_snapshots'), headers: {}) 15 | 16 | expected_string = <<-eos 17 | 5.10 x64 (id: 6372321, resource_type: droplet, created_at: 2014-09-26T16:40:18Z) 18 | eos 19 | 20 | expect { cli.snapshots }.to output(expected_string).to_stdout 21 | 22 | expect(a_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1')).to have_been_made.once 23 | expect(a_request(:get, 'https://api.digitalocean.com/v2/snapshots?page=1&per_page=20')).to have_been_made.once 24 | end 25 | 26 | it 'shows a message when no snapshots exist' do 27 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 28 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 29 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 30 | 31 | stub_request(:get, "https://api.digitalocean.com/v2/snapshots?page=1&per_page=20"). 32 | with(headers: {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'Bearer foo', 'Content-Type'=>'application/json', 'User-Agent'=>'Faraday v0.9.2'}). 33 | to_return(status: 200, body: fixture('show_snapshots_empty'), headers: {}) 34 | 35 | expected_string = <<-eos 36 | You don't appear to have any snapshots. 37 | eos 38 | 39 | expect { cli.snapshots }.to output(expected_string).to_stdout 40 | 41 | expect(a_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1')).to have_been_made.once 42 | expect(a_request(:get, 'https://api.digitalocean.com/v2/snapshots?page=1&per_page=20')).to have_been_made.once 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/cli/ssh_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'ssh' do 7 | it "tries to fetch the droplet's IP from the API" do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 11 | 12 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 13 | to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplets')) 14 | allow(Kernel).to receive(:exec).with('ssh', anything, anything, anything, anything, anything, anything, anything, anything, anything, anything, anything, anything, anything) 15 | 16 | expect { cli.ssh('example.com') }.to output(%r{Attempting SSH: baz@104.236.32.182}).to_stdout 17 | end 18 | 19 | it "wait's until droplet active if -w command is given and droplet already active" do 20 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 21 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 22 | to_return(status: 200, body: '', headers: {}) 23 | 24 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets/6918990?per_page=200'). 25 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 26 | to_return(status: 200, body: fixture('show_droplet'), headers: {}) 27 | 28 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 29 | to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplets')) 30 | allow(Kernel).to receive(:exec).with('ssh', anything, anything, anything, anything, anything, anything, anything, anything, anything, anything, anything, anything, anything) 31 | 32 | cli.options = cli.options.merge(wait: true) 33 | 34 | expected_string = <<-eos 35 | Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) 36 | Executing SSH on Droplet (example.com)... 37 | Wait flag given, waiting for droplet to become active 38 | .done\e[0m (0s) 39 | Attempting SSH: baz@104.236.32.182 40 | SShing with options: -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i #{Dir.home}/.ssh/id_rsa2 -p 33 baz@104.236.32.182 41 | eos 42 | 43 | expect { cli.ssh('example.com') }.to output(expected_string).to_stdout 44 | end 45 | 46 | it "wait's until droplet active if -w command is given and droplet eventually active" do 47 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 48 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 49 | to_return(status: 200, body: '', headers: {}) 50 | 51 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets/6918990?per_page=200'). 52 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 53 | to_return( 54 | { status: 200, body: fixture('show_droplet_inactive'), headers: {} }, 55 | status: 200, body: fixture('show_droplet'), headers: {} 56 | ) 57 | 58 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 59 | to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplets')) 60 | allow(Kernel).to receive(:exec).with('ssh', anything, anything, anything, anything, anything, anything, anything, anything, anything, anything, anything, anything, anything) 61 | 62 | cli.options = cli.options.merge(wait: true) 63 | 64 | expected_string = <<-eos 65 | Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) 66 | Executing SSH on Droplet (example.com)... 67 | Wait flag given, waiting for droplet to become active 68 | ..done\e[0m (2s) 69 | Attempting SSH: baz@104.236.32.182 70 | SShing with options: -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i #{Dir.home}/.ssh/id_rsa2 -p 33 baz@104.236.32.182 71 | eos 72 | 73 | expect { cli.ssh('example.com') }.to output(expected_string).to_stdout 74 | end 75 | 76 | it 'does not allow ssh into a droplet that is inactive' do 77 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 78 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 79 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 80 | 81 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 82 | to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplets')) 83 | allow(Kernel).to receive(:exec) 84 | 85 | expected_string = <<-eos 86 | Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 3164444 (example3.com) 87 | Droplet must be on for this operation to be successful. 88 | eos 89 | 90 | expect { expect { cli.ssh('example3.com') }.to raise_error(SystemExit) }.to output(expected_string).to_stdout 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/cli/start_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'start' do 7 | it 'starts the droplet with a fuzzy name' do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 11 | 12 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 13 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 14 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 15 | 16 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets/3164444/actions'). 17 | with(body: '{"type":"power_on"}', 18 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 19 | to_return(status: 200, body: fixture('droplet_start_response'), headers: {}) 20 | 21 | expected_string = <<-eos 22 | Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 3164444 (example3.com) 23 | Queuing start for 3164444 (example3.com)...Start complete! 24 | eos 25 | 26 | expect { cli.start('example3.com') }.to output(expected_string).to_stdout 27 | end 28 | 29 | it 'starts the droplet with an id' do 30 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 31 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 32 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 33 | 34 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets/3164494?per_page=200'). 35 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 36 | to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplet_inactive')) 37 | 38 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets/3164494/actions'). 39 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 40 | to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplet_inactive')) 41 | 42 | expected_string = <<-eos 43 | Droplet id provided. Finding Droplet...done\e[0m, 3164494 (example.com) 44 | Queuing start for 3164494 (example.com)...Start complete! 45 | eos 46 | 47 | cli.options = cli.options.merge(id: '3164494') 48 | expect { cli.start }.to output(expected_string).to_stdout 49 | end 50 | 51 | it 'starts the droplet with a name' do 52 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 53 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 54 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 55 | 56 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 57 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 58 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 59 | 60 | stub_request(:post, 'https://api.digitalocean.com/v2/droplets/3164444/actions'). 61 | with(body: '{"type":"power_on"}', 62 | headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 63 | to_return(status: 200, body: fixture('droplet_start_response'), headers: {}) 64 | 65 | expected_string = <<-eos 66 | Droplet name provided. Finding droplet ID...done\e[0m, 3164444 (example3.com) 67 | Queuing start for 3164444 (example3.com)...Start complete! 68 | eos 69 | 70 | cli.options = cli.options.merge(name: 'example3.com') 71 | expect { cli.start }.to output(expected_string).to_stdout 72 | end 73 | 74 | it 'does not start a droplet that is inactive' do 75 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 76 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 77 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 78 | 79 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). 80 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 81 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 82 | 83 | expected_string = <<-eos 84 | Droplet name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) 85 | Droplet must be off for this operation to be successful. 86 | eos 87 | 88 | cli.options = cli.options.merge(name: 'example.com') 89 | expect { cli.start }.to raise_error(SystemExit).and output(expected_string).to_stdout 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/cli/verify_cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tugboat::CLI do 4 | include_context 'spec' 5 | 6 | describe 'verify' do 7 | it 'returns confirmation text when verify passes' do 8 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 9 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 10 | to_return(status: 200, body: fixture('show_droplets'), headers: {}) 11 | 12 | expect { cli.verify }.to output("Authentication with DigitalOcean was successful.\n").to_stdout 13 | expect(a_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1')).to have_been_made 14 | end 15 | 16 | it 'returns error when verify fails' do 17 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 18 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 19 | to_return(headers: { 'Content-Type' => 'text/html' }, status: 401, body: fixture('401')) 20 | 21 | expect { cli.verify }.to raise_error(SystemExit).and output(%r{Failed to connect to DigitalOcean. Reason given from API: unauthorized - Unable to authenticate you.}).to_stdout 22 | expect(a_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1')).to have_been_made 23 | end 24 | 25 | it 'returns error string when verify fails and a non-json reponse is given' do 26 | stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). 27 | with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). 28 | to_return(headers: { 'Content-Type' => 'text/html' }, status: 500, body: fixture('500', 'html')) 29 | 30 | expect { cli.verify }.to raise_error(SystemExit).and output(%r{
Oh no! It seems as though we've encountered a problem! Please try your request again.
57 |©2011-2013 DigitalOceanTM, Inc. All Rights Reserved.
64 |