From e70b871660e112e8dea01596782d8ab1b433560b Mon Sep 17 00:00:00 2001 From: Brian Cain Date: Mon, 25 Jun 2018 12:56:54 -0700 Subject: [PATCH] Introduce `cloud` command This commit adds a new command to Vagrant called `cloud`. It handles any and all interactions with the external service Vagrant Cloud. --- lib/vagrant.rb | 7 + plugins/commands/cloud/auth/login.rb | 91 +++++ plugins/commands/cloud/auth/logout.rb | 42 +++ plugins/commands/cloud/auth/plugin.rb | 20 ++ plugins/commands/cloud/auth/root.rb | 73 ++++ plugins/commands/cloud/auth/whoami.rb | 62 ++++ plugins/commands/cloud/box/create.rb | 74 ++++ plugins/commands/cloud/box/delete.rb | 65 ++++ plugins/commands/cloud/box/plugin.rb | 19 + plugins/commands/cloud/box/root.rb | 77 ++++ plugins/commands/cloud/box/show.rb | 73 ++++ plugins/commands/cloud/box/update.rb | 71 ++++ plugins/commands/cloud/client/client.rb | 258 +++++++++++++ plugins/commands/cloud/errors.rb | 24 ++ plugins/commands/cloud/list.rb | 52 +++ plugins/commands/cloud/locales/en.yml | 138 +++++++ plugins/commands/cloud/plugin.rb | 30 ++ plugins/commands/cloud/provider/create.rb | 72 ++++ plugins/commands/cloud/provider/delete.rb | 70 ++++ plugins/commands/cloud/provider/plugin.rb | 19 + plugins/commands/cloud/provider/root.rb | 77 ++++ plugins/commands/cloud/provider/update.rb | 72 ++++ plugins/commands/cloud/provider/upload.rb | 68 ++++ plugins/commands/cloud/publish.rb | 119 ++++++ plugins/commands/cloud/root.rb | 104 ++++++ plugins/commands/cloud/search.rb | 82 +++++ plugins/commands/cloud/util.rb | 199 ++++++++++ plugins/commands/cloud/version/create.rb | 68 ++++ plugins/commands/cloud/version/delete.rb | 68 ++++ plugins/commands/cloud/version/plugin.rb | 19 + plugins/commands/cloud/version/release.rb | 68 ++++ plugins/commands/cloud/version/revoke.rb | 68 ++++ plugins/commands/cloud/version/root.rb | 81 +++++ plugins/commands/cloud/version/update.rb | 68 ++++ .../plugins/commands/cloud/auth/login_test.rb | 103 ++++++ .../commands/cloud/auth/logout_test.rb | 43 +++ .../commands/cloud/auth/whoami_test.rb | 54 +++ .../plugins/commands/cloud/box/create_test.rb | 61 ++++ .../plugins/commands/cloud/box/delete_test.rb | 62 ++++ .../plugins/commands/cloud/box/show_test.rb | 63 ++++ .../plugins/commands/cloud/box/update_test.rb | 64 ++++ .../plugins/commands/cloud/client_test.rb | 261 ++++++++++++++ test/unit/plugins/commands/cloud/list_test.rb | 24 ++ .../commands/cloud/provider/create_test.rb | 85 +++++ .../commands/cloud/provider/delete_test.rb | 70 ++++ .../commands/cloud/provider/update_test.rb | 85 +++++ .../commands/cloud/provider/upload_test.rb | 70 ++++ .../plugins/commands/cloud/publish_test.rb | 80 +++++ .../plugins/commands/cloud/search_test.rb | 77 ++++ .../commands/cloud/version/create_test.rb | 65 ++++ .../commands/cloud/version/delete_test.rb | 66 ++++ .../commands/cloud/version/release_test.rb | 66 ++++ .../commands/cloud/version/revoke_test.rb | 66 ++++ .../commands/cloud/version/update_test.rb | 65 ++++ vagrant.gemspec | 1 + website/source/docs/cli/cloud.html.md | 339 ++++++++++++++++++ website/source/layouts/docs.erb | 1 + 57 files changed, 4369 insertions(+) create mode 100644 plugins/commands/cloud/auth/login.rb create mode 100644 plugins/commands/cloud/auth/logout.rb create mode 100644 plugins/commands/cloud/auth/plugin.rb create mode 100644 plugins/commands/cloud/auth/root.rb create mode 100644 plugins/commands/cloud/auth/whoami.rb create mode 100644 plugins/commands/cloud/box/create.rb create mode 100644 plugins/commands/cloud/box/delete.rb create mode 100644 plugins/commands/cloud/box/plugin.rb create mode 100644 plugins/commands/cloud/box/root.rb create mode 100644 plugins/commands/cloud/box/show.rb create mode 100644 plugins/commands/cloud/box/update.rb create mode 100644 plugins/commands/cloud/client/client.rb create mode 100644 plugins/commands/cloud/errors.rb create mode 100644 plugins/commands/cloud/list.rb create mode 100644 plugins/commands/cloud/locales/en.yml create mode 100644 plugins/commands/cloud/plugin.rb create mode 100644 plugins/commands/cloud/provider/create.rb create mode 100644 plugins/commands/cloud/provider/delete.rb create mode 100644 plugins/commands/cloud/provider/plugin.rb create mode 100644 plugins/commands/cloud/provider/root.rb create mode 100644 plugins/commands/cloud/provider/update.rb create mode 100644 plugins/commands/cloud/provider/upload.rb create mode 100644 plugins/commands/cloud/publish.rb create mode 100644 plugins/commands/cloud/root.rb create mode 100644 plugins/commands/cloud/search.rb create mode 100644 plugins/commands/cloud/util.rb create mode 100644 plugins/commands/cloud/version/create.rb create mode 100644 plugins/commands/cloud/version/delete.rb create mode 100644 plugins/commands/cloud/version/plugin.rb create mode 100644 plugins/commands/cloud/version/release.rb create mode 100644 plugins/commands/cloud/version/revoke.rb create mode 100644 plugins/commands/cloud/version/root.rb create mode 100644 plugins/commands/cloud/version/update.rb create mode 100644 test/unit/plugins/commands/cloud/auth/login_test.rb create mode 100644 test/unit/plugins/commands/cloud/auth/logout_test.rb create mode 100644 test/unit/plugins/commands/cloud/auth/whoami_test.rb create mode 100644 test/unit/plugins/commands/cloud/box/create_test.rb create mode 100644 test/unit/plugins/commands/cloud/box/delete_test.rb create mode 100644 test/unit/plugins/commands/cloud/box/show_test.rb create mode 100644 test/unit/plugins/commands/cloud/box/update_test.rb create mode 100644 test/unit/plugins/commands/cloud/client_test.rb create mode 100644 test/unit/plugins/commands/cloud/list_test.rb create mode 100644 test/unit/plugins/commands/cloud/provider/create_test.rb create mode 100644 test/unit/plugins/commands/cloud/provider/delete_test.rb create mode 100644 test/unit/plugins/commands/cloud/provider/update_test.rb create mode 100644 test/unit/plugins/commands/cloud/provider/upload_test.rb create mode 100644 test/unit/plugins/commands/cloud/publish_test.rb create mode 100644 test/unit/plugins/commands/cloud/search_test.rb create mode 100644 test/unit/plugins/commands/cloud/version/create_test.rb create mode 100644 test/unit/plugins/commands/cloud/version/delete_test.rb create mode 100644 test/unit/plugins/commands/cloud/version/release_test.rb create mode 100644 test/unit/plugins/commands/cloud/version/revoke_test.rb create mode 100644 test/unit/plugins/commands/cloud/version/update_test.rb create mode 100644 website/source/docs/cli/cloud.html.md diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 27a05de22..5df512049 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -61,6 +61,13 @@ if ENV["VAGRANT_LOG"] && ENV["VAGRANT_LOG"] != "" end Log4r::Outputter.stderr.formatter = Vagrant::Util::LoggingFormatter.new(base_formatter) logger = nil + + # Cloud gem uses RestClient to make HTTP requests, so + # log them if debug is enabled + if level == 1 + # TODO: Need to ensure token is marked sensitive, if possible here + ENV["RESTCLIENT_LOG"] = "stdout" + end end end diff --git a/plugins/commands/cloud/auth/login.rb b/plugins/commands/cloud/auth/login.rb new file mode 100644 index 000000000..4dc206bd2 --- /dev/null +++ b/plugins/commands/cloud/auth/login.rb @@ -0,0 +1,91 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module AuthCommand + module Command + class Login < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud auth login [options]" + o.separator "" + o.separator "Options:" + o.separator "" + o.on("-c", "--check", "Only checks if you're logged in") do |c| + options[:check] = c + end + + o.on("-d", "--description DESCRIPTION", String, "Description for the Vagrant Cloud token") do |d| + options[:description] = d + end + + o.on("-k", "--logout", "Logs you out if you're logged in") do |k| + options[:logout] = k + end + + o.on("-t", "--token TOKEN", String, "Set the Vagrant Cloud token") do |t| + options[:token] = t + end + + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |l| + options[:login] = l + end + end + # TODO: Should be an alias for the existing login command + + # Parse the options + argv = parse_options(opts) + return if !argv + + @client = Client.new(@env) + @client.username_or_email = options[:login] + + # Determine what task we're actually taking based on flags + if options[:check] + return execute_check + elsif options[:logout] + return execute_logout + elsif options[:token] + return execute_token(options[:token]) + else + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options) + end + + 0 + end + + def execute_check + if @client.logged_in? + @env.ui.success(I18n.t("cloud_command.check_logged_in")) + return 0 + else + @env.ui.error(I18n.t("cloud_command.check_not_logged_in")) + return 1 + end + end + + def execute_logout + @client.clear_token + @env.ui.success(I18n.t("cloud_command.logged_out")) + return 0 + end + + def execute_token(token) + @client.store_token(token) + @env.ui.success(I18n.t("cloud_command.token_saved")) + + if @client.logged_in? + @env.ui.success(I18n.t("cloud_command.check_logged_in")) + return 0 + else + @env.ui.error(I18n.t("cloud_command.invalid_token")) + return 1 + end + end + end + end + end + end +end diff --git a/plugins/commands/cloud/auth/logout.rb b/plugins/commands/cloud/auth/logout.rb new file mode 100644 index 000000000..19d7741ff --- /dev/null +++ b/plugins/commands/cloud/auth/logout.rb @@ -0,0 +1,42 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module AuthCommand + module Command + class Logout < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud auth logout [options]" + o.separator "" + o.separator "Logs you out if you're logged in locally." + o.separator "" + o.separator "Options:" + o.separator "" + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |l| + options[:login] = l + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.length > 1 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + # Initializes client and deletes token on disk + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + + @client.clear_token + @env.ui.success(I18n.t("cloud_command.logged_out")) + return 0 + end + end + end + end + end +end diff --git a/plugins/commands/cloud/auth/plugin.rb b/plugins/commands/cloud/auth/plugin.rb new file mode 100644 index 000000000..9cb82d5cd --- /dev/null +++ b/plugins/commands/cloud/auth/plugin.rb @@ -0,0 +1,20 @@ + +require "vagrant" + +module VagrantPlugins + module CloudCommand + module AuthCommand + class Plugin < Vagrant.plugin("2") + name "vagrant cloud auth" + description <<-DESC + Authorization commands for Vagrant Cloud + DESC + + command(:auth) do + require_relative "root" + Command::Root + end + end + end + end +end diff --git a/plugins/commands/cloud/auth/root.rb b/plugins/commands/cloud/auth/root.rb new file mode 100644 index 000000000..ab32c5d98 --- /dev/null +++ b/plugins/commands/cloud/auth/root.rb @@ -0,0 +1,73 @@ +module VagrantPlugins + module CloudCommand + module AuthCommand + module Command + class Root < Vagrant.plugin("2", :command) + def self.synopsis + "Manages everything authorization related to Vagrant Cloud" + end + + def initialize(argv, env) + super + + @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) + @subcommands = Vagrant::Registry.new + @subcommands.register(:login) do + require File.expand_path("../login", __FILE__) + Command::Login + end + @subcommands.register(:logout) do + require File.expand_path("../logout", __FILE__) + Command::Logout + end + @subcommands.register(:whoami) do + require File.expand_path("../whoami", __FILE__) + Command::Whoami + end + end + + def execute + if @main_args.include?("-h") || @main_args.include?("--help") + # Print the help for all the box commands. + return help + end + + # If we reached this far then we must have a subcommand. If not, + # then we also just print the help and exit. + command_class = @subcommands.get(@sub_command.to_sym) if @sub_command + return help if !command_class || !@sub_command + @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") + + # Initialize and execute the command class + command_class.new(@sub_args, @env).execute + end + + # Prints the help out for this command + def help + opts = OptionParser.new do |opts| + opts.banner = "Usage: vagrant cloud auth []" + opts.separator "" + opts.separator "Helper commands for authorization with Vagrant Cloud" + opts.separator "" + opts.separator "Available subcommands:" + + # Add the available subcommands as separators in order to print them + # out as well. + keys = [] + @subcommands.each { |key, value| keys << key.to_s } + + keys.sort.each do |key| + opts.separator " #{key}" + end + + opts.separator "" + opts.separator "For help on any individual subcommand run `vagrant cloud auth -h`" + end + + @env.ui.info(opts.help, prefix: false) + end + end + end + end + end +end diff --git a/plugins/commands/cloud/auth/whoami.rb b/plugins/commands/cloud/auth/whoami.rb new file mode 100644 index 000000000..81f174c00 --- /dev/null +++ b/plugins/commands/cloud/auth/whoami.rb @@ -0,0 +1,62 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module AuthCommand + module Command + class Whoami < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud auth whoami [options] [token]" + o.separator "" + o.separator "Determine who you are logged in as" + o.separator "" + o.separator "Options:" + o.separator "" + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |l| + options[:login] = l + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.size > 1 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + + if argv.first + token = argv.first + else + token = @client.token + end + + whoami(token, options[:username]) + end + + def whoami(access_token, username) + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(username, access_token, server_url) + + begin + success = account.validate_token + user = success["user"]["username"] + @env.ui.success("Currently logged in as #{user}") + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.whoami.read_error", org: username)) + @env.ui.error(e) + return 1 + end + return 1 + end + end + end + end + end +end diff --git a/plugins/commands/cloud/box/create.rb b/plugins/commands/cloud/box/create.rb new file mode 100644 index 000000000..a3e34b589 --- /dev/null +++ b/plugins/commands/cloud/box/create.rb @@ -0,0 +1,74 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module BoxCommand + module Command + class Create < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud box create [options] organization/box-name" + o.separator "" + o.separator "Creates an empty box entry on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-d", "--description DESCRIPTION", String, "Longer description of the box") do |d| + options[:description] = d + end + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + o.on("-s", "--short-description DESCRIPTION", String, "Short description of the box") do |s| + options[:short] = s + end + o.on("-p", "--private", "Makes box private") do |p| + options[:private] = p + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + + box = argv.first.split('/') + org = box[0] + box_name = box[1] + create_box(org, box_name, options, @client.token) + end + + # @param [String] - org + # @param [String] - box_name + # @param [Hash] - options + def create_box(org, box_name, options, access_token) + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(org, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, options[:short], options[:description], access_token) + + begin + success = box.create + @env.ui.success(I18n.t("cloud_command.box.create_success", org: org, box_name: box_name)) + VagrantPlugins::CloudCommand::Util.format_box_results(success.compact, @env) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.box.create_fail", org: org, box_name: box_name)) + @env.ui.error(e) + return 1 + end + + return 1 + end + end + end + end + end +end diff --git a/plugins/commands/cloud/box/delete.rb b/plugins/commands/cloud/box/delete.rb new file mode 100644 index 000000000..95a17c509 --- /dev/null +++ b/plugins/commands/cloud/box/delete.rb @@ -0,0 +1,65 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module BoxCommand + module Command + class Delete < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud box delete [options] organization/box-name" + o.separator "" + o.separator "Deletes box entry on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @env.ui.warn(I18n.t("cloud_command.box.delete_warn", box: argv.first)) + continue = @env.ui.ask(I18n.t("cloud_command.continue")) + return 1 if continue.downcase != "y" + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + + box = argv.first.split('/') + org = box[0] + box_name = box[1] + delete_box(org, box_name, options[:username], @client.token) + end + + def delete_box(org, box_name, username, access_token) + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(username, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + + begin + success = box.delete(org, box_name) + @env.ui.success(I18n.t("cloud_command.box.delete_success", org: org, box_name: box_name)) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.box.delete_fail", org: org, box_name: box_name)) + @env.ui.error(e) + return 1 + end + + return 1 + end + end + end + end + end +end diff --git a/plugins/commands/cloud/box/plugin.rb b/plugins/commands/cloud/box/plugin.rb new file mode 100644 index 000000000..2ef0ccfdc --- /dev/null +++ b/plugins/commands/cloud/box/plugin.rb @@ -0,0 +1,19 @@ +require "vagrant" + +module VagrantPlugins + module CloudCommand + module BoxCommand + class Plugin < Vagrant.plugin("2") + name "vagrant cloud box" + description <<-DESC + Box CRUD commands for Vagrant Cloud + DESC + + command(:box) do + require_relative "root" + Command::Root + end + end + end + end +end diff --git a/plugins/commands/cloud/box/root.rb b/plugins/commands/cloud/box/root.rb new file mode 100644 index 000000000..e687e5016 --- /dev/null +++ b/plugins/commands/cloud/box/root.rb @@ -0,0 +1,77 @@ +module VagrantPlugins + module CloudCommand + module BoxCommand + module Command + class Root < Vagrant.plugin("2", :command) + def self.synopsis + "Commands to manage boxes on Vagrant Cloud" + end + + def initialize(argv, env) + super + + @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) + @subcommands = Vagrant::Registry.new + @subcommands.register(:create) do + require File.expand_path("../create", __FILE__) + Command::Create + end + @subcommands.register(:delete) do + require File.expand_path("../delete", __FILE__) + Command::Delete + end + @subcommands.register(:show) do + require File.expand_path("../show", __FILE__) + Command::Show + end + @subcommands.register(:update) do + require File.expand_path("../update", __FILE__) + Command::Update + end + end + + def execute + if @main_args.include?("-h") || @main_args.include?("--help") + # Print the help for all the box commands. + return help + end + + # If we reached this far then we must have a subcommand. If not, + # then we also just print the help and exit. + command_class = @subcommands.get(@sub_command.to_sym) if @sub_command + return help if !command_class || !@sub_command + @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") + + # Initialize and execute the command class + command_class.new(@sub_args, @env).execute + end + + # Prints the help out for this command + def help + opts = OptionParser.new do |opts| + opts.banner = "Usage: vagrant cloud box []" + opts.separator "" + opts.separator "Commands to manage boxes on Vagrant Cloud" + opts.separator "" + opts.separator "Available subcommands:" + + # Add the available subcommands as separators in order to print them + # out as well. + keys = [] + @subcommands.each { |key, value| keys << key.to_s } + + keys.sort.each do |key| + opts.separator " #{key}" + end + + opts.separator "" + opts.separator "For help on any individual subcommand run `vagrant cloud box -h`" + end + + @env.ui.info(opts.help, prefix: false) + end + end + end + end + end +end diff --git a/plugins/commands/cloud/box/show.rb b/plugins/commands/cloud/box/show.rb new file mode 100644 index 000000000..0cf6bb2ee --- /dev/null +++ b/plugins/commands/cloud/box/show.rb @@ -0,0 +1,73 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module BoxCommand + module Command + class Show < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud box show [options] organization/box-name" + o.separator "" + o.separator "Displays a boxes attributes on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |u| + options[:username] = u + end + o.on("--versions VERSION", String, "Display box information for a specific version") do |v| + options[:version] = v + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + box = argv.first.split('/') + + show_box(box[0], box[1], options, @client.token) + end + + def show_box(org, box_name, options, access_token) + username = options[:username] + + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(username, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + + begin + success = box.read(org, box_name) + + if options[:version] + # show *this* version only + results = success["versions"].select{ |v| v if v["version"] == options[:version] }.first + if !results + @env.ui.warn(I18n.t("cloud_command.box.show_filter_empty", version: options[:version], org: org,box_name:box_name)) + return 0 + end + else + results = success + end + VagrantPlugins::CloudCommand::Util.format_box_results(results.compact, @env) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.box.show_fail", org: org,box_name:box_name)) + @env.ui.error(e) + return 1 + end + end + end + end + end + end +end diff --git a/plugins/commands/cloud/box/update.rb b/plugins/commands/cloud/box/update.rb new file mode 100644 index 000000000..0d766bd41 --- /dev/null +++ b/plugins/commands/cloud/box/update.rb @@ -0,0 +1,71 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module BoxCommand + module Command + class Update < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud box update [options] organization/box-name" + o.separator "" + o.separator "Updates a box entry on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + + + o.on("-d", "--description DESCRIPTION", "Longer desscription of the box") do |d| + options[:description] = d + end + o.on("-u", "--username", "The username of the organization that will own the box") do |u| + options[:username] = u + end + o.on("-s", "--short-description DESCRIPTION", "Short description of the box") do |s| + options[:short_description] = s + end + o.on("-p", "--private", "Makes box private") do |p| + options[:private] = p + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 || options.length == 0 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + box = argv.first.split('/') + + update_box(box[0], box[1], options, @client.token) + end + + def update_box(org, box_name, options, access_token) + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(options[:username], access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + + options[:organization] = org + options[:name] = box_name + begin + success = box.update(options) + @env.ui.success(I18n.t("cloud_command.box.update_success", org: org, box_name: box_name)) + VagrantPlugins::CloudCommand::Util.format_box_results(success.compact, @env) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.box.update_fail", org: org, box_name: box_name)) + @env.ui.error(e) + return 1 + end + return 1 + end + end + end + end + end +end diff --git a/plugins/commands/cloud/client/client.rb b/plugins/commands/cloud/client/client.rb new file mode 100644 index 000000000..599189ab5 --- /dev/null +++ b/plugins/commands/cloud/client/client.rb @@ -0,0 +1,258 @@ +require "rest_client" +require "vagrant_cloud" +require "vagrant/util/downloader" +require "vagrant/util/presence" +require Vagrant.source_root.join("plugins/commands/cloud/errors") + +module VagrantPlugins + module CloudCommand + class Client + ###################################################################### + # Class that deals with managing users 'local' token for Vagrant Cloud + ###################################################################### + APP = "app".freeze + + include Vagrant::Util::Presence + + attr_accessor :username_or_email + attr_accessor :password + attr_reader :two_factor_default_delivery_method + attr_reader :two_factor_delivery_methods + + # Initializes a login client with the given Vagrant::Environment. + # + # @param [Vagrant::Environment] env + def initialize(env) + @logger = Log4r::Logger.new("vagrant::cloud::client") + @env = env + end + + # Removes the token, effectively logging the user out. + def clear_token + @logger.info("Clearing token") + token_path.delete if token_path.file? + end + + # Checks if the user is logged in by verifying their authentication + # token. + # + # @return [Boolean] + def logged_in? + token = self.token + return false if !token + + with_error_handling do + url = "#{Vagrant.server_url}/api/v1/authenticate" + + "?access_token=#{token}" + RestClient.get(url, content_type: :json) + true + end + rescue Errors::Unauthorized + false + end + + # Login logs a user in and returns the token for that user. The token + # is _not_ stored unless {#store_token} is called. + # + # @param [String] description + # @param [String] code + # @return [String] token The access token, or nil if auth failed. + def login(description: nil, code: nil) + @logger.info("Logging in '#{username_or_email}'") + + response = post( + "/api/v1/authenticate", { + user: { + login: username_or_email, + password: password + }, + token: { + description: description + }, + two_factor: { + code: code + } + } + ) + + response["token"] + end + + # Requests a 2FA code + # @param [String] delivery_method + def request_code(delivery_method) + @env.ui.warn("Requesting 2FA code via #{delivery_method.upcase}...") + + response = post( + "/api/v1/two-factor/request-code", { + user: { + login: username_or_email, + password: password + }, + two_factor: { + delivery_method: delivery_method.downcase + } + } + ) + + two_factor = response['two_factor'] + obfuscated_destination = two_factor['obfuscated_destination'] + + @env.ui.success("2FA code sent to #{obfuscated_destination}.") + end + + # Issues a post to a Vagrant Cloud path with the given payload. + # @param [String] path + # @param [Hash] payload + # @return [Hash] response data + def post(path, payload) + with_error_handling do + url = File.join(Vagrant.server_url, path) + + proxy = nil + proxy ||= ENV["HTTPS_PROXY"] || ENV["https_proxy"] + proxy ||= ENV["HTTP_PROXY"] || ENV["http_proxy"] + RestClient.proxy = proxy + + response = RestClient::Request.execute( + method: :post, + url: url, + payload: JSON.dump(payload), + proxy: proxy, + headers: { + accept: :json, + content_type: :json, + user_agent: Vagrant::Util::Downloader::USER_AGENT, + }, + ) + + JSON.load(response.to_s) + end + end + + # Stores the given token locally, removing any previous tokens. + # + # @param [String] token + def store_token(token) + @logger.info("Storing token in #{token_path}") + + token_path.open("w") do |f| + f.write(token) + end + + nil + end + + # Reads the access token if there is one. This will first read the + # `VAGRANT_CLOUD_TOKEN` environment variable and then fallback to the stored + # access token on disk. + # + # @return [String] + def token + if present?(ENV["VAGRANT_CLOUD_TOKEN"]) && token_path.exist? + @env.ui.warn <<-EOH.strip +Vagrant detected both the VAGRANT_CLOUD_TOKEN environment variable and a Vagrant login +token are present on this system. The VAGRANT_CLOUD_TOKEN environment variable takes +precedence over the locally stored token. To remove this error, either unset +the VAGRANT_CLOUD_TOKEN environment variable or remove the login token stored on disk: + + ~/.vagrant.d/data/vagrant_login_token + +EOH + end + + if present?(ENV["VAGRANT_CLOUD_TOKEN"]) + @logger.debug("Using authentication token from environment variable") + return ENV["VAGRANT_CLOUD_TOKEN"] + end + + if token_path.exist? + @logger.debug("Using authentication token from disk at #{token_path}") + return token_path.read.strip + end + + if present?(ENV["ATLAS_TOKEN"]) + @logger.warn("ATLAS_TOKEN detected within environment. Using ATLAS_TOKEN in place of VAGRANT_CLOUD_TOKEN.") + return ENV["ATLAS_TOKEN"] + end + + @logger.debug("No authentication token in environment or #{token_path}") + + nil + end + + protected + + def with_error_handling(&block) + yield + rescue RestClient::Unauthorized + @logger.debug("Unauthorized!") + raise Errors::Unauthorized + rescue RestClient::BadRequest => e + @logger.debug("Bad request:") + @logger.debug(e.message) + @logger.debug(e.backtrace.join("\n")) + parsed_response = JSON.parse(e.response) + errors = parsed_response["errors"].join("\n") + raise Errors::ServerError, errors: errors + rescue RestClient::NotAcceptable => e + @logger.debug("Got unacceptable response:") + @logger.debug(e.message) + @logger.debug(e.backtrace.join("\n")) + + parsed_response = JSON.parse(e.response) + + if two_factor = parsed_response['two_factor'] + store_two_factor_information two_factor + + if two_factor_default_delivery_method != APP + request_code two_factor_default_delivery_method + end + + raise Errors::TwoFactorRequired + end + + begin + errors = parsed_response["errors"].join("\n") + raise Errors::ServerError, errors: errors + rescue JSON::ParserError; end + + raise "An unexpected error occurred: #{e.inspect}" + rescue SocketError + @logger.info("Socket error") + raise Errors::ServerUnreachable, url: Vagrant.server_url.to_s + end + + def token_path + @env.data_dir.join("vagrant_login_token") + end + + def store_two_factor_information(two_factor) + @two_factor_default_delivery_method = + two_factor['default_delivery_method'] + + @two_factor_delivery_methods = + two_factor['delivery_methods'] + + @env.ui.warn "2FA is enabled for your account." + if two_factor_default_delivery_method == APP + @env.ui.info "Enter the code from your authenticator." + else + @env.ui.info "Default method is " \ + "'#{two_factor_default_delivery_method}'." + end + + other_delivery_methods = + two_factor_delivery_methods - [APP] + + if other_delivery_methods.any? + other_delivery_methods_sentence = other_delivery_methods + .map { |word| "'#{word}'" } + .join(' or ') + @env.ui.info "You can also type #{other_delivery_methods_sentence} " \ + "to request a new code." + end + end + end + end +end diff --git a/plugins/commands/cloud/errors.rb b/plugins/commands/cloud/errors.rb new file mode 100644 index 000000000..1ba5652b2 --- /dev/null +++ b/plugins/commands/cloud/errors.rb @@ -0,0 +1,24 @@ +module VagrantPlugins + module CloudCommand + module Errors + class Error < Vagrant::Errors::VagrantError + error_namespace("cloud_command.errors") + end + + class ServerError < Error + error_key(:server_error) + end + + class ServerUnreachable < Error + error_key(:server_unreachable) + end + + class Unauthorized < Error + error_key(:unauthorized) + end + + class TwoFactorRequired < Error + end + end + end +end diff --git a/plugins/commands/cloud/list.rb b/plugins/commands/cloud/list.rb new file mode 100644 index 000000000..2344216bd --- /dev/null +++ b/plugins/commands/cloud/list.rb @@ -0,0 +1,52 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module Command + class List < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud list [options] organization" + o.separator "" + o.separator "Search for boxes managed by a specific user" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-j", "--json", "Formats results in JSON") do |j| + options[:check] = j + end + o.on("-l", "--limit", Integer, "Max number of search results (default is 25)") do |l| + options[:check] = l + end + o.on("-p", "--provider", "Comma separated list of providers to filter search on. Defaults to all.") do |p| + options[:check] = p + end + o.on("-s", "--sort-by", "Column to sort list (created, downloads, updated)") do |s| + options[:check] = s + end + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + + # TODO: This endpoint is not implemented yet + + 0 + end + end + end + end +end diff --git a/plugins/commands/cloud/locales/en.yml b/plugins/commands/cloud/locales/en.yml new file mode 100644 index 000000000..bdd85f355 --- /dev/null +++ b/plugins/commands/cloud/locales/en.yml @@ -0,0 +1,138 @@ +en: + cloud_command: + publish: + box_create: + Creating a box entry... + version_create: + Creating a version entry... + provider_create: + Creating a provider entry... + upload_provider: + Uploading provider with file %{file} + release: + Releasing box... + complete: + Complete! Published %{org}/%{box_name} + continue: |- + Do you wish to continue? [y/N] + box: + show_filter_empty: |- + No version matched %{version} for %{org}/%{box_name} + create_success: |- + Created box %{org}/%{box_name} + delete_success: |- + Deleted box %{org}/%{box_name} + delete_warn: |- + This will completely remove %{box} from Vagrant Cloud. This cannot be undone. + update_success: |- + Updated box %{org}/%{box_name} + search: + no_results: |- + No results found for %{query} + upload: + no_url: |- + No URL was provided to upload the provider + You will need to run the `vagrant cloud provider upload` command to provide a box + provider: + upload: |- + Uploading provider %{provider_file} ... + upload_success: |- + Uploaded provider %{provider} on %{org}/%{box_name} for version %{version} + delete_warn: |- + This will completely remove provider %{provider} on version %{version} from %{box} on Vagrant Cloud. This cannot be undone. + create_success: |- + Created provider %{provider} on %{org}/%{box_name} for version %{version} + delete_success: |- + Deleted provider %{provider} on %{org}/%{box_name} for version %{version} + update_success: |- + Updated provider %{provider} on %{org}/%{box_name} for version %{version} + version: + create_success: |- + Created version %{version} on %{org}/%{box_name} for version %{version} + delete_success: |- + Deleted version %{version} on %{org}/%{box_name} + release_success: |- + Released version %{version} on %{org}/%{box_name} + revoke_success: |- + Revoked version %{version} on %{org}/%{box_name} + update_success: |- + Updated version %{version} on %{org}/%{box_name} + revoke_warn: |- + This will revoke version %{version} from %{box} from Vagrant Cloud. This cannot be undone. + release_warn: |- + This will release version %{version} from %{box} to Vagrant Cloud and be available to download. + delete_warn: |- + This will completely remove version %{version} from %{box} from Vagrant Cloud. This cannot be undone. + errors: + search: + fail: |- + Could not complete search request + publish: + fail: |- + Failed to create box %{org}/%{box_name} + box: + create_fail: |- + Failed to create box %{org}/%{box_name} + delete_fail: |- + Failed to delete box %{org}/%{box_name} + show_fail: |- + Could not get information about box %{org}/%{box_name} + update_fail: |- + Failed to update box %{org}/%{box_name} + whoami: + read_error: |- + Failed to read organization %{org} + provider: + create_fail: |- + Failed to create provider %{provider} on box %{org}/%{box_name} for version %{version} + update_fail: |- + Failed to update provider %{provider} on box %{org}/%{box_name} for version %{version} + delete_fail: |- + Failed to delete provider %{provider} on box %{org}/%{box_name} for version %{version} + upload_fail: |- + Failed to upload provider %{provider} on box %{org}/%{box_name} for version %{version} + version: + create_fail: |- + Failed to create version %{version} on box %{org}/%{box_name} + delete_fail: |- + Failed to delete version %{version} on box %{org}/%{box_name} + release_fail: |- + Failed to release version %{version} on box %{org}/%{box_name} + revoke_fail: |- + Failed to revoke version %{version} on box %{org}/%{box_name} + update_fail: |- + Failed to update version %{version} on box %{org}/%{box_name} + server_error: |- + The Vagrant Cloud server responded with a not-OK response: + + %{errors} + server_unreachable: |- + The Vagrant Cloud server is not currently accepting connections. Please check + your network connection and try again later. + + unauthorized: |- + Invalid username or password. Please try again. + + check_logged_in: |- + You are already logged in. + check_not_logged_in: |- + You are not currently logged in. Please run `vagrant login` and provide + your login information to authenticate. + command_header: |- + In a moment we will ask for your username and password to HashiCorp's + Vagrant Cloud. After authenticating, we will store an access token locally on + disk. Your login details will be transmitted over a secure connection, and + are never stored on disk locally. + + If you do not have an Vagrant Cloud account, sign up at + https://www.vagrantcloud.com + invalid_login: |- + Invalid username or password. Please try again. + invalid_token: |- + Invalid token. Please try again. + logged_in: |- + You are now logged in. + logged_out: |- + You are logged out. + token_saved: |- + The token was successfully saved. diff --git a/plugins/commands/cloud/plugin.rb b/plugins/commands/cloud/plugin.rb new file mode 100644 index 000000000..5ed5b5408 --- /dev/null +++ b/plugins/commands/cloud/plugin.rb @@ -0,0 +1,30 @@ +require "vagrant" +require 'vagrant_cloud' +require Vagrant.source_root.join("plugins/commands/cloud/util") +require Vagrant.source_root.join("plugins/commands/cloud/client/client") + +module VagrantPlugins + module CloudCommand + class Plugin < Vagrant.plugin("2") + name "vagrant-cloud" + description <<-DESC + Provides the cloud command and internal API access to Vagrant Cloud. + DESC + + command(:cloud) do + require_relative "root" + init! + Command::Root + end + + protected + + def self.init! + return if defined?(@_init) + I18n.load_path << File.expand_path("../locales/en.yml", __FILE__) + I18n.reload! + @_init = true + end + end + end +end diff --git a/plugins/commands/cloud/provider/create.rb b/plugins/commands/cloud/provider/create.rb new file mode 100644 index 000000000..646370f70 --- /dev/null +++ b/plugins/commands/cloud/provider/create.rb @@ -0,0 +1,72 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module ProviderCommand + module Command + class Create < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud provider create [options] organization/box-name provider-name version [url]" + o.separator "" + o.separator "Creates a provider entry on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 4 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + + box = argv.first.split('/') + org = box[0] + box_name = box[1] + provider_name = argv[1] + version = argv[2] + url = argv[3] + + upload_provider(org, box_name, provider_name, version, url, @client.token, options) + end + + def upload_provider(org, box_name, provider_name, version, url, access_token, options) + if !url + @env.ui.warn(I18n.t("cloud_command.upload.no_url")) + end + + org = options[:username] if options[:username] + + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(org, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + cloud_version = VagrantCloud::Version.new(box, version, nil, nil, access_token) + provider = VagrantCloud::Provider.new(cloud_version, provider_name, nil, url, org, box_name, access_token) + + begin + success = provider.create_provider + @env.ui.success(I18n.t("cloud_command.provider.create_success", provider:provider_name, org: org, box_name: box_name, version: version)) + VagrantPlugins::CloudCommand::Util.format_box_results(success.compact, @env) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.provider.create_fail", provider:provider_name, org: org, box_name: box_name, version: version)) + @env.ui.error(e) + return 1 + end + end + end + end + end + end +end diff --git a/plugins/commands/cloud/provider/delete.rb b/plugins/commands/cloud/provider/delete.rb new file mode 100644 index 000000000..625d01fd1 --- /dev/null +++ b/plugins/commands/cloud/provider/delete.rb @@ -0,0 +1,70 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module ProviderCommand + module Command + class Delete < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud provider delete [options] organization/box-name provider-name version" + o.separator "" + o.separator "Deletes a provider entry on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 3 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + box = argv.first.split('/') + org = box[0] + box_name = box[1] + provider_name = argv[1] + version = argv[2] + + @env.ui.warn(I18n.t("cloud_command.provider.delete_warn", provider: provider_name, version:version, box: argv.first)) + continue = @env.ui.ask(I18n.t("cloud_command.continue")) + return 1 if continue.downcase != "y" + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + + delete_provider(org, box_name, provider_name, version, @client.token, options) + end + + def delete_provider(org, box_name, provider_name, version, access_token, options) + org = options[:username] if options[:username] + + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(org, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + cloud_version = VagrantCloud::Version.new(box, version, nil, nil, access_token) + provider = VagrantCloud::Provider.new(cloud_version, provider_name, nil, nil, nil, nil, access_token) + + begin + success = provider.delete + @env.ui.error(I18n.t("cloud_command.provider.delete_success", provider: provider_name, org: org, box_name: box_name, version: version)) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.provider.delete_fail", provider: provider_name, org: org, box_name: box_name, version: version)) + @env.ui.error(e) + return 1 + end + end + end + end + end + end +end diff --git a/plugins/commands/cloud/provider/plugin.rb b/plugins/commands/cloud/provider/plugin.rb new file mode 100644 index 000000000..b2c536587 --- /dev/null +++ b/plugins/commands/cloud/provider/plugin.rb @@ -0,0 +1,19 @@ +require "vagrant" + +module VagrantPlugins + module CloudCommand + module ProviderCommand + class Plugin < Vagrant.plugin("2") + name "vagrant cloud box" + description <<-DESC + Provider CRUD commands for Vagrant Cloud + DESC + + command(:provider) do + require_relative "root" + Command::Root + end + end + end + end +end diff --git a/plugins/commands/cloud/provider/root.rb b/plugins/commands/cloud/provider/root.rb new file mode 100644 index 000000000..3f09d4266 --- /dev/null +++ b/plugins/commands/cloud/provider/root.rb @@ -0,0 +1,77 @@ +module VagrantPlugins + module CloudCommand + module ProviderCommand + module Command + class Root < Vagrant.plugin("2", :command) + def self.synopsis + "Provider commands" + end + + def initialize(argv, env) + super + + @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) + @subcommands = Vagrant::Registry.new + @subcommands.register(:create) do + require File.expand_path("../create", __FILE__) + Command::Create + end + @subcommands.register(:delete) do + require File.expand_path("../delete", __FILE__) + Command::Delete + end + @subcommands.register(:update) do + require File.expand_path("../update", __FILE__) + Command::Update + end + @subcommands.register(:upload) do + require File.expand_path("../upload", __FILE__) + Command::Upload + end + end + + def execute + if @main_args.include?("-h") || @main_args.include?("--help") + # Print the help for all the provider commands. + return help + end + + # If we reached this far then we must have a subcommand. If not, + # then we also just print the help and exit. + command_class = @subcommands.get(@sub_command.to_sym) if @sub_command + return help if !command_class || !@sub_command + @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") + + # Initialize and execute the command class + command_class.new(@sub_args, @env).execute + end + + # Prints the help out for this command + def help + opts = OptionParser.new do |opts| + opts.banner = "Usage: vagrant cloud provider []" + opts.separator "" + opts.separator "For various provider actions with Vagrant Cloud" + opts.separator "" + opts.separator "Available subcommands:" + + # Add the available subcommands as separators in order to print them + # out as well. + keys = [] + @subcommands.each { |key, value| keys << key.to_s } + + keys.sort.each do |key| + opts.separator " #{key}" + end + + opts.separator "" + opts.separator "For help on any individual subcommand run `vagrant cloud provider -h`" + end + + @env.ui.info(opts.help, prefix: false) + end + end + end + end + end +end diff --git a/plugins/commands/cloud/provider/update.rb b/plugins/commands/cloud/provider/update.rb new file mode 100644 index 000000000..dae22f9d2 --- /dev/null +++ b/plugins/commands/cloud/provider/update.rb @@ -0,0 +1,72 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module ProviderCommand + module Command + class Update < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud provider update [options] organization/box-name provider-name version url" + o.separator "" + o.separator "Updates a provider entry on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 4 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + + box = argv.first.split('/') + org = box[0] + box_name = box[1] + provider_name = argv[1] + version = argv[2] + url = argv[3] + + update_provider(org, box_name, provider_name, version, url, @client.token, options) + end + + def update_provider(org, box_name, provider_name, version, url, access_token, options) + if !url + @env.ui.warn(I18n.t("cloud_command.upload.no_url")) + end + + org = options[:username] if options[:username] + + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(org, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + cloud_version = VagrantCloud::Version.new(box, version, nil, nil, access_token) + provider = VagrantCloud::Provider.new(cloud_version, provider_name, nil, url, org, box_name, access_token) + + begin + success = provider.update + @env.ui.success(I18n.t("cloud_command.provider.update_success", provider:provider_name, org: org, box_name: box_name, version: version)) + VagrantPlugins::CloudCommand::Util.format_box_results(success.compact, @env) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.provider.update_fail", provider:provider_name, org: org, box_name: box_name, version: version)) + @env.ui.error(e) + return 1 + end + end + end + end + end + end +end diff --git a/plugins/commands/cloud/provider/upload.rb b/plugins/commands/cloud/provider/upload.rb new file mode 100644 index 000000000..8d25a199a --- /dev/null +++ b/plugins/commands/cloud/provider/upload.rb @@ -0,0 +1,68 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module ProviderCommand + module Command + class Upload < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud provider upload [options] organization/box-name provider-name version box-file" + o.separator "" + o.separator "Uploads a box file to Vagrant Cloud for a specific provider" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 4 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + + box = argv.first.split('/') + org = box[0] + box_name = box[1] + provider_name = argv[1] + version = argv[2] + file = argv[3] + + upload_provider(org, box_name, provider_name, version, file, @client.token, options) + end + + def upload_provider(org, box_name, provider_name, version, file, access_token, options) + org = options[:username] if options[:username] + + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(org, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + cloud_version = VagrantCloud::Version.new(box, version, nil, nil, access_token) + provider = VagrantCloud::Provider.new(cloud_version, provider_name, nil, nil, org, box_name, access_token) + + begin + @env.ui.info(I18n.t("cloud_command.provider.upload", provider_file: file)) + success = provider.upload_file(file) + @env.ui.success(I18n.t("cloud_command.provider.upload_success", provider: provider_name, org: org, box_name: box_name, version: version)) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.provider.upload_fail", provider: provider_name, org: org, box_name: box_name, version: version)) + @env.ui.error(e) + return 1 + end + end + end + end + end + end +end diff --git a/plugins/commands/cloud/publish.rb b/plugins/commands/cloud/publish.rb new file mode 100644 index 000000000..e598f8f61 --- /dev/null +++ b/plugins/commands/cloud/publish.rb @@ -0,0 +1,119 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module Command + class Publish < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud publish [options] organization/box-name version provider-name [provider-file]" + o.separator "" + o.separator "A Start-To-Finish command for creating and releasing a new Vagrant Box on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("--box-version VERSION", String, "Version of box to create") do |v| + options[:box_version] = v + end + o.on("--url", String, "Valid remote URL to download this provider") do |u| + options[:url] = u + end + o.on("-d", "--description DESCRIPTION", String, "Longer description of box") do |d| + options[:description] = d + end + o.on("--version-description DESCRIPTION", String, "Description of the version to create") do |v| + options[:version_description] = v + end + o.on("-f", "--force", "Disables confirmation to create or update box") do |f| + options[:force] = f + end + o.on("-p", "--private", "Makes box private") do |p| + options[:private] = p + end + o.on("-r", "--release", "Releases box") do |p| + options[:release] = p + end + o.on("-s", "--short-description DESCRIPTION", String, "Short description of the box") do |s| + options[:short_description] = s + end + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 4 || argv.length < 4 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + + box = argv.first.split('/') + org = box[0] + box_name = box[1] + version = argv[1] + provider_name = argv[2] + box_file = argv[3] + publish_box(org, box_name, version, provider_name, box_file, options, @client.token) + end + + def publish_box(org, box_name, version, provider_name, box_file, options, access_token) + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + + @env.ui.warn("You are about to create a box on Vagrant Cloud with the following options:\n") + box_opts = " #{org}/#{box_name} (#{version}) for #{provider_name}\n" + box_opts << " Private: true\n" if options[:private] + box_opts << " Automatic Release: true\n" if options[:release] + box_opts << " Remote Box file: true\n" if options[:url] + box_opts << " Box Description: #{options[:description]}\n" if options[:description] + box_opts << " Box Short Description: #{options[:short_description]}\n" if options[:short_description] + box_opts << " Version Description: #{options[:version_description]}\n" if options[:version_description] + + @env.ui.info(box_opts) + + if !options[:force] + continue = @env.ui.ask(I18n.t("cloud_command.continue")) + return 1 if continue.downcase != "y" + end + + account = VagrantPlugins::CloudCommand::Util.account?(org, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, options[:short_description], options[:description], access_token) + cloud_version = VagrantCloud::Version.new(box, version, nil, options[:version_description], access_token) + provider = VagrantCloud::Provider.new(cloud_version, provider_name, nil, options[:url], org, box_name, access_token) + + begin + @env.ui.info(I18n.t("cloud_command.publish.box_create")) + box.create + @env.ui.info(I18n.t("cloud_command.publish.version_create")) + cloud_version.create_version + @env.ui.info(I18n.t("cloud_command.publish.provider_create")) + provider.create_provider + if !options[:url] + @env.ui.info(I18n.t("cloud_command.publish.upload_provider", file: box_file)) + provider.upload_file(box_file) + end + if options[:release] + @env.ui.info(I18n.t("cloud_command.publish.release")) + cloud_version.release + end + @env.ui.success(I18n.t("cloud_command.publish.complete", org: org, box_name: box_name)) + success = box.read(org, box_name) + VagrantPlugins::CloudCommand::Util.format_box_results(success.compact, @env) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.publish.fail", org: org, box_name: box_name)) + @env.ui.error(e) + return 1 + end + return 1 + end + end + end + end +end diff --git a/plugins/commands/cloud/root.rb b/plugins/commands/cloud/root.rb new file mode 100644 index 000000000..a846584f3 --- /dev/null +++ b/plugins/commands/cloud/root.rb @@ -0,0 +1,104 @@ +module VagrantPlugins + module CloudCommand + module Command + class Root < Vagrant.plugin("2", :command) + def self.synopsis + "manages everything related to Vagrant Cloud" + end + + def initialize(argv, env) + super + + @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) + @subcommands = Vagrant::Registry.new + @subcommand_helptext = {} + + @subcommands.register(:auth) do + require File.expand_path("../auth/root", __FILE__) + AuthCommand::Command::Root + end + @subcommand_helptext[:auth] = "For various authorization operations on Vagrant Cloud" + + @subcommands.register(:box) do + require File.expand_path("../box/root", __FILE__) + BoxCommand::Command::Root + end + @subcommand_helptext[:box] = "For managing a Vagrant box entry on Vagrant Cloud" + + # TODO: Uncomment this when API endpoint exists + #@subcommands.register(:list) do + # require File.expand_path("../list", __FILE__) + # List + #end + #@subcommand_helptext[:list] = "Displays a list of Vagrant boxes that the current user manages" + + @subcommands.register(:search) do + require File.expand_path("../search", __FILE__) + Search + end + @subcommand_helptext[:search] = "Search Vagrant Cloud for available boxes" + + @subcommands.register(:provider) do + require File.expand_path("../provider/root", __FILE__) + ProviderCommand::Command::Root + end + @subcommand_helptext[:provider] = "For managing a Vagrant box's provider options" + + @subcommands.register(:publish) do + require File.expand_path("../publish", __FILE__) + Publish + end + @subcommand_helptext[:publish] = "A start-to-finish solution for creating or updating a new box on Vagrant Cloud" + + @subcommands.register(:version) do + require File.expand_path("../version/root", __FILE__) + VersionCommand::Command::Root + end + @subcommand_helptext[:version] = "For managing a Vagrant box's versions" + end + + def execute + if @main_args.include?("-h") || @main_args.include?("--help") + # Print the help for all the box commands. + return help + end + + # If we reached this far then we must have a subcommand. If not, + # then we also just print the help and exit. + command_class = @subcommands.get(@sub_command.to_sym) if @sub_command + return help if !command_class || !@sub_command + @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") + + # Initialize and execute the command class + command_class.new(@sub_args, @env).execute + end + + # Prints the help out for this command + def help + opts = OptionParser.new do |opts| + opts.banner = "Usage: vagrant cloud []" + opts.separator "" + opts.separator "The cloud command can be used for taking actions against" + opts.separator "Vagrant Cloud like searching or uploading a Vagrant Box" + opts.separator "" + opts.separator "Available subcommands:" + + # Add the available subcommands as separators in order to print them + # out as well. + keys = [] + @subcommands.each { |key, value| keys << key.to_s } + + keys.sort.each do |key| + opts.separator " #{key.ljust(15)} #{@subcommand_helptext[key.to_sym]}" + end + + opts.separator "" + opts.separator "For help on any individual subcommand run `vagrant cloud -h`" + end + + @env.ui.info(opts.help, prefix: false) + end + end + end + end +end diff --git a/plugins/commands/cloud/search.rb b/plugins/commands/cloud/search.rb new file mode 100644 index 000000000..b822da2b3 --- /dev/null +++ b/plugins/commands/cloud/search.rb @@ -0,0 +1,82 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module Command + class Search < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud search [options] query" + o.separator "" + o.separator "Search for a box on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-j", "--json", "Formats results in JSON") do |j| + options[:json] = j + end + o.on("-p", "--page PAGE", Integer, "The page to display Default: 1") do |j| + options[:page] = j + end + o.on("-s", "--short", "Shows a simple list of box names") do |s| + options[:short] = s + end + o.on("-o", "--order ORDER", String, "Order to display results ('desc' or 'asc') Default: 'desc'") do |o| + options[:order] = o + end + o.on("-l", "--limit LIMIT", Integer, "Max number of search results Default: 25") do |l| + options[:limit] = l + end + o.on("-p", "--provider PROVIDER", String, "Filter search results to a single provider. Defaults to all.") do |p| + options[:provider] = p + end + o.on("--sort-by SORT", "Field to sort results on (created, downloads, updated) Default: downloads") do |s| + options[:sort] = s + end + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + query = argv.first + + options[:limit] = 25 if !options[:limit] + + search(query, options, @client.token) + end + + def search(query, options, access_token) + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + search = VagrantCloud::Search.new(access_token, server_url) + + begin + search_results = search.search(query, options[:provider], options[:sort], options[:order], options[:limit], options[:page]) + if !search_results["boxes"].empty? + VagrantPlugins::CloudCommand::Util.format_search_results(search_results["boxes"], options[:short], options[:json], @env) + else + @env.ui.warn(I18n.t("cloud_command.search.no_results", query: query)) + end + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.search.fail")) + @env.ui.error(e) + return 1 + end + return 1 + end + end + end + end +end diff --git a/plugins/commands/cloud/util.rb b/plugins/commands/cloud/util.rb new file mode 100644 index 000000000..149b37d89 --- /dev/null +++ b/plugins/commands/cloud/util.rb @@ -0,0 +1,199 @@ +module VagrantPlugins + module CloudCommand + class Util + class << self + def account?(username, access_token, vagrant_cloud_server) + if !defined?(@_account) + @_account = VagrantCloud::Account.new(username, access_token, vagrant_cloud_server) + end + @_account + end + + def api_server_url + if Vagrant.server_url == Vagrant::DEFAULT_SERVER_URL + return "#{Vagrant.server_url}/api/v1" + else + return Vagrant.server_url + end + end + + def client_login(env, options) + if !defined?(@_client) + @_client = Client.new(env) + return @_client if @_client.logged_in? + + # Let the user know what is going on. + env.ui.output(I18n.t("cloud_command.command_header") + "\n") + + # If it is a private cloud installation, show that + if Vagrant.server_url != Vagrant::DEFAULT_SERVER_URL + env.ui.output("Vagrant Cloud URL: #{Vagrant.server_url}") + end + + # Ask for the username + if @_client.username_or_email + env.ui.output("Vagrant Cloud username or email: #{@_client.username_or_email}") + end + until @_client.username_or_email + @_client.username_or_email = env.ui.ask("Vagrant Cloud username or email: ") + end + + until @_client.password + @_client.password = env.ui.ask("Password (will be hidden): ", echo: false) + end + + if options + description = options[:description] + end + description_default = "Vagrant login from #{Socket.gethostname}" + until description + description = + env.ui.ask("Token description (Defaults to #{description_default.inspect}): ") + end + description = description_default if description.empty? + + code = nil + + begin + token = @_client.login(description: description, code: code) + rescue Errors::TwoFactorRequired + until code + code = env.ui.ask("2FA code: ") + + if @_client.two_factor_delivery_methods.include?(code.downcase) + delivery_method, code = code, nil + @_client.request_code delivery_method + end + end + + retry + end + + @_client.store_token(token) + env.ui.success(I18n.t("cloud_command.logged_in")) + @_client + end + @_client + end + + # =================================================== + # Modified from https://stackoverflow.com/a/28685559 + # for printing arrays of hashes in formatted tables + # =================================================== + + # @param [Vagrant::Environment] - env + # @param [Hash] - column_labels - A hash of key values for table labels (i.e. {:col1=>"COL1", :col2=>"COL2"}) + # @param [Array] - results - An array of hashes + # @param [Array] - to_jrust_keys - An array of column keys that should be right justified (default is left justified for all columns) + def print_search_table(env, column_labels, results, to_rjust_keys) + columns = column_labels.each_with_object({}) { |(col,label),h| + h[col] = { label: label, + width: [results.map { |g| g[col].size }.max, label.size].max + }} + + write_header(env, columns) + write_divider(env, columns) + results.each { |h| write_line(env, columns, h,to_rjust_keys) } + write_divider(env, columns) + end + + def write_header(env, columns) + env.ui.info "| #{ columns.map { |_,g| g[:label].ljust(g[:width]) }.join(' | ') } |" + end + + def write_divider(env, columns) + env.ui.info "+-#{ columns.map { |_,g| "-"*g[:width] }.join("-+-") }-+" + end + + def write_line(env, columns,h,to_rjust_keys) + str = h.keys.map { |k| + if to_rjust_keys.include?(k) + h[k].rjust(columns[k][:width]) + else + h[k].ljust(columns[k][:width]) + end + }.join(" | ") + env.ui.info "| #{str} |" + end + + # =================================================== + # =================================================== + + # Takes a "mostly" flat key=>value hash from Vagrant Cloud + # and prints its results in a list + # + # @param [Hash] - results - A response hash from vagrant cloud + # @param [Vagrant::Environment] - env + def format_box_results(results, env) + # TODO: remove other description fields? Maybe leave "short"? + results.delete("description_html") + + if results["current_version"] + versions = results.delete("versions") + results["providers"] = results["current_version"]["providers"] + + results["old_versions"] = versions.map{ |v| v["version"] }[1..5].join(", ") + "..." + end + + + width = results.keys.map{|k| k.size}.max + results.each do |k,v| + if k == "versions" + v = v.map{ |ver| ver["version"] }.join(", ") + elsif k == "current_version" + v = v["version"] + elsif k == "providers" + v = v.map{ |p| p["name"] }.join(", ") + elsif k == "downloads" + v = format_downloads(v.to_s) + end + + whitespace = width-k.size + env.ui.info "#{k}:" + "".ljust(whitespace) + " #{v}" + end + end + + # Converts a string of numbers into a formatted number + # + # 1234 -> 1,234 + # + # @param [String] - download_string + def format_downloads(download_string) + return download_string.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse + end + + + # @param [Array] search_results - Box search results from Vagrant Cloud + # @param [String,nil] short - determines if short version will be printed + # @param [String,nil] json - determines if json version will be printed + # @param [Vagrant::Environment] - env + def format_search_results(search_results, short, json, env) + result = [] + search_results.each do |b| + box = {} + box = { + name: b["tag"], + version: b["current_version"]["version"], + downloads: format_downloads(b["downloads"].to_s), + providers: b["current_version"]["providers"].map{ |p| p["name"] }.join(",") + } + result << box + end + + if short + result.map {|b| env.ui.info(b[:name])} + elsif json + env.ui.info(result.to_json) + else + column_labels = {} + columns = result.first.keys + columns.each do |c| + column_labels[c] = c.to_s.upcase + end + print_search_table(env, column_labels, result, [:downloads]) + end + end + end + end + end +end diff --git a/plugins/commands/cloud/version/create.rb b/plugins/commands/cloud/version/create.rb new file mode 100644 index 000000000..116fa365f --- /dev/null +++ b/plugins/commands/cloud/version/create.rb @@ -0,0 +1,68 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module VersionCommand + module Command + class Create < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud version create [options] organization/box-name version" + o.separator "" + o.separator "Creates a version entry on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-d", "--description DESCRIPTION", String, "A description for this version") do |d| + options[:description] = d + end + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + box = argv.first.split('/') + org = box[0] + box_name = box[1] + version = argv[1] + + create_version(org, box_name, version, @client.token, options) + end + + def create_version(org, box_name, box_version, access_token, options) + org = options[:username] if options[:username] + + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(org, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + version = VagrantCloud::Version.new(box, box_version, nil, options[:description], access_token) + + begin + success = version.create_version + @env.ui.success(I18n.t("cloud_command.version.create_success", version: box_version, org: org, box_name: box_name)) + VagrantPlugins::CloudCommand::Util.format_box_results(success.compact, @env) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.version.create_fail", version: box_version, org: org, box_name: box_name)) + @env.ui.error(e) + return 1 + end + return 1 + end + end + end + end + end +end diff --git a/plugins/commands/cloud/version/delete.rb b/plugins/commands/cloud/version/delete.rb new file mode 100644 index 000000000..39f599787 --- /dev/null +++ b/plugins/commands/cloud/version/delete.rb @@ -0,0 +1,68 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module VersionCommand + module Command + class Delete < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud version delete [options] organization/box-name version" + o.separator "" + o.separator "Deletes a version entry on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + box = argv.first.split('/') + org = box[0] + box_name = box[1] + version = argv[1] + + @env.ui.warn(I18n.t("cloud_command.version.delete_warn", version: version, box: argv.first)) + continue = @env.ui.ask(I18n.t("cloud_command.continue")) + return 1 if continue.downcase != "y" + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + + delete_version(org, box_name, version, options, @client.token) + end + + def delete_version(org, box_name, box_version, options, access_token) + org = options[:username] if options[:username] + + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(org, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + version = VagrantCloud::Version.new(box, box_version, nil, nil, access_token) + + begin + success = version.delete + @env.ui.success(I18n.t("cloud_command.version.delete_success", version: box_version, org: org, box_name: box_name)) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.version.delete_fail", version: box_version, org: org, box_name: box_name)) + @env.ui.error(e) + return 1 + end + return 1 + end + end + end + end + end +end diff --git a/plugins/commands/cloud/version/plugin.rb b/plugins/commands/cloud/version/plugin.rb new file mode 100644 index 000000000..6dc177421 --- /dev/null +++ b/plugins/commands/cloud/version/plugin.rb @@ -0,0 +1,19 @@ +require "vagrant" + +module VagrantPlugins + module CloudCommand + module VersionCommand + class Plugin < Vagrant.plugin("2") + name "vagrant cloud version" + description <<-DESC + Version CRUD commands for Vagrant Cloud + DESC + + command(:version) do + require_relative "root" + Command::Root + end + end + end + end +end diff --git a/plugins/commands/cloud/version/release.rb b/plugins/commands/cloud/version/release.rb new file mode 100644 index 000000000..a6e01aeaf --- /dev/null +++ b/plugins/commands/cloud/version/release.rb @@ -0,0 +1,68 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module VersionCommand + module Command + class Release < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud version release [options] organization/box-name version" + o.separator "" + o.separator "Releases a version entry on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @env.ui.warn(I18n.t("cloud_command.version.release_warn", version: argv[1], box: argv.first)) + continue = @env.ui.ask(I18n.t("cloud_command.continue")) + return 1 if continue.downcase != "y" + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + box = argv.first.split('/') + org = box[0] + box_name = box[1] + version = argv[1] + + release_version(org, box_name, version, @client.token, options) + end + + def release_version(org, box_name, version, access_token, options) + org = options[:username] if options[:username] + + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(org, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + version = VagrantCloud::Version.new(box, version, nil, nil, access_token) + + begin + success = version.release + @env.ui.success(I18n.t("cloud_command.version.release_success", version: version, org: org, box_name: box_name)) + VagrantPlugins::CloudCommand::Util.format_box_results(success.compact, @env) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.version.release_fail", version: version, org: org, box_name: box_name)) + @env.ui.error(e) + return 1 + end + return 1 + end + end + end + end + end +end diff --git a/plugins/commands/cloud/version/revoke.rb b/plugins/commands/cloud/version/revoke.rb new file mode 100644 index 000000000..f1550e788 --- /dev/null +++ b/plugins/commands/cloud/version/revoke.rb @@ -0,0 +1,68 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module VersionCommand + module Command + class Revoke < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud version revoke [options] organization/box-name version" + o.separator "" + o.separator "Revokes a version entry on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @env.ui.warn(I18n.t("cloud_command.version.revoke_warn", version: argv[1], box: argv.first)) + continue = @env.ui.ask(I18n.t("cloud_command.continue")) + return 1 if continue.downcase != "y" + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + box = argv.first.split('/') + org = box[0] + box_name = box[1] + version = argv[1] + + revoke_version(org, box_name, version, @client.token, options) + end + + def revoke_version(org, box_name, box_version, access_token, options) + org = options[:username] if options[:username] + + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(org, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + version = VagrantCloud::Version.new(box, box_version, nil, nil, access_token) + + begin + success = version.revoke + @env.ui.success(I18n.t("cloud_command.version.revoke_success", version: box_version, org: org, box_name: box_name)) + VagrantPlugins::CloudCommand::Util.format_box_results(success.compact, @env) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.version.revoke_fail", version: box_version, org: org, box_name: box_name)) + @env.ui.error(e) + return 1 + end + return 1 + end + end + end + end + end +end diff --git a/plugins/commands/cloud/version/root.rb b/plugins/commands/cloud/version/root.rb new file mode 100644 index 000000000..de2a50c9f --- /dev/null +++ b/plugins/commands/cloud/version/root.rb @@ -0,0 +1,81 @@ +module VagrantPlugins + module CloudCommand + module VersionCommand + module Command + class Root < Vagrant.plugin("2", :command) + def self.synopsis + "Version commands" + end + + def initialize(argv, env) + super + + @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) + @subcommands = Vagrant::Registry.new + @subcommands.register(:create) do + require File.expand_path("../create", __FILE__) + Command::Create + end + @subcommands.register(:delete) do + require File.expand_path("../delete", __FILE__) + Command::Delete + end + @subcommands.register(:revoke) do + require File.expand_path("../revoke", __FILE__) + Command::Revoke + end + @subcommands.register(:release) do + require File.expand_path("../release", __FILE__) + Command::Release + end + @subcommands.register(:update) do + require File.expand_path("../update", __FILE__) + Command::Update + end + end + + def execute + if @main_args.include?("-h") || @main_args.include?("--help") + # Print the help for all the version commands. + return help + end + + # If we reached this far then we must have a subcommand. If not, + # then we also just print the help and exit. + command_class = @subcommands.get(@sub_command.to_sym) if @sub_command + return help if !command_class || !@sub_command + @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") + + # Initialize and execute the command class + command_class.new(@sub_args, @env).execute + end + + # Prints the help out for this command + def help + opts = OptionParser.new do |opts| + opts.banner = "Usage: vagrant cloud version []" + opts.separator "" + opts.separator "For taking various actions against a Vagrant boxes version attribute on Vagrant Cloud" + opts.separator "" + opts.separator "Available subcommands:" + + # Add the available subcommands as separators in order to print them + # out as well. + keys = [] + @subcommands.each { |key, value| keys << key.to_s } + + keys.sort.each do |key| + opts.separator " #{key}" + end + + opts.separator "" + opts.separator "For help on any individual subcommand run `vagrant cloud version -h`" + end + + @env.ui.info(opts.help, prefix: false) + end + end + end + end + end +end diff --git a/plugins/commands/cloud/version/update.rb b/plugins/commands/cloud/version/update.rb new file mode 100644 index 000000000..032251a8c --- /dev/null +++ b/plugins/commands/cloud/version/update.rb @@ -0,0 +1,68 @@ +require 'optparse' + +module VagrantPlugins + module CloudCommand + module VersionCommand + module Command + class Update < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cloud version update [options] organization/box-name version" + o.separator "" + o.separator "Updates a version entry on Vagrant Cloud" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-d", "--description DESCRIPTION", "A description for this version") do |d| + options[:description] = d + end + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:username] = u + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + @client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username]) + box = argv.first.split('/') + org = box[0] + box_name = box[1] + version = argv[1] + + update_version(org, box_name, version, @client.token, options) + end + + def update_version(org, box_name, box_version, access_token, options) + org = options[:username] if options[:username] + + server_url = VagrantPlugins::CloudCommand::Util.api_server_url + account = VagrantPlugins::CloudCommand::Util.account?(org, access_token, server_url) + box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token) + version = VagrantCloud::Version.new(box, box_version, nil, options[:description], access_token) + + begin + success = version.update + @env.ui.success(I18n.t("cloud_command.version.update_success", version: box_version, org: org, box_name: box_name)) + VagrantPlugins::CloudCommand::Util.format_box_results(success.compact, @env) + return 0 + rescue VagrantCloud::ClientError => e + @env.ui.error(I18n.t("cloud_command.errors.version.update_fail", version: box_version, org: org, box_name: box_name)) + @env.ui.error(e) + return 1 + end + return 1 + end + end + end + end + end +end diff --git a/test/unit/plugins/commands/cloud/auth/login_test.rb b/test/unit/plugins/commands/cloud/auth/login_test.rb new file mode 100644 index 000000000..c2868a5a3 --- /dev/null +++ b/test/unit/plugins/commands/cloud/auth/login_test.rb @@ -0,0 +1,103 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/auth/login") + +describe VagrantPlugins::CloudCommand::AuthCommand::Command::Login do + include_context "unit" + + let(:argv) { [] } + let(:env) { isolated_environment.create_vagrant_env } + + let(:token_path) { env.data_dir.join("vagrant_login_token") } + + let(:stdout) { StringIO.new } + let(:stderr) { StringIO.new } + + subject { described_class.new(argv, env) } + + before do + stub_env("ATLAS_TOKEN" => "") + end + + let(:action_runner) { double("action_runner") } + + before do + allow(env).to receive(:action_runner).and_return(action_runner) + end + + describe "#execute" do + context "with no args" do + let(:argv) { [] } + end + + context "with --check" do + let(:argv) { ["--check"] } + + context "when there is a token" do + before do + stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate}) + .to_return(status: 200) + end + + before do + File.open(token_path, "w+") { |f| f.write("abcd1234") } + end + + it "returns 0" do + expect(subject.execute).to eq(0) + end + end + + context "when there is no token" do + it "returns 1" do + expect(subject.execute).to eq(1) + end + end + end + + context "with --logout" do + let(:argv) { ["--logout"] } + + it "returns 0" do + expect(subject.execute).to eq(0) + end + + it "clears the token" do + subject.execute + expect(File.exist?(token_path)).to be(false) + end + end + + context "with --token" do + let(:argv) { ["--token", "efgh5678"] } + + context "when the token is valid" do + before do + stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate}) + .to_return(status: 200) + end + + it "sets the token" do + subject.execute + token = File.read(token_path).strip + expect(token).to eq("efgh5678") + end + + it "returns 0" do + expect(subject.execute).to eq(0) + end + end + + context "when the token is invalid" do + before do + stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate}) + .to_return(status: 401) + end + + it "returns 1" do + expect(subject.execute).to eq(1) + end + end + end + end +end diff --git a/test/unit/plugins/commands/cloud/auth/logout_test.rb b/test/unit/plugins/commands/cloud/auth/logout_test.rb new file mode 100644 index 000000000..1356d1136 --- /dev/null +++ b/test/unit/plugins/commands/cloud/auth/logout_test.rb @@ -0,0 +1,43 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/auth/logout") + +describe VagrantPlugins::CloudCommand::AuthCommand::Command::Logout do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + end + + context "with any arguments" do + let (:argv) { ["stuff", "things"] } + + it "shows the help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with no arguments" do + it "logs you out" do + expect(client).to receive(:clear_token) + expect(subject.execute).to eq(0) + end + end +end diff --git a/test/unit/plugins/commands/cloud/auth/whoami_test.rb b/test/unit/plugins/commands/cloud/auth/whoami_test.rb new file mode 100644 index 000000000..ff10691b6 --- /dev/null +++ b/test/unit/plugins/commands/cloud/auth/whoami_test.rb @@ -0,0 +1,54 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/auth/whoami") + +describe VagrantPlugins::CloudCommand::AuthCommand::Command::Whoami do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:account) { double("account") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:account?). + and_return(account) + end + + context "with too many arguments" do + let(:argv) { ["token", "token", "token"] } + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with username" do + let(:argv) { ["token"] } + let(:org_hash) { {"user"=>{"username"=>"mario"}, "boxes"=>[{"name"=>"box"}]} } + + it "gets information about a user" do + expect(account).to receive(:validate_token).and_return(org_hash) + expect(subject.execute).to eq(0) + end + + it "returns 1 if encountering an error making request" do + allow(account).to receive(:validate_token). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/test/unit/plugins/commands/cloud/box/create_test.rb b/test/unit/plugins/commands/cloud/box/create_test.rb new file mode 100644 index 000000000..206e21f38 --- /dev/null +++ b/test/unit/plugins/commands/cloud/box/create_test.rb @@ -0,0 +1,61 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/box/create") + +describe VagrantPlugins::CloudCommand::BoxCommand::Command::Create do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name", "-s", "short", "-d", "long"] } + + it "creates a box" do + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, "short", "long", client.token) + .and_return(box) + + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(box).to receive(:create).and_return({}) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, "short", "long", client.token) + .and_return(box) + + allow(box).to receive(:create). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/test/unit/plugins/commands/cloud/box/delete_test.rb b/test/unit/plugins/commands/cloud/box/delete_test.rb new file mode 100644 index 000000000..09343d1a6 --- /dev/null +++ b/test/unit/plugins/commands/cloud/box/delete_test.rb @@ -0,0 +1,62 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/box/delete") + +describe VagrantPlugins::CloudCommand::BoxCommand::Command::Delete do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(iso_env.ui).to receive(:ask). + and_return("y") + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name"] } + + it "creates a box" do + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + + expect(box).to receive(:delete).and_return({}) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + + allow(box).to receive(:delete). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/test/unit/plugins/commands/cloud/box/show_test.rb b/test/unit/plugins/commands/cloud/box/show_test.rb new file mode 100644 index 000000000..22eba219d --- /dev/null +++ b/test/unit/plugins/commands/cloud/box/show_test.rb @@ -0,0 +1,63 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/box/show") + +describe VagrantPlugins::CloudCommand::BoxCommand::Command::Show do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(iso_env.ui).to receive(:ask). + and_return("y") + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name"] } + + it "creates a box" do + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + + expect(box).to receive(:read).and_return({}) + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + + allow(box).to receive(:read). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/test/unit/plugins/commands/cloud/box/update_test.rb b/test/unit/plugins/commands/cloud/box/update_test.rb new file mode 100644 index 000000000..f0fb55961 --- /dev/null +++ b/test/unit/plugins/commands/cloud/box/update_test.rb @@ -0,0 +1,64 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/box/update") + +describe VagrantPlugins::CloudCommand::BoxCommand::Command::Update do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name", "-d", "update", "-s", "short"] } + + it "creates a box" do + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + + expect(box).to receive(:update). + with(organization: "vagrant", name: "box-name", description: "update", short_description: "short"). + and_return({}) + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + + allow(box).to receive(:update). + with(organization: "vagrant", name: "box-name", description: "update", short_description: "short"). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/test/unit/plugins/commands/cloud/client_test.rb b/test/unit/plugins/commands/cloud/client_test.rb new file mode 100644 index 000000000..bad54a811 --- /dev/null +++ b/test/unit/plugins/commands/cloud/client_test.rb @@ -0,0 +1,261 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/client/client") + +describe VagrantPlugins::CloudCommand::Client do + include_context "unit" + + let(:env) { isolated_environment.create_vagrant_env } + + subject(:client) { described_class.new(env) } + + before(:all) do + I18n.load_path << Vagrant.source_root.join("plugins/commands/cloud/locales/en.yml") + I18n.reload! + end + + before do + stub_env("ATLAS_TOKEN" => nil) + subject.clear_token + end + + describe "#logged_in?" do + let(:url) { "#{Vagrant.server_url}/api/v1/authenticate?access_token=#{token}" } + let(:headers) { { "Content-Type" => "application/json" } } + + before { allow(subject).to receive(:token).and_return(token) } + + context "when there is no token" do + let(:token) { nil } + + it "returns false" do + expect(subject.logged_in?).to be(false) + end + end + + context "when there is a token" do + let(:token) { "ABCD1234" } + + it "returns true if the endpoint returns a 200" do + stub_request(:get, url) + .with(headers: headers) + .to_return(body: JSON.pretty_generate("token" => token)) + expect(subject.logged_in?).to be(true) + end + + it "raises an error if the endpoint returns a non-200" do + stub_request(:get, url) + .with(headers: headers) + .to_return(body: JSON.pretty_generate("bad" => true), status: 401) + expect(subject.logged_in?).to be(false) + end + + it "raises an exception if the server cannot be found" do + stub_request(:get, url) + .to_raise(SocketError) + expect { subject.logged_in? } + .to raise_error(VagrantPlugins::CloudCommand::Errors::ServerUnreachable) + end + end + end + + describe "#login" do + let(:request) { + { + user: { + login: login, + password: password, + }, + token: { + description: description, + }, + two_factor: { + code: nil + } + } + } + + let(:login) { "foo" } + let(:password) { "bar" } + let(:description) { "Token description" } + + let(:headers) { + { + "Accept" => "application/json", + "Content-Type" => "application/json", + } + } + let(:response) { + { + token: "baz" + } + } + + it "returns the access token after successful login" do + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + with(body: JSON.dump(request), headers: headers). + to_return(status: 200, body: JSON.dump(response)) + + client.username_or_email = login + client.password = password + + expect(client.login(description: "Token description")).to eq("baz") + end + + context "when 2fa is required" do + let(:response) { + { + two_factor: { + default_delivery_method: default_delivery_method, + delivery_methods: delivery_methods + } + } + } + let(:default_delivery_method) { "app" } + let(:delivery_methods) { ["app"] } + + before do + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + to_return(status: 406, body: JSON.dump(response)) + end + + it "raises a two-factor required error" do + expect { + client.login + }.to raise_error(VagrantPlugins::CloudCommand::Errors::TwoFactorRequired) + end + + context "when the default delivery method is not app" do + let(:default_delivery_method) { "sms" } + let(:delivery_methods) { ["app", "sms"] } + + it "requests a code and then raises a two-factor required error" do + expect(client) + .to receive(:request_code) + .with(default_delivery_method) + + expect { + client.login + }.to raise_error(VagrantPlugins::CloudCommand::Errors::TwoFactorRequired) + end + end + end + + context "on bad login" do + before do + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + to_return(status: 401, body: "") + end + + it "raises an error" do + expect { + client.login + }.to raise_error(VagrantPlugins::CloudCommand::Errors::Unauthorized) + end + end + + context "if it can't reach the server" do + before do + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + to_raise(SocketError) + end + + it "raises an exception" do + expect { + subject.login + }.to raise_error(VagrantPlugins::CloudCommand::Errors::ServerUnreachable) + end + end + end + + describe "#request_code" do + let(:request) { + { + user: { + login: login, + password: password, + }, + two_factor: { + delivery_method: delivery_method + } + } + } + + let(:login) { "foo" } + let(:password) { "bar" } + let(:delivery_method) { "sms" } + + let(:headers) { + { + "Accept" => "application/json", + "Content-Type" => "application/json" + } + } + + let(:response) { + { + two_factor: { + obfuscated_destination: "SMS number ending in 1234" + } + } + } + + it "displays that the code was sent" do + expect(env.ui) + .to receive(:success) + .with("2FA code sent to SMS number ending in 1234.") + + stub_request(:post, "#{Vagrant.server_url}/api/v1/two-factor/request-code"). + with(body: JSON.dump(request), headers: headers). + to_return(status: 201, body: JSON.dump(response)) + + client.username_or_email = login + client.password = password + + client.request_code delivery_method + end + end + + describe "#token" do + it "reads ATLAS_TOKEN" do + stub_env("ATLAS_TOKEN" => "ABCD1234") + expect(subject.token).to eq("ABCD1234") + end + + it "reads the stored file" do + subject.store_token("EFGH5678") + expect(subject.token).to eq("EFGH5678") + end + + it "prefers the environment variable" do + stub_env("VAGRANT_CLOUD_TOKEN" => "ABCD1234") + subject.store_token("EFGH5678") + expect(subject.token).to eq("ABCD1234") + end + + it "prints a warning if the envvar and stored file are both present" do + stub_env("VAGRANT_CLOUD_TOKEN" => "ABCD1234") + subject.store_token("EFGH5678") + expect(env.ui).to receive(:warn).with(/detected both/) + subject.token + end + + it "returns nil if there's no token set" do + expect(subject.token).to be(nil) + end + end + + describe "#store_token, #clear_token" do + it "stores the token and can re-access it" do + subject.store_token("foo") + expect(subject.token).to eq("foo") + expect(described_class.new(env).token).to eq("foo") + end + + it "deletes the token" do + subject.store_token("foo") + subject.clear_token + expect(subject.token).to be_nil + end + end +end diff --git a/test/unit/plugins/commands/cloud/list_test.rb b/test/unit/plugins/commands/cloud/list_test.rb new file mode 100644 index 000000000..4e4375920 --- /dev/null +++ b/test/unit/plugins/commands/cloud/list_test.rb @@ -0,0 +1,24 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/list") + +describe VagrantPlugins::CloudCommand::Command::List do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + end + +end diff --git a/test/unit/plugins/commands/cloud/provider/create_test.rb b/test/unit/plugins/commands/cloud/provider/create_test.rb new file mode 100644 index 000000000..bc6eafa59 --- /dev/null +++ b/test/unit/plugins/commands/cloud/provider/create_test.rb @@ -0,0 +1,85 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/provider/create") + +describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Create do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + let(:version) { double("version") } + let(:provider) { double("provider") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + allow(VagrantCloud::Version).to receive(:new) + .with(box, "1.0.0", nil, nil, client.token) + .and_return(version) + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0"] } + + it "creates a provider" do + allow(VagrantCloud::Provider).to receive(:new). + with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token). + and_return(provider) + + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(iso_env.ui).to receive(:warn) + expect(provider).to receive(:create_provider).and_return({}) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Provider).to receive(:new). + with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token). + and_return(provider) + + allow(provider).to receive(:create_provider). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end + + context "with arguments and a remote url" do + let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0", "https://box.com/box"] } + + it "creates a provider" do + allow(VagrantCloud::Provider).to receive(:new). + with(version, "virtualbox", nil, "https://box.com/box", "vagrant", "box-name", client.token). + and_return(provider) + + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(iso_env.ui).not_to receive(:warn) + expect(provider).to receive(:create_provider).and_return({}) + expect(subject.execute).to eq(0) + end + end +end diff --git a/test/unit/plugins/commands/cloud/provider/delete_test.rb b/test/unit/plugins/commands/cloud/provider/delete_test.rb new file mode 100644 index 000000000..d1cba4223 --- /dev/null +++ b/test/unit/plugins/commands/cloud/provider/delete_test.rb @@ -0,0 +1,70 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/provider/delete") + +describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Delete do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + let(:version) { double("version") } + let(:provider) { double("provider") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + allow(VagrantCloud::Version).to receive(:new) + .with(box, "1.0.0", nil, nil, client.token) + .and_return(version) + allow(iso_env.ui).to receive(:ask). + and_return("y") + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0"] } + + it "deletes a provider" do + allow(VagrantCloud::Provider).to receive(:new). + with(version, "virtualbox", nil, nil, nil, nil, client.token). + and_return(provider) + + expect(provider).to receive(:delete).and_return({}) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Provider).to receive(:new). + with(version, "virtualbox", nil, nil, nil, nil, client.token). + and_return(provider) + + allow(provider).to receive(:delete). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/test/unit/plugins/commands/cloud/provider/update_test.rb b/test/unit/plugins/commands/cloud/provider/update_test.rb new file mode 100644 index 000000000..ad0f7629c --- /dev/null +++ b/test/unit/plugins/commands/cloud/provider/update_test.rb @@ -0,0 +1,85 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/provider/update") + +describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Update do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box", create: true, read: {}) } + let(:version) { double("version", create_version: true, release: true) } + let(:provider) { double("provider", create_provider: true, upload_file: true) } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + allow(VagrantCloud::Version).to receive(:new) + .with(box, "1.0.0", nil, nil, client.token) + .and_return(version) + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0"] } + + it "updates a provider" do + allow(VagrantCloud::Provider).to receive(:new). + with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token). + and_return(provider) + + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(iso_env.ui).to receive(:warn) + expect(provider).to receive(:update).and_return({}) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Provider).to receive(:new). + with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token). + and_return(provider) + + allow(provider).to receive(:update). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end + + context "with arguments and a remote url" do + let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0", "https://box.com/box"] } + + it "creates a provider" do + allow(VagrantCloud::Provider).to receive(:new). + with(version, "virtualbox", nil, "https://box.com/box", "vagrant", "box-name", client.token). + and_return(provider) + + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(iso_env.ui).not_to receive(:warn) + expect(provider).to receive(:update).and_return({}) + expect(subject.execute).to eq(0) + end + end +end diff --git a/test/unit/plugins/commands/cloud/provider/upload_test.rb b/test/unit/plugins/commands/cloud/provider/upload_test.rb new file mode 100644 index 000000000..2fed0defe --- /dev/null +++ b/test/unit/plugins/commands/cloud/provider/upload_test.rb @@ -0,0 +1,70 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/provider/upload") + +describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Upload do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + let(:version) { double("version") } + let(:provider) { double("provider") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + allow(VagrantCloud::Version).to receive(:new) + .with(box, "1.0.0", nil, nil, client.token) + .and_return(version) + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0", "path/to/box.box"] } + + it "uploads a provider" do + allow(VagrantCloud::Provider).to receive(:new). + with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token). + and_return(provider) + + expect(provider).to receive(:upload_file). + with("path/to/box.box"). + and_return({}) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Provider).to receive(:new). + with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token). + and_return(provider) + + allow(provider).to receive(:upload_file). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/test/unit/plugins/commands/cloud/publish_test.rb b/test/unit/plugins/commands/cloud/publish_test.rb new file mode 100644 index 000000000..0d4e44030 --- /dev/null +++ b/test/unit/plugins/commands/cloud/publish_test.rb @@ -0,0 +1,80 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/publish") + +describe VagrantPlugins::CloudCommand::Command::Publish do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:client) { double("client", token: "1234token1234") } + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:box) { double("box", create: true, read: {}) } + let(:version) { double("version", create_version: true, release: true) } + let(:provider) { double("provider", create_provider: true, upload_file: true) } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(iso_env.ui).to receive(:ask). + and_return("y") + allow(VagrantCloud::Box).to receive(:new).and_return(box) + allow(VagrantCloud::Version).to receive(:new).and_return(version) + allow(VagrantCloud::Provider).to receive(:new).and_return(provider) + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let(:argv) { ["vagrant/box", "1.0.0", "virtualbox", "path/to/the/virtualbox.box"] } + + it "publishes a box given options" do + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(subject.execute).to eq(0) + end + + it "catches a ClientError if something goes wrong" do + allow(box).to receive(:create). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end + + context "with arguments and releasing a box" do + let(:argv) { ["vagrant/box", "1.0.0", "virtualbox", "path/to/the/virtualbox.box", "--release"] } + + it "releases the box" do + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(version).to receive(:release) + expect(subject.execute).to eq(0) + end + end + + context "with arguments and a remote url" do + let(:argv) { ["vagrant/box", "1.0.0", "virtualbox", "--url", "https://www.boxes.com/path/to/the/virtualbox.box"] } + + it "does not upload a file" do + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(subject.execute).to eq(0) + expect(provider).not_to receive(:upload_file) + end + end +end diff --git a/test/unit/plugins/commands/cloud/search_test.rb b/test/unit/plugins/commands/cloud/search_test.rb new file mode 100644 index 000000000..f34025a5c --- /dev/null +++ b/test/unit/plugins/commands/cloud/search_test.rb @@ -0,0 +1,77 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/search") + +describe VagrantPlugins::CloudCommand::Command::Search do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:client) { double("client", token: "1234token1234") } + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_search_results). + and_return(true) + end + + context "with no arguments" do + let (:search) { double("search", search: {"boxes"=>["all of them"]}) } + + it "makes a request to search all boxes and formats them" do + allow(VagrantCloud::Search).to receive(:new). + and_return(search) + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_search_results) + expect(subject.execute).to eq(0) + end + end + + context "with no arguments and an error occurs making requests" do + let (:search) { double("search") } + + it "catches a ClientError if something goes wrong" do + allow(VagrantCloud::Search).to receive(:new). + and_return(search) + allow(search).to receive(:search). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + + expect(subject.execute).to eq(1) + end + end + + context "with no arguments and no results" do + let (:search) { double("search", search: {"boxes"=>[]}) } + + it "makes a request to search all boxes and formats them" do + allow(VagrantCloud::Search).to receive(:new). + and_return(search) + expect(VagrantPlugins::CloudCommand::Util).not_to receive(:format_search_results) + subject.execute + end + end + + context "with arguments" do + let (:search) { double("search", search: {"boxes"=>["all of them"]}) } + let (:argv) { ["ubuntu", "--page", "1", "--order", "desc", "--limit", "100", "--provider", "provider", "--sort", "downloads"] } + + it "sends the options to make a request with" do + allow(VagrantCloud::Search).to receive(:new). + and_return(search) + expect(search).to receive(:search). + with("ubuntu", "provider", "downloads", "desc", 100, 1) + subject.execute + end + end +end diff --git a/test/unit/plugins/commands/cloud/version/create_test.rb b/test/unit/plugins/commands/cloud/version/create_test.rb new file mode 100644 index 000000000..d367360d4 --- /dev/null +++ b/test/unit/plugins/commands/cloud/version/create_test.rb @@ -0,0 +1,65 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/version/create") + +describe VagrantPlugins::CloudCommand::VersionCommand::Command::Create do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + let(:version) { double("version") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name", "1.0.0", "-d", "description"] } + + it "creates a version" do + allow(VagrantCloud::Version).to receive(:new). + with(box, "1.0.0", nil, "description", client.token). + and_return(version) + + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(version).to receive(:create_version).and_return({}) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Version).to receive(:new). + with(box, "1.0.0", nil, "description", client.token). + and_return(version) + + allow(version).to receive(:create_version). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/test/unit/plugins/commands/cloud/version/delete_test.rb b/test/unit/plugins/commands/cloud/version/delete_test.rb new file mode 100644 index 000000000..abbcc09a2 --- /dev/null +++ b/test/unit/plugins/commands/cloud/version/delete_test.rb @@ -0,0 +1,66 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/version/delete") + +describe VagrantPlugins::CloudCommand::VersionCommand::Command::Delete do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + let(:version) { double("version") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + allow(iso_env.ui).to receive(:ask). + and_return("y") + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name", "1.0.0"] } + + it "deletes a version" do + allow(VagrantCloud::Version).to receive(:new). + with(box, "1.0.0", nil, nil, client.token). + and_return(version) + + expect(version).to receive(:delete).and_return({}) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Version).to receive(:new). + with(box, "1.0.0", nil, nil, client.token). + and_return(version) + + allow(version).to receive(:delete). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/test/unit/plugins/commands/cloud/version/release_test.rb b/test/unit/plugins/commands/cloud/version/release_test.rb new file mode 100644 index 000000000..77d1b788f --- /dev/null +++ b/test/unit/plugins/commands/cloud/version/release_test.rb @@ -0,0 +1,66 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/version/release") + +describe VagrantPlugins::CloudCommand::VersionCommand::Command::Release do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + let(:version) { double("version") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + allow(iso_env.ui).to receive(:ask). + and_return("y") + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name", "1.0.0"] } + + it "releases a version" do + allow(VagrantCloud::Version).to receive(:new). + with(box, "1.0.0", nil, nil, client.token). + and_return(version) + + expect(version).to receive(:release).and_return({}) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Version).to receive(:new). + with(box, "1.0.0", nil, nil, client.token). + and_return(version) + + allow(version).to receive(:release). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/test/unit/plugins/commands/cloud/version/revoke_test.rb b/test/unit/plugins/commands/cloud/version/revoke_test.rb new file mode 100644 index 000000000..c75f064fa --- /dev/null +++ b/test/unit/plugins/commands/cloud/version/revoke_test.rb @@ -0,0 +1,66 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/version/revoke") + +describe VagrantPlugins::CloudCommand::VersionCommand::Command::Revoke do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + let(:version) { double("version") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + allow(iso_env.ui).to receive(:ask). + and_return("y") + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name", "1.0.0"] } + + it "revokes a version" do + allow(VagrantCloud::Version).to receive(:new). + with(box, "1.0.0", nil, nil, client.token). + and_return(version) + + expect(version).to receive(:revoke).and_return({}) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Version).to receive(:new). + with(box, "1.0.0", nil, nil, client.token). + and_return(version) + + expect(version).to receive(:revoke). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/test/unit/plugins/commands/cloud/version/update_test.rb b/test/unit/plugins/commands/cloud/version/update_test.rb new file mode 100644 index 000000000..5d5b24877 --- /dev/null +++ b/test/unit/plugins/commands/cloud/version/update_test.rb @@ -0,0 +1,65 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/version/update") + +describe VagrantPlugins::CloudCommand::VersionCommand::Command::Update do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + subject { described_class.new(argv, iso_env) } + + let(:action_runner) { double("action_runner") } + + let(:client) { double("client", token: "1234token1234") } + let(:box) { double("box") } + let(:version) { double("version") } + + before do + allow(iso_env).to receive(:action_runner).and_return(action_runner) + allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login). + and_return(client) + allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results). + and_return(true) + allow(VagrantCloud::Box).to receive(:new) + .with(anything, "box-name", nil, nil, nil, client.token) + .and_return(box) + end + + context "with no arguments" do + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) + end + end + + context "with arguments" do + let (:argv) { ["vagrant/box-name", "1.0.0", "-d", "description"] } + + it "updates a version" do + allow(VagrantCloud::Version).to receive(:new). + with(box, "1.0.0", nil, "description", client.token). + and_return(version) + + expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results) + expect(version).to receive(:update).and_return({}) + expect(subject.execute).to eq(0) + end + + it "displays an error if encoutering a problem with the request" do + allow(VagrantCloud::Version).to receive(:new). + with(box, "1.0.0", nil, "description", client.token). + and_return(version) + + allow(version).to receive(:update). + and_raise(VagrantCloud::ClientError.new("Fail Message", "Message")) + expect(subject.execute).to eq(1) + end + end +end diff --git a/vagrant.gemspec b/vagrant.gemspec index 0d804e446..11627738e 100644 --- a/vagrant.gemspec +++ b/vagrant.gemspec @@ -32,6 +32,7 @@ Gem::Specification.new do |s| s.add_dependency "winrm", "~> 2.1" s.add_dependency "winrm-fs", "~> 1.0" s.add_dependency "winrm-elevated", "~> 1.1" + s.add_dependency "vagrant_cloud", "~> 2.0.0" # NOTE: The ruby_dep gem is an implicit dependency from the listen gem. Later versions # of the ruby_dep gem impose an aggressive constraint on the required ruby version (>= 2.2.5). diff --git a/website/source/docs/cli/cloud.html.md b/website/source/docs/cli/cloud.html.md new file mode 100644 index 000000000..4854110a9 --- /dev/null +++ b/website/source/docs/cli/cloud.html.md @@ -0,0 +1,339 @@ +--- +layout: "docs" +page_title: "vagrant cloud - Command-Line Interface" +sidebar_current: "cli-cloud" +description: |- + The "vagrant cloud" command can be used for taking actions against + Vagrant Cloud like searching or uploading a Vagrant Box +--- + +# Cloud + +**Command: `vagrant cloud`** + +This is the command used to manage anything related to [Vagrant Cloud](https://vagrantcloud.com) + +The main functionality of this command is exposed via even more subcommands: + +* [`auth`](#cloud-auth) +* [`box`](#cloud-box) +* [`provider`](#cloud-provider) +* [`publish`](#cloud-publish) +* [`search`](#cloud-search) +* [`version`](#cloud-version) + +# Cloud Auth + +**Command: `vagrant cloud auth`** + +Information about this subcommand goes here + +* [`login`](#cloud-auth-login) +* [`logout`](#cloud-auth-logout) +* [`who`](#cloud-auth-who) + +## Cloud Auth Login + +**Command: `vagrant cloud auth login`** + +The login command is used to authenticate with the +[HashiCorp's Vagrant Cloud](/docs/vagrant-cloud) server. Logging is only +necessary if you are accessing protected boxes or using +[Vagrant Share](/docs/share/). + +**Logging in is not a requirement to use Vagrant.** The vast majority +of Vagrant does _not_ require a login. Only certain features such as protected +boxes or [Vagrant Share](/docs/share/) require a login. + +The reference of available command-line flags to this command +is available below. + +### Options + +* `--check` - This will check if you are logged in. In addition to outputting + whether you are logged in or not, the command will have exit status 0 if you are + logged in, and exit status 1 if you are not. + +* `--logout` - This will log you out if you are logged in. If you are already + logged out, this command will do nothing. It is not an error to call this + command if you are already logged out. + +* `--token` - This will set the Vagrant Cloud login token manually to the provided + string. It is assumed this token is a valid Vagrant Cloud access token. + +### Examples + +Securely authenticate to Vagrant Cloud using a username and password: + +```text +$ vagrant cloud auth login +# ... +Vagrant Cloud username: +Vagrant Cloud password: +``` + +Check if the current user is authenticated: + +```text +$ vagrant cloud auth login --check +You are already logged in. +``` + +Securely authenticate with Vagrant Cloud using a token: + +```text +$ vagrant cloud auth login --token ABCD1234 +The token was successfully saved. +``` + +## Cloud Auth Logout + +**Command: `vagrant cloud auth logout`** + +This will log you out if you are logged in. If you are already +logged out, this command will do nothing. It is not an error to call this +command if you are already logged out. + +## Cloud Auth Whomi + +**Command: `vagrant cloud auth whoami [TOKEN]`** + +This command will validate your Vagrant Cloud token and will print the user who +it belongs to. If a token is passed in, it will attempt to validate it instead +of the token stored stored on disk. + +# Cloud Box + +**Command: `vagrant cloud box`** + +The `cloud box` command is used to manage CRUD operations for all `box` entities on +Vagrant Cloud. + +* [`create`](#cloud-box-create) +* [`delete`](#cloud-box-delete) +* [`show`](#cloud-box-show) +* [`update`](#cloud-box-update) + +## Cloud Box Create + +**Command: `vagrant cloud box create ORGANIZATION/BOX-NAME`** + +The box create command is used to create a new box entry on Vagrant Cloud. + +### Options + +* `--description DESCRIPTION` - A longer description of the box. Can be + formatted with Markdown. +* `--short-description DESCRIPTION` - A short summary of the box. +* `--private` - Will make the new box private (Public by default) + +## Cloud Box Delete + +**Command: `vagrant cloud box delete ORGANIZATION/BOX-NAME`** + +The box delete command will _permanently_ delete the given box entry on Vagrant Cloud. Before +making the request, it will ask if you are sure you want to delete the box. + +## Cloud Box Show + +**Command: `vagrant cloud box show ORGANIZATION/BOX-NAME`** + +The box show command will display information about the latest version for the given Vagrant box. + +## Cloud Box Update + +**Command: `vagrant cloud box update ORGANIZATION/BOX-NAME`** + +The box update command will update an already created box on Vagrant Cloud with the given options. + +### Options + +* `--description DESCRIPTION` - A longer description of the box. Can be + formatted with Markdown. +* `--short-description DESCRIPTION` - A short summary of the box. +* `--private` - Will make the new box private (Public by default) + +# Cloud Provider + +**Command: `vagrant cloud provider`** + +The `cloud provider` command is used to manage CRUD operations for all `provider` entities on +Vagrant Cloud. + +* [`create`](#cloud-provider-create) +* [`delete`](#cloud-provider-delete) +* [`update`](#cloud-provider-update) +* [`upload`](#cloud-provider-upload) + +## Cloud Provider Create + +**Command: `vagrant cloud provider create ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION [URL]`** + + +The provider create command is used to create a new provider entry on Vagrant Cloud. +The `url` argument is expected to be a valid remote URL that Vagrant Cloud can use +to download the provider. If no `url` is specified, the provider entry can be updated +later with a url or the [upload](#cloud-provider-upload) command can be used to +upload a Vagrant [box file](/docs/boxes.html). + +## Cloud Provider Delete + +**Command: `vagrant cloud provider delete ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION`** + +The provider delete command is used to delete a provider entry on Vagrant Cloud. +Before making the request, it will ask if you are sure you want to delete the provider. + +## Cloud Provider Update + +**Command: `vagrant cloud provider update ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION [URL]`** + +The provider update command will update an already created provider for a box on +Vagrant Cloud with the given options. + +## Cloud Provider Upload + +**Command: `vagrant cloud provider upload ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION BOX-FILE`** + +The provider upload command will upload a Vagrant [box file](/docs/boxes.html) to Vagrant Cloud for +the specified version and provider. + +# Cloud Publish + +**Command: `vagrant cloud publish ORGANIZATION/BOX-NAME VERSION PROVIDER-NAME [PROVIDER-FILE]`** + +The publish command is a start-to-finish solution for creating and updating a +Vagrant box on Vagrant Cloud. Instead of having to create each attribute of a Vagrant +box with separate commands, the publish command instead askes you to provide all +the information you need up front to create or update a new box. + +## Options + +* `--box-version VERSION` - Version to create for the box +* `--description DESCRIPTION` - A longer description of the box. Can be + formatted with Markdown. +* `--force` - Disables confirmation when creating or updating a box. +* `--short-description DESCRIPTION` - A short summary of the box. +* `--private` - Will make the new box private (Public by default) +* `--release` - Automatically releases the box after creation (Unreleased by default) +* `--url` - Valid remote URL to download the box file +* `--version-description DESCRIPTION` - Description of the version that will be created. + +## Examples + +Creating a new box on Vagrant Cloud + +```text +$ vagrant cloud publish briancain/supertest 1.0.0 virtualbox boxes/my/virtualbox.box -d "A really cool box to download and use" --version-description "A cool version" --release --short-description "Donwload me!" +You are about to create a box on Vagrant Cloud with the following options: + + briancain/supertest (1.0.0) for virtualbox + Automatic Release: true + Box Description: A really cool box to download and use + Box Short Description: Download me! + Version Description: A cool version + +Do you wish to continue? [y/N] y +Creating a box entry... +Creating a version entry... +Creating a provider entry... +Uploading provider with file boxes/my/virtualbox.box +Releasing box... +Complete! Published briancain/supertest +tag: briancain/supertest +username: briancain +name: supertest +private: false +downloads: 0 +created_at: 2018-07-25T17:53:04.340Z +updated_at: 2018-07-25T18:01:10.665Z +short_description: Download me! +description_markdown: A reall cool box to download and use +current_version: 1.0.0 +providers: virtualbox +``` + +# Cloud Search + +**Command: `vagrant cloud search QUERY`** + +The cloud search command will take a query and search Vagrant Cloud for any matching +Vagrant boxes. Various filters can be applied to the results. + +## Options + +* `--json` - Format search results in JSON. +* `--page PAGE` - The page to display. Defaults to the first page of results. +* `--short` - Shows a simple list of box names for the results. +* `--order ORDER` - Order to display results. Can either be `desc` or `asc`. +Defaults to `desc`. +* `--limit LIMIT` - Max number of search results to display. Defaults to 25. +* `--provider PROVIDER` - Filter search results to a single provider. +* `--sort-by SORT` - The field to sort results on. Can be `created`, `downloads` +, or `updated`. Defaults to `downloads`. + +## Examples + +If you are looking for a HashiCorp box: + +```text +vagrant cloud search hashicorp --limit 5 +| NAME | VERSION | DOWNLOADS | PROVIDERS | ++-------------------------+---------+-----------+---------------------------------+ +| hashicorp/precise64 | 1.1.0 | 6,675,725 | virtualbox,vmware_fusion,hyperv | +| hashicorp/precise32 | 1.0.0 | 2,261,377 | virtualbox | +| hashicorp/boot2docker | 1.7.8 | 59,284 | vmware_desktop,virtualbox | +| hashicorp/connect-vm | 0.1.0 | 6,912 | vmware_desktop,virtualbox | +| hashicorp/vagrant-share | 0.1.0 | 3,488 | vmware_desktop,virtualbox | ++-------------------------+---------+-----------+---------------------------------+ +``` + +# Cloud Version + +**Command: `vagrant cloud version`** + +* [`create`](#cloud-version-create) +* [`delete`](#cloud-version-delete) +* [`release`](#cloud-version-release) +* [`revoke`](#cloud-version-revoke) +* [`update`](#cloud-version-update) + +## Cloud Version Create + +**Command: `vagrant cloud version create ORGANIZATION/BOX-NAME VERSION`** + +The cloud create command creates a version entry for a box on Vagrant Cloud. + +### Options + +* `--description DESCRIPTION` - Description of the version that will be created. + +## Cloud Version Delete + +**Command: `vagrant cloud version delete ORGANIZATION/BOX-NAME VERSION`** + +The cloud delete command deletes a version entry for a box on Vagrant Cloud. +Before making the request, it will ask if you are sure you want to delete the version. + +## Cloud Version Release + +**Command: `vagrant cloud version release ORGANIZATION/BOX-NAME VERSION`** + +The cloud release command releases a version entry for a box on Vagrant Cloud +if it already exists. Before making the request, it will ask if you are sure you +want to release the version. + +## Cloud Version Revoke + +**Command: `vagrant cloud version revoke ORGANIZATION/BOX-NAME VERSION`** + +The cloud revoke command revokes a version entry for a box on Vagrant Cloud +if it already exists. Before making the request, it will ask if you are sure you +want to revoke the version. + +## Cloud Version Update + +**Command: `vagrant cloud version update ORGANIZATION/BOX-NAME VERSION`** + +### Options + +* `--description DESCRIPTION` - Description of the version that will be created. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index bf17897fe..e4ea9aa30 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -20,6 +20,7 @@ Commands (CLI)