diff --git a/CHANGELOG.md b/CHANGELOG.md index 42e753943..5111ccd0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ FEATURES: providers are chosen before later ones. [GH-3812] - If the default insecure keypair is used, Vagrant will automatically replace it with a randomly generated keypair on first `vagrant up`. [GH-2608] + - Vagrant Login is now part of Vagrant core - Chef Zero provisioner: Use Chef 11's "local" mode to run recipes against an in-memory Chef Server - Chef Apply provisioner: Specify inline Chef recipes and recipe snippets diff --git a/lib/vagrant.rb b/lib/vagrant.rb index d3ab6f37c..4b5dc84ba 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -123,6 +123,7 @@ module Vagrant c.register([:"2", :host]) { Plugin::V2::Host } c.register([:"2", :provider]) { Plugin::V2::Provider } c.register([:"2", :provisioner]) { Plugin::V2::Provisioner } + c.register([:"2", :push]) { Plugin::V2::Push } c.register([:"2", :synced_folder]) { Plugin::V2::SyncedFolder } end diff --git a/lib/vagrant/action/builtin/box_add.rb b/lib/vagrant/action/builtin/box_add.rb index 554613b94..13688bae5 100644 --- a/lib/vagrant/action/builtin/box_add.rb +++ b/lib/vagrant/action/builtin/box_add.rb @@ -147,7 +147,7 @@ module Vagrant # element is an authenticated URL. # @param [Hash] env # @param [Bool] expanded True if the metadata URL was expanded with - # a Vagrant Cloud server URL. + # a Atlas server URL. def add_from_metadata(url, env, expanded) original_url = env[:box_url] provider = env[:box_provider] diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 4f087a103..acda0c178 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -539,6 +539,41 @@ module Vagrant end end + # This executes the push with the given name, raising any exceptions that + # occur. + # + # Precondition: the push is not nil and exists. + def push(name) + @logger.info("Getting push: #{name}") + + name = name.to_sym + + pushes = self.vagrantfile.config.push.__compiled_pushes + if !pushes.key?(name) + raise Vagrant::Errors::PushStrategyNotDefined, + name: name, + pushes: pushes.keys + end + + strategy, config = pushes[name] + push_registry = Vagrant.plugin("2").manager.pushes + klass, _ = push_registry.get(strategy) + if klass.nil? + raise Vagrant::Errors::PushStrategyNotLoaded, + name: strategy, + pushes: push_registry.keys + end + + klass.new(self, config).push + end + + # The list of pushes defined in this Vagrantfile. + # + # @return [Array] + def pushes + self.vagrantfile.config.push.__compiled_pushes.keys + end + # This returns a machine with the proper provider for this environment. # The machine named by `name` must be in this environment. # diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 623073a97..de002ffed 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -556,6 +556,22 @@ module Vagrant error_key(:plugin_uninstall_system) end + class PushesNotDefined < VagrantError + error_key(:pushes_not_defined) + end + + class PushStrategyNotDefined < VagrantError + error_key(:push_strategy_not_defined) + end + + class PushStrategyNotLoaded < VagrantError + error_key(:push_strategy_not_loaded) + end + + class PushStrategyNotProvided < VagrantError + error_key(:push_strategy_not_provided) + end + class RSyncError < VagrantError error_key(:rsync_error) end diff --git a/lib/vagrant/plugin/v2.rb b/lib/vagrant/plugin/v2.rb index 1539667bd..953f73ff6 100644 --- a/lib/vagrant/plugin/v2.rb +++ b/lib/vagrant/plugin/v2.rb @@ -16,6 +16,7 @@ module Vagrant autoload :Manager, "vagrant/plugin/v2/manager" autoload :Plugin, "vagrant/plugin/v2/plugin" autoload :Provider, "vagrant/plugin/v2/provider" + autoload :Push, "vagrant/plugin/v2/push" autoload :Provisioner, "vagrant/plugin/v2/provisioner" autoload :SyncedFolder, "vagrant/plugin/v2/synced_folder" end diff --git a/lib/vagrant/plugin/v2/components.rb b/lib/vagrant/plugin/v2/components.rb index 7bae6c29a..d7c64d370 100644 --- a/lib/vagrant/plugin/v2/components.rb +++ b/lib/vagrant/plugin/v2/components.rb @@ -54,6 +54,11 @@ module Vagrant # @return [Hash] attr_reader :provider_capabilities + # This contains all the push implementations by name. + # + # @return [Registry>] + attr_reader :pushes + # This contains all the synced folder implementations by name. # # @return [Registry>] @@ -71,6 +76,7 @@ module Vagrant @host_capabilities = Hash.new { |h, k| h[k] = Registry.new } @providers = Registry.new @provider_capabilities = Hash.new { |h, k| h[k] = Registry.new } + @pushes = Registry.new @synced_folders = Registry.new end end diff --git a/lib/vagrant/plugin/v2/manager.rb b/lib/vagrant/plugin/v2/manager.rb index 62a23f82a..ce76d1fc1 100644 --- a/lib/vagrant/plugin/v2/manager.rb +++ b/lib/vagrant/plugin/v2/manager.rb @@ -172,6 +172,28 @@ module Vagrant end end + # This returns all registered pushes. + # + # @return [Registry] + def pushes + Registry.new.tap do |result| + @registered.each do |plugin| + result.merge!(plugin.components.pushes) + end + end + end + + # This returns all the config classes for the various pushes. + # + # @return [Registry] + def push_configs + Registry.new.tap do |result| + @registered.each do |plugin| + result.merge!(plugin.components.configs[:push]) + end + end + end + # This returns all synced folder implementations. # # @return [Registry] diff --git a/lib/vagrant/plugin/v2/plugin.rb b/lib/vagrant/plugin/v2/plugin.rb index 9a7f6177d..a39b3cf10 100644 --- a/lib/vagrant/plugin/v2/plugin.rb +++ b/lib/vagrant/plugin/v2/plugin.rb @@ -221,6 +221,18 @@ module Vagrant data[:provisioners] end + # Registers additional pushes to be available. + # + # @param [String] name Name of the push. + # @param [Hash] options List of options for the push. + def self.push(name, options=nil, &block) + components.pushes.register(name.to_sym) do + [block.call, options] + end + + nil + end + # Registers additional synced folder implementations. # # @param [String] name Name of the implementation. diff --git a/lib/vagrant/plugin/v2/push.rb b/lib/vagrant/plugin/v2/push.rb new file mode 100644 index 000000000..f8bc15d53 --- /dev/null +++ b/lib/vagrant/plugin/v2/push.rb @@ -0,0 +1,27 @@ +module Vagrant + module Plugin + module V2 + class Push + attr_reader :env + attr_reader :config + + # Initializes the pusher with the given environment the push + # configuration. + # + # @param [Environment] env + # @param [Object] config Push configuration + def initialize(env, config) + @env = env + @config = config + end + + # This is the method called when the actual pushing should be + # done. + # + # No return value is expected. + def push + end + end + end + end +end diff --git a/lib/vagrant/registry.rb b/lib/vagrant/registry.rb index 5095f2097..f3c86edec 100644 --- a/lib/vagrant/registry.rb +++ b/lib/vagrant/registry.rb @@ -34,7 +34,7 @@ module Vagrant def has_key?(key) @items.has_key?(key) end - + # Returns an array populated with the keys of this object. # # @return [Array] @@ -49,6 +49,21 @@ module Vagrant end end + # Return the number of elements in this registry. + # + # @return [Fixnum] + def length + @items.keys.length + end + alias_method :size, :length + + # Checks if this registry has any items. + # + # @return [Boolean] + def empty? + @items.keys.empty? + end + # Merge one registry with another and return a completely new # registry. Note that the result cache is completely busted, so # any gets on the new registry will result in a cache miss. diff --git a/lib/vagrant/shared_helpers.rb b/lib/vagrant/shared_helpers.rb index c2c52ae10..b195c36df 100644 --- a/lib/vagrant/shared_helpers.rb +++ b/lib/vagrant/shared_helpers.rb @@ -5,12 +5,12 @@ require "thread" module Vagrant @@global_lock = Mutex.new - # This is the default endpoint of the Vagrant Cloud in + # This is the default endpoint of the Atlas in # use. API calls will be made to this for various functions # of Vagrant that may require remote access. # # @return [String] - DEFAULT_SERVER_URL = "https://vagrantcloud.com" + DEFAULT_SERVER_URL = "https://atlas.hashicorp.com" # This holds a global lock for the duration of the block. This should # be invoked around anything that is modifying process state (such as diff --git a/plugins/commands/box/command/add.rb b/plugins/commands/box/command/add.rb index 0356ee298..d82d441a8 100644 --- a/plugins/commands/box/command/add.rb +++ b/plugins/commands/box/command/add.rb @@ -47,7 +47,7 @@ module VagrantPlugins end o.separator "" - o.separator "The box descriptor can be the name of a box on Vagrant Cloud," + o.separator "The box descriptor can be the name of a box on HashiCorp's Atlas," o.separator "or a URL, or a local .box file, or a local .json file containing" o.separator "the catalog metadata." o.separator "" diff --git a/plugins/commands/login/client.rb b/plugins/commands/login/client.rb new file mode 100644 index 000000000..f06772be9 --- /dev/null +++ b/plugins/commands/login/client.rb @@ -0,0 +1,95 @@ +require "rest_client" + +module VagrantPlugins + module LoginCommand + class Client + # Initializes a login client with the given Vagrant::Environment. + # + # @param [Vagrant::Environment] env + def initialize(env) + @env = env + end + + # Removes the token, effectively logging the user out. + def clear_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 + 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] user + # @param [String] pass + # @return [String] token The access token, or nil if auth failed. + def login(user, pass) + with_error_handling do + url = "#{Vagrant.server_url}/api/v1/authenticate" + request = { "user" => { "login" => user, "password" => pass } } + response = RestClient.post( + url, JSON.dump(request), content_type: :json) + data = JSON.load(response.to_s) + data["token"] + end + end + + # Stores the given token locally, removing any previous tokens. + # + # @param [String] token + def store_token(token) + token_path.open("w") do |f| + f.write(token) + end + nil + end + + # Reads the access token if there is one, or returns nil otherwise. + # + # @return [String] + def token + token_path.read + rescue Errno::ENOENT + return nil + end + + protected + + def with_error_handling(&block) + yield + rescue RestClient::Unauthorized + false + rescue RestClient::NotAcceptable => e + begin + errors = JSON.parse(e.response)["errors"] + .map { |h| h["message"] } + .join("\n") + + raise Errors::ServerError, errors: errors + rescue JSON::ParserError; end + + raise "An unexpected error occurred: #{e.inspect}" + rescue SocketError + raise Errors::ServerUnreachable, url: Vagrant.server_url.to_s + end + + def token_path + @env.data_dir.join("vagrant_login_token") + end + end + end +end diff --git a/plugins/commands/login/command.rb b/plugins/commands/login/command.rb new file mode 100644 index 000000000..e16b1afaf --- /dev/null +++ b/plugins/commands/login/command.rb @@ -0,0 +1,83 @@ +module VagrantPlugins + module LoginCommand + class Command < Vagrant.plugin("2", "command") + def self.synopsis + "log in to HashiCorp's Atlas" + end + + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant login" + o.separator "" + o.on("-c", "--check", "Only checks if you're logged in") do |c| + options[:check] = c + end + + o.on("-k", "--logout", "Logs you out if you're logged in") do |k| + options[:logout] = k + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + @client = Client.new(@env) + + # Determine what task we're actually taking based on flags + if options[:check] + return execute_check + elsif options[:logout] + return execute_logout + end + + # Let the user know what is going on. + @env.ui.output(I18n.t("login_command.command_header") + "\n") + + # If it is a private cloud installation, show that + if Vagrant.server_url != Vagrant::DEFAULT_SERVER_URL + @env.ui.output("Atlas URL: #{Vagrant.server_url}") + end + + # Ask for the username + login = nil + password = nil + while !login + login = @env.ui.ask("Atlas Username: ") + end + + while !password + password = @env.ui.ask("Password (will be hidden): ", echo: false) + end + + token = @client.login(login, password) + if !token + @env.ui.error(I18n.t("login_command.invalid_login")) + return 1 + end + + @client.store_token(token) + @env.ui.success(I18n.t("login_command.logged_in")) + 0 + end + + def execute_check + if @client.logged_in? + @env.ui.success(I18n.t("login_command.check_logged_in")) + return 0 + else + @env.ui.error(I18n.t("login_command.check_not_logged_in")) + return 1 + end + end + + def execute_logout + @client.clear_token + @env.ui.success(I18n.t("login_command.logged_out")) + return 0 + end + end + end +end diff --git a/plugins/commands/login/errors.rb b/plugins/commands/login/errors.rb new file mode 100644 index 000000000..614c37cf6 --- /dev/null +++ b/plugins/commands/login/errors.rb @@ -0,0 +1,17 @@ +module VagrantPlugins + module LoginCommand + module Errors + class Error < Vagrant::Errors::VagrantError + error_namespace("login_command.errors") + end + + class ServerError < Error + error_key(:server_error) + end + + class ServerUnreachable < Error + error_key(:server_unreachable) + end + end + end +end diff --git a/plugins/commands/login/locales/en.yml b/plugins/commands/login/locales/en.yml new file mode 100644 index 000000000..51020df1d --- /dev/null +++ b/plugins/commands/login/locales/en.yml @@ -0,0 +1,30 @@ +en: + login_command: + errors: + server_error: |- + The Atlas server responded with an not-OK response: + + %{errors} + server_unreachable: |- + The Atlas server is not currently accepting connections. Please check + your network connection and try again later. + + 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 + Atlas. 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 Atlas account, sign up at + https://atlas.hashicorp.com. + invalid_login: |- + Invalid username or password. Please try again. + logged_in: |- + You are now logged in. + logged_out: |- + You are logged out. diff --git a/plugins/commands/login/middleware/add_authentication.rb b/plugins/commands/login/middleware/add_authentication.rb new file mode 100644 index 000000000..bfb7dd46b --- /dev/null +++ b/plugins/commands/login/middleware/add_authentication.rb @@ -0,0 +1,35 @@ +require "uri" + +require_relative "../client" + +module VagrantPlugins + module LoginCommand + class AddAuthentication + def initialize(app, env) + @app = app + end + + def call(env) + client = Client.new(env[:env]) + token = client.token + + if token && Vagrant.server_url + server_uri = URI.parse(Vagrant.server_url) + + env[:box_urls].map! do |url| + u = URI.parse(url) + if u.host == server_uri.host + u.query ||= "" + u.query += "&" if u.query != "" + u.query += "access_token=#{token}" + end + + u.to_s + end + end + + @app.call(env) + end + end + end +end diff --git a/plugins/commands/login/plugin.rb b/plugins/commands/login/plugin.rb new file mode 100644 index 000000000..efb84a556 --- /dev/null +++ b/plugins/commands/login/plugin.rb @@ -0,0 +1,35 @@ +require "vagrant" + +module VagrantPlugins + module LoginCommand + autoload :Client, File.expand_path("../client", __FILE__) + autoload :Errors, File.expand_path("../errors", __FILE__) + + class Plugin < Vagrant.plugin("2") + name "vagrant-login" + description <<-DESC + Provides the login command and internal API access to Atlas. + DESC + + command(:login) do + require_relative "command" + init! + Command + end + + action_hook(:cloud_authenticated_boxes, :authenticate_box_url) do |hook| + require_relative "middleware/add_authentication" + hook.prepend(AddAuthentication) + 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/push/command.rb b/plugins/commands/push/command.rb new file mode 100644 index 000000000..d04e296fb --- /dev/null +++ b/plugins/commands/push/command.rb @@ -0,0 +1,68 @@ +require 'optparse' + +module VagrantPlugins + module CommandPush + class Command < Vagrant.plugin("2", :command) + def self.synopsis + "deploys code in this environment to a configured destination" + end + + # @todo support multiple strategies if requested by the community + def execute + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant push [strategy] [options]" + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + name = validate_pushes!(@env.pushes, argv[0]) + + @logger.debug("'push' environment with strategy: `#{name}'") + @env.push(name) + + 0 + end + + # Validate that the given list of names corresponds to valid pushes. + # + # @raise Vagrant::Errors::PushesNotDefined + # if there are no pushes defined + # @raise Vagrant::Errors::PushStrategyNotProvided + # if there are multiple push strategies defined and none were specified + # @raise Vagrant::Errors::PushStrategyNotDefined + # if the given push name do not correspond to a push strategy + # + # @param [Array] pushes + # the list of pushes defined by the environment + # @param [String] name + # the name provided by the user on the command line + # + # @return [Symbol] + # the compiled list of pushes + # + def validate_pushes!(pushes, name = nil) + if pushes.nil? || pushes.empty? + raise Vagrant::Errors::PushesNotDefined + end + + if name.nil? + if pushes.length == 1 + return pushes.first.to_sym + else + raise Vagrant::Errors::PushStrategyNotProvided, pushes: pushes + end + end + + if !pushes.include?(name) + raise Vagrant::Errors::PushStrategyNotDefined, + name: name, + pushes: pushes + end + + return name.to_sym + end + end + end +end diff --git a/plugins/commands/push/plugin.rb b/plugins/commands/push/plugin.rb new file mode 100644 index 000000000..ecd24dd7a --- /dev/null +++ b/plugins/commands/push/plugin.rb @@ -0,0 +1,17 @@ +require "vagrant" + +module VagrantPlugins + module CommandPush + class Plugin < Vagrant.plugin("2") + name "push command" + description <<-DESC + The `push` command deploys code in this environment. + DESC + + command("push") do + require File.expand_path("../command", __FILE__) + Command + end + end + end +end diff --git a/plugins/kernel_v2/config/push.rb b/plugins/kernel_v2/config/push.rb new file mode 100644 index 000000000..e0137b29d --- /dev/null +++ b/plugins/kernel_v2/config/push.rb @@ -0,0 +1,127 @@ +require "vagrant" + +module VagrantPlugins + module Kernel_V2 + class PushConfig < Vagrant.plugin("2", :config) + VALID_OPTIONS = [:strategy].freeze + + attr_accessor :name + + def initialize + @logger = Log4r::Logger.new("vagrant::config::push") + + # Internal state + @__defined_pushes = {} + @__compiled_pushes = {} + @__finalized = false + end + + def finalize! + @logger.debug("finalizing") + + # Compile all the provider configurations + @__defined_pushes.each do |name, tuples| + # Find the configuration class for this push + config_class = Vagrant.plugin("2").manager.push_configs[name] + config_class ||= Vagrant::Config::V2::DummyConfig + + # Load it up + config = config_class.new + + # Capture the strategy so we can use it later. This will be used in + # the block iteration for merging/overwriting + strategy = name + strategy = tuples[0][0] if tuples[0] + + begin + tuples.each do |s, b| + # Update the strategy if it has changed, reseting the current + # config object. + if s != strategy + @logger.warn("duplicate strategy defined, overwriting config") + strategy = s + config = config_class.new + end + + # If we don't have any blocks, then ignore it + next if b.nil? + + new_config = config_class.new + b.call(new_config, Vagrant::Config::V2::DummyConfig.new) + config = config.merge(new_config) + end + rescue Exception => e + raise Vagrant::Errors::VagrantfileLoadError, + path: "", + message: e.message + end + + config.finalize! + + # Store it for retrieval later + @__compiled_pushes[name] = [strategy, config] + end + + @__finalized = true + end + + # Define a new push in the Vagrantfile with the given name. + # + # @example + # vm.push.define "ftp" + # + # @example + # vm.push.define "ftp" do |s| + # s.host = "..." + # end + # + # @example + # vm.push.define "production", strategy: "docker" do |s| + # # ... + # end + # + # @param [#to_sym] name The name of the this strategy. By default, this + # is also the name of the strategy, but the `:strategy` key can be given + # to customize this behavior + # @param [Hash] options The list of options + # + def define(name, **options, &block) + name = name.to_sym + strategy = options[:strategy] || name + + @__defined_pushes[name] ||= [] + @__defined_pushes[name] << [strategy.to_sym, block] + end + + # The String representation of this Push. + # + # @return [String] + def to_s + "Push" + end + + # Custom merge method + def merge(other) + super.tap do |result| + other_pushes = other.instance_variable_get(:@__defined_pushes) + new_pushes = @__defined_pushes.dup + + other_pushes.each do |key, tuples| + new_pushes[key] ||= [] + new_pushes[key] += tuples + end + + result.instance_variable_set(:@__defined_pushes, new_pushes) + end + end + + # This returns the list of compiled pushes as a hash by name. + # + # @return [Hash>] + def __compiled_pushes + raise "Must finalize first!" if !@__finalized + @__compiled_pushes.dup + end + end + end +end diff --git a/plugins/kernel_v2/plugin.rb b/plugins/kernel_v2/plugin.rb index 0904481df..27737854f 100644 --- a/plugins/kernel_v2/plugin.rb +++ b/plugins/kernel_v2/plugin.rb @@ -25,6 +25,11 @@ module VagrantPlugins PackageConfig end + config("push") do + require File.expand_path("../config/push", __FILE__) + PushConfig + end + config("vagrant") do require File.expand_path("../config/vagrant", __FILE__) VagrantConfig diff --git a/plugins/pushes/atlas/config.rb b/plugins/pushes/atlas/config.rb new file mode 100644 index 000000000..d4d2da2c5 --- /dev/null +++ b/plugins/pushes/atlas/config.rb @@ -0,0 +1,147 @@ +module VagrantPlugins + module AtlasPush + class Config < Vagrant.plugin("2", :config) + # The address of the Atlas server to upload to. By default this will + # be the public Atlas server. + # + # @return [String] + attr_accessor :address + + # The Atlas token to use. If the user has run `vagrant login`, this will + # use that token. If the environment variable `ATLAS_TOKEN` is set, the + # uploader will use this value. By default, this is nil. + # + # @return [String, nil] + attr_accessor :token + + # The name of the application to push to. This will be created (with + # user confirmation) if it doesn't already exist. + # + # @return [String] + attr_accessor :app + + # The base directory with file contents to upload. By default this + # is the same directory as the Vagrantfile, but you can specify this + # if you have a `src` folder or `bin` folder or some other folder + # you want to upload. + # + # @return [String] + attr_accessor :dir + + # Lists of files to include/exclude in what is uploaded. Exclude is + # always the last run filter, so if a file is matched in both include + # and exclude, it will be excluded. + # + # The value of the array elements should be a simple file glob relative + # to the directory being packaged. + # + # @return [Array] + attr_accessor :includes + attr_accessor :excludes + + # If set to true, Vagrant will automatically use VCS data to determine + # the files to upload. As a caveat: uncommitted changes will not be + # deployed. + # + # @return [Boolean] + attr_accessor :vcs + + # The path to the uploader binary to shell out to. This usually + # is only set for debugging/development. If not set, the uploader + # will be looked for within the Vagrant installer dir followed by + # the PATH. + # + # @return [String] + attr_accessor :uploader_path + + def initialize + @address = UNSET_VALUE + @token = UNSET_VALUE + @app = UNSET_VALUE + @dir = UNSET_VALUE + @vcs = UNSET_VALUE + @includes = [] + @excludes = [] + @uploader_path = UNSET_VALUE + end + + def merge(other) + super.tap do |result| + result.includes = self.includes.dup.concat(other.includes).uniq + result.excludes = self.excludes.dup.concat(other.excludes).uniq + end + end + + def finalize! + @address = nil if @address == UNSET_VALUE + @token = nil if @token == UNSET_VALUE + @app = nil if @app == UNSET_VALUE + @dir = "." if @dir == UNSET_VALUE + @uploader_path = nil if @uploader_path == UNSET_VALUE + @vcs = true if @vcs == UNSET_VALUE + end + + def validate(machine) + errors = _detected_errors + + if missing?(@token) + token = token_from_vagrant_login(machine.env) || ENV["ATLAS_TOKEN"] + if missing?(token) + errors << I18n.t("atlas_push.errors.missing_token") + else + @token = token + end + end + + if missing?(@app) + errors << I18n.t("atlas_push.errors.missing_attribute", + attribute: "app", + ) + end + + if missing?(@dir) + errors << I18n.t("atlas_push.errors.missing_attribute", + attribute: "dir", + ) + end + + { "Atlas push" => errors } + end + + # Add the filepath to the list of includes + # @param [String] filepath + def include(filepath) + @includes << filepath + end + alias_method :include=, :include + + # Add the filepath to the list of excludes + # @param [String] filepath + def exclude(filepath) + @excludes << filepath + end + alias_method :exclude=, :exclude + + private + + # Determine if the given string is "missing" (blank) + # @return [true, false] + def missing?(obj) + obj.to_s.strip.empty? + end + + # Attempt to load the token from disk using the vagrant-login plugin. If + # the constant is not defined, that means the user is operating in some + # bespoke and unsupported Ruby environment. + # + # @param [Vagrant::Environment] env + # + # @return [String, nil] + # the token, or nil if it does not exist + def token_from_vagrant_login(env) + client = VagrantPlugins::LoginCommand::Client.new(env) + client.token + end + end + end +end diff --git a/plugins/pushes/atlas/errors.rb b/plugins/pushes/atlas/errors.rb new file mode 100644 index 000000000..7ba1d712a --- /dev/null +++ b/plugins/pushes/atlas/errors.rb @@ -0,0 +1,13 @@ +module VagrantPlugins + module AtlasPush + module Errors + class Error < Vagrant::Errors::VagrantError + error_namespace("atlas_push.errors") + end + + class UploaderNotFound < Error + error_key(:uploader_not_found) + end + end + end +end diff --git a/plugins/pushes/atlas/locales/en.yml b/plugins/pushes/atlas/locales/en.yml new file mode 100644 index 000000000..3377f62dd --- /dev/null +++ b/plugins/pushes/atlas/locales/en.yml @@ -0,0 +1,22 @@ +en: + atlas_push: + errors: + missing_attribute: |- + Missing required attribute '%{attribute}'. The Vagrant Atlas Push plugin + requires you set this attribute. Please set this attribute in your + Vagrantfile, for example: + + config.push.define "atlas" do |push| + push.%{attribute} = "..." + end + missing_token: |- + Missing required configuration parameter 'token'. This is required for + Vagrant to securely communicate with your Atlas account. + + To generate an access token, run 'vagrant login'. + uploader_not_found: |- + Vagrant was unable to find the Atlas uploader CLI. If your Vagrantfile + specifies the path explicitly with "uploader_path", then make sure that + path is valid. Otherwise, make sure that you have a valid install of + Vagrant. If you installed Vagrant outside of the official installers, + the "atlas-upload" binary must exist on your PATH. diff --git a/plugins/pushes/atlas/plugin.rb b/plugins/pushes/atlas/plugin.rb new file mode 100644 index 000000000..2eee2517d --- /dev/null +++ b/plugins/pushes/atlas/plugin.rb @@ -0,0 +1,35 @@ +require "vagrant" + +module VagrantPlugins + module AtlasPush + autoload :Errors, File.expand_path("../errors", __FILE__) + + class Plugin < Vagrant.plugin("2") + name "atlas" + description <<-DESC + Deploy using HashiCorp's Atlas service. + DESC + + config(:atlas, :push) do + require_relative "config" + init! + Config + end + + push(:atlas) do + require_relative "push" + init! + Push + 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/pushes/atlas/push.rb b/plugins/pushes/atlas/push.rb new file mode 100644 index 000000000..24a6d28b0 --- /dev/null +++ b/plugins/pushes/atlas/push.rb @@ -0,0 +1,57 @@ +require "vagrant/util/safe_exec" +require "vagrant/util/subprocess" +require "vagrant/util/which" + +module VagrantPlugins + module AtlasPush + class Push < Vagrant.plugin("2", :push) + UPLOADER_BIN = "atlas-upload".freeze + + def push + uploader = self.uploader_path + + # If we didn't find the uploader binary it is a critical error + raise Errors::UploaderNotFound if !uploader + + # We found it. Build up the command and the args. + execute(uploader) + return 0 + end + + # Executes the uploader with the proper flags based on the configuration. + # This function shouldn't return since it will exec, but might return + # if we're on a system that doesn't support exec, so handle that properly. + def execute(uploader) + cmd = [] + cmd << "-vcs" if config.vcs + cmd += config.includes.map { |v| ["-include", v] } + cmd += config.excludes.map { |v| ["-exclude", v] } + cmd += ["-address", config.address] if config.address + cmd += ["-token", config.token] if config.token + cmd << config.app + cmd << File.expand_path(config.dir, env.root_path) + Vagrant::Util::SafeExec.exec(uploader, *cmd.flatten) + end + + # This returns the path to the uploader binary, or nil if it can't + # be found. + # + # @return [String] + def uploader_path + # Determine the uploader path + uploader = config.uploader_path + if uploader + return uploader + end + + if Vagrant.in_installer? + path = File.join( + Vagrant.installer_embedded_dir, "bin", UPLOADER_BIN) + return path if File.file?(path) + end + + return Vagrant::Util::Which.which(UPLOADER_BIN) + end + end + end +end diff --git a/plugins/pushes/ftp/adapter.rb b/plugins/pushes/ftp/adapter.rb new file mode 100644 index 000000000..f370d411f --- /dev/null +++ b/plugins/pushes/ftp/adapter.rb @@ -0,0 +1,107 @@ +module VagrantPlugins + module FTPPush + class Adapter + attr_reader :host + attr_reader :port + attr_reader :username + attr_reader :password + attr_reader :options + attr_reader :server + + def initialize(host, username, password, options = {}) + @host, @port = parse_host(host) + @username = username + @password = password + @options = options + @server = nil + end + + # Parse the host into it's url and port parts. + # @return [Array] + def parse_host(host) + if host.include?(":") + split = host.split(":", 2) + [split[0], split[1].to_i] + else + [host, default_port] + end + end + + def default_port + raise NotImplementedError + end + + def connect(&block) + raise NotImplementedError + end + + def upload(local, remote) + raise NotImplementedError + end + end + + # + # The FTP Adapter + # + class FTPAdapter < Adapter + def initialize(*) + require "net/ftp" + super + end + + def default_port + 20 + end + + def connect(&block) + @server = Net::FTP.new + @server.passive = options.fetch(:passive, true) + @server.connect(host, port) + @server.login(username, password) + + begin + yield self + ensure + @server.close + end + end + + def upload(local, remote) + parent = File.dirname(remote) + + # Create the parent directory if it does not exist + if !@server.list("/").any? { |f| f.start_with?(parent) } + @server.mkdir(parent) + end + + # Upload the file + @server.putbinaryfile(local, remote) + end + end + + # + # The SFTP Adapter + # + class SFTPAdapter < Adapter + def initialize(*) + require "net/sftp" + super + end + + def default_port + 22 + end + + def connect(&block) + Net::SFTP.start(@host, @username, password: @password, port: @port) do |server| + @server = server + yield self + end + end + + def upload(local, remote) + @server.upload!(local, remote, mkdir: true) + end + end + end +end diff --git a/plugins/pushes/ftp/config.rb b/plugins/pushes/ftp/config.rb new file mode 100644 index 000000000..e493259db --- /dev/null +++ b/plugins/pushes/ftp/config.rb @@ -0,0 +1,130 @@ +module VagrantPlugins + module FTPPush + class Config < Vagrant.plugin("2", :config) + # The (S)FTP host to use. + # @return [String] + attr_accessor :host + + # The username to use for authentication with the (S)FTP server. + # @return [String] + attr_accessor :username + + # The password to use for authentication with the (S)FTP server. + # @return [String] + attr_accessor :password + + # Use passive FTP (default is true). + # @return [true, false] + attr_accessor :passive + + # Use secure (SFTP) (default is false). + # @return [true, false] + attr_accessor :secure + + # The root destination on the target system to sync the files (default is + # /). + # @return [String] + attr_accessor :destination + + # Lists of files to include/exclude in what is uploaded. Exclude is + # always the last run filter, so if a file is matched in both include + # and exclude, it will be excluded. + # + # The value of the array elements should be a simple file glob relative + # to the directory being packaged. + # @return [Array] + attr_accessor :includes + attr_accessor :excludes + + # The base directory with file contents to upload. By default this + # is the same directory as the Vagrantfile, but you can specify this + # if you have a `src` folder or `bin` folder or some other folder + # you want to upload. + # @return [String] + attr_accessor :dir + + def initialize + @host = UNSET_VALUE + @username = UNSET_VALUE + @password = UNSET_VALUE + @passive = UNSET_VALUE + @secure = UNSET_VALUE + @destination = UNSET_VALUE + + @includes = [] + @excludes = [] + + @dir = UNSET_VALUE + end + + def merge(other) + super.tap do |result| + result.includes = self.includes.dup.concat(other.includes).uniq + result.excludes = self.excludes.dup.concat(other.excludes).uniq + end + end + + def finalize! + @host = nil if @host == UNSET_VALUE + @username = nil if @username == UNSET_VALUE + @password = nil if @password == UNSET_VALUE + @passive = true if @passive == UNSET_VALUE + @secure = false if @secure == UNSET_VALUE + @destination = "/" if @destination == UNSET_VALUE + @dir = "." if @dir == UNSET_VALUE + end + + def validate(machine) + errors = _detected_errors + + if missing?(@host) + errors << I18n.t("ftp_push.errors.missing_attribute", + attribute: "host", + ) + end + + if missing?(@username) + errors << I18n.t("ftp_push.errors.missing_attribute", + attribute: "username", + ) + end + + if missing?(@destination) + errors << I18n.t("ftp_push.errors.missing_attribute", + attribute: "destination", + ) + end + + if missing?(@dir) + errors << I18n.t("ftp_push.errors.missing_attribute", + attribute: "dir", + ) + end + + { "FTP push" => errors } + end + + # Add the filepath to the list of includes + # @param [String] filepath + def include(filepath) + @includes << filepath + end + alias_method :include=, :include + + # Add the filepath to the list of excludes + # @param [String] filepath + def exclude(filepath) + @excludes << filepath + end + alias_method :exclude=, :exclude + + private + + # Determine if the given string is "missing" (blank) + # @return [true, false] + def missing?(obj) + obj.to_s.strip.empty? + end + end + end +end diff --git a/plugins/pushes/ftp/locales/en.yml b/plugins/pushes/ftp/locales/en.yml new file mode 100644 index 000000000..0bbbe51f1 --- /dev/null +++ b/plugins/pushes/ftp/locales/en.yml @@ -0,0 +1,11 @@ +en: + ftp_push: + errors: + missing_attribute: |- + Missing required attribute '%{attribute}'. The Vagrant FTP Push plugin + requires you set this attribute. Please set this attribute in your + Vagrantfile, for example: + + config.push.define "ftp" do |push| + push.%{attribute} = "..." + end diff --git a/plugins/pushes/ftp/plugin.rb b/plugins/pushes/ftp/plugin.rb new file mode 100644 index 000000000..ef333807b --- /dev/null +++ b/plugins/pushes/ftp/plugin.rb @@ -0,0 +1,33 @@ +require "vagrant" + +module VagrantPlugins + module FTPPush + class Plugin < Vagrant.plugin("2") + name "ftp" + description <<-DESC + Deploy to a remote FTP or SFTP server. + DESC + + config(:ftp, :push) do + require File.expand_path("../config", __FILE__) + init! + Config + end + + push(:ftp) do + require File.expand_path("../push", __FILE__) + init! + Push + 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/pushes/ftp/push.rb b/plugins/pushes/ftp/push.rb new file mode 100644 index 000000000..3cf4169a0 --- /dev/null +++ b/plugins/pushes/ftp/push.rb @@ -0,0 +1,116 @@ +require "net/ftp" +require "pathname" + +require_relative "adapter" + +module VagrantPlugins + module FTPPush + class Push < Vagrant.plugin("2", :push) + IGNORED_FILES = %w(. ..).freeze + + def push + # Grab files early so if there's an exception or issue, we don't have to + # wait and close the (S)FTP connection as well + files = Hash[*all_files.flat_map do |file| + relative_path = relative_path_for(file, config.dir) + destination = File.expand_path(File.join(config.destination, relative_path)) + [file, destination] + end] + + connect do |ftp| + files.each do |local, remote| + ftp.upload(local, remote) + end + end + end + + # Helper method for creating the FTP or SFTP connection. + # @yield [Adapter] + def connect(&block) + klass = config.secure ? SFTPAdapter : FTPAdapter + ftp = klass.new(config.host, config.username, config.password, + passive: config.passive) + ftp.connect(&block) + end + + # Parse the host into it's url and port parts. + # @return [Array] + def parse_host(host) + if host.include?(":") + host.split(":", 2) + else + [host, "22"] + end + end + + # The list of all files that should be pushed by this push. This method + # only returns **files**, not folders or symlinks! + # @return [Array] + def all_files + files = glob("#{config.dir}/**/*") + includes_files + filter_excludes!(files, config.excludes) + files.reject! { |f| !File.file?(f) } + files + end + + # The list of files to include in addition to those specified in `dir`. + # @return [Array] + def includes_files + includes = config.includes.flat_map do |i| + path = absolute_path_for(i, config.dir) + [path, "#{path}/**/*"] + end + + glob("{#{includes.join(",")}}") + end + + # Filter the excludes out of the given list. This method modifies the + # given list in memory! + # + # @param [Array] list + # the filepaths + # @param [Array] excludes + # the exclude patterns or files + def filter_excludes!(list, excludes) + excludes = Array(excludes).flat_map { |e| [e, "#{e}/*"] } + list.reject! do |file| + basename = relative_path_for(file, config.dir) + + # Handle the special case where the file is outside of the working + # directory... + if basename.start_with?("../") + basename = file + end + + excludes.any? { |e| File.fnmatch?(e, basename, File::FNM_DOTMATCH) } + end + end + + # Get the list of files that match the given pattern. + # @return [Array] + def glob(pattern) + Dir.glob(pattern, File::FNM_DOTMATCH).sort.reject do |file| + IGNORED_FILES.include?(File.basename(file)) + end + end + + # The absolute path to the given `path` and `parent`, unless the given + # path is absolute. + # @return [String] + def absolute_path_for(path, parent) + path = Pathname.new(path) + return path if path.absolute? + File.expand_path(path, parent) + end + + # The relative path from the given `parent`. If files exist on another + # device, this will probably blow up. + # @return [String] + def relative_path_for(path, parent) + Pathname.new(path).relative_path_from(Pathname.new(parent)).to_s + rescue ArgumentError + return path + end + end + end +end diff --git a/plugins/pushes/heroku/config.rb b/plugins/pushes/heroku/config.rb new file mode 100644 index 000000000..927c1a7b9 --- /dev/null +++ b/plugins/pushes/heroku/config.rb @@ -0,0 +1,74 @@ +module VagrantPlugins + module HerokuPush + class Config < Vagrant.plugin("2", :config) + # The name of the Heroku application to push to. + # @return [String] + attr_accessor :app + + # The base directory with file contents to upload. By default this + # is the same directory as the Vagrantfile, but you can specify this + # if you have a `src` folder or `bin` folder or some other folder + # you want to upload. This directory must be a git repository. + # @return [String] + attr_accessor :dir + + # The path to the git binary to shell out to. This usually is only set for + # debugging/development. If not set, the git bin will be searched for + # in the PATH. + # @return [String] + attr_accessor :git_bin + + # The Git remote to push to (default: "heroku"). + # @return [String] + attr_accessor :remote + + def initialize + @app = UNSET_VALUE + @dir = UNSET_VALUE + + @git_bin = UNSET_VALUE + @remote = UNSET_VALUE + end + + def finalize! + @app = nil if @app == UNSET_VALUE + @dir = "." if @dir == UNSET_VALUE + + @git_bin = "git" if @git_bin == UNSET_VALUE + @remote = "heroku" if @remote == UNSET_VALUE + end + + def validate(machine) + errors = _detected_errors + + if missing?(@dir) + errors << I18n.t("heroku_push.errors.missing_attribute", + attribute: "dir", + ) + end + + if missing?(@git_bin) + errors << I18n.t("heroku_push.errors.missing_attribute", + attribute: "git_bin", + ) + end + + if missing?(@remote) + errors << I18n.t("heroku_push.errors.missing_attribute", + attribute: "remote", + ) + end + + { "Heroku push" => errors } + end + + private + + # Determine if the given string is "missing" (blank) + # @return [true, false] + def missing?(obj) + obj.to_s.strip.empty? + end + end + end +end diff --git a/plugins/pushes/heroku/errors.rb b/plugins/pushes/heroku/errors.rb new file mode 100644 index 000000000..c92a7b76c --- /dev/null +++ b/plugins/pushes/heroku/errors.rb @@ -0,0 +1,21 @@ +module VagrantPlugins + module HerokuPush + module Errors + class Error < Vagrant::Errors::VagrantError + error_namespace("heroku_push.errors") + end + + class CommandFailed < Error + error_key(:command_failed) + end + + class GitNotFound < Error + error_key(:git_not_found) + end + + class NotAGitRepo < Error + error_key(:not_a_git_repo) + end + end + end +end diff --git a/plugins/pushes/heroku/locales/en.yml b/plugins/pushes/heroku/locales/en.yml new file mode 100644 index 000000000..e6f6bf441 --- /dev/null +++ b/plugins/pushes/heroku/locales/en.yml @@ -0,0 +1,30 @@ +en: + heroku_push: + errors: + command_failed: |- + The following command exited with a non-zero exit status: + + %{cmd} + + stdout: %{stdout} + stderr: %{stderr} + git_not_found: |- + The Git binary '%{bin}' could not be found. Please ensure you + have downloaded and installed the latest version of Git: + + http://git-scm.com/downloads + missing_attribute: |- + Missing required attribute '%{attribute}'. The Vagrant Heroku Push + plugin requires you set this attribute. Please set this attribute in + your Vagrantfile, for example: + + config.push.define "heroku" do |push| + push.%{attribute} = "..." + end + not_a_git_repo: |- + The following path is not a valid Git repository: + + %{path} + + Please ensure you are working in the correct directory. In order to use + the Vagrant Heroku Push plugin, you must have a git repository. diff --git a/plugins/pushes/heroku/plugin.rb b/plugins/pushes/heroku/plugin.rb new file mode 100644 index 000000000..40865ee5d --- /dev/null +++ b/plugins/pushes/heroku/plugin.rb @@ -0,0 +1,33 @@ +require "vagrant" + +module VagrantPlugins + module HerokuPush + class Plugin < Vagrant.plugin("2") + name "heroku" + description <<-DESC + Deploy to a Heroku + DESC + + config(:heroku, :push) do + require File.expand_path("../config", __FILE__) + init! + Config + end + + push(:heroku) do + require File.expand_path("../push", __FILE__) + init! + Push + 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/pushes/heroku/push.rb b/plugins/pushes/heroku/push.rb new file mode 100644 index 000000000..b894eba81 --- /dev/null +++ b/plugins/pushes/heroku/push.rb @@ -0,0 +1,136 @@ +require "vagrant/util/subprocess" +require "vagrant/util/which" + +require_relative "errors" + +module VagrantPlugins + module HerokuPush + class Push < Vagrant.plugin("2", :push) + def push + # Expand any paths relative to the root + dir = File.expand_path(config.dir, env.root_path) + + # Verify git is installed + verify_git_bin!(config.git_bin) + + # Verify we are operating in a git repo + verify_git_repo!(dir) + + # Get the current branch + branch = git_branch(dir) + + # Get the name of the app + app = config.app || interpret_app(dir) + + # Check if we need to add the git remote + if !has_git_remote?(config.remote, dir) + add_heroku_git_remote(config.remote, app, dir) + end + + # Push to Heroku + git_push_heroku(config.remote, branch, dir) + end + + # Verify that git is installed. + # @raise [Errors::GitNotFound] + def verify_git_bin!(path) + if Vagrant::Util::Which.which(path).nil? + raise Errors::GitNotFound, bin: path + end + end + + # Verify that the given path is a git directory. + # @raise [Errors::NotAGitRepo] + # @param [String] + def verify_git_repo!(path) + if !File.directory?(git_dir(path)) + raise Errors::NotAGitRepo, path: path + end + end + + # Interpret the name of the Heroku application from the given path. + # @param [String] path + # @return [String] + def interpret_app(path) + File.basename(path) + end + + # The git directory for the given path. + # @param [String] path + # @return [String] + def git_dir(path) + "#{path}/.git" + end + + # The name of the current git branch. + # @param [String] path + # @return [String] + def git_branch(path) + result = execute!("git", + "--git-dir", git_dir(path), + "--work-tree", path, + "branch", + ) + + # Returns something like "* master" + result.stdout.sub("*", "").strip + end + + # Push to the Heroku remote. + # @param [String] remote + # @param [String] branch + def git_push_heroku(remote, branch, path) + execute!("git", + "--git-dir", git_dir(path), + "--work-tree", path, + "push", remote, "#{branch}:master", + ) + end + + # Check if the git remote has the given remote. + # @param [String] remote + # @return [true, false] + def has_git_remote?(remote, path) + result = execute!("git", + "--git-dir", git_dir(path), + "--work-tree", path, + "remote", + ) + remotes = result.stdout.split(/\r?\n/).map(&:strip) + remotes.include?(remote.to_s) + end + + # Add the Heroku to the current repository. + # @param [String] remote + # @param [String] app + def add_heroku_git_remote(remote, app, path) + execute!("git", + "--git-dir", git_dir(path), + "--work-tree", path, + "remote", "add", remote, heroku_git_url(app), + ) + end + + # The URL for this project on Heroku. + # @return [String] + def heroku_git_url(app) + "git@heroku.com:#{app}.git" + end + + # Execute the command, raising an exception if it fails. + # @return [Vagrant::Util::Subprocess::Result] + def execute!(*cmd) + result = Vagrant::Util::Subprocess.execute(*cmd) + + if result.exit_code != 0 + raise Errors::CommandFailed, + cmd: cmd.join(" "), + stdout: result.stdout, + stderr: result.stderr + end + + result + end + end + end +end diff --git a/plugins/pushes/local-exec/config.rb b/plugins/pushes/local-exec/config.rb new file mode 100644 index 000000000..747ff8925 --- /dev/null +++ b/plugins/pushes/local-exec/config.rb @@ -0,0 +1,48 @@ +module VagrantPlugins + module LocalExecPush + class Config < Vagrant.plugin("2", :config) + # The path (relative to the machine root) to a local script that will be + # executed. + # @return [String] + attr_accessor :script + + # The command (as a string) to execute. + # @return [String] + attr_accessor :inline + + def initialize + @script = UNSET_VALUE + @inline = UNSET_VALUE + end + + def finalize! + @script = nil if @script == UNSET_VALUE + @inline = nil if @inline == UNSET_VALUE + end + + def validate(machine) + errors = _detected_errors + + if missing?(@script) && missing?(@inline) + errors << I18n.t("local_exec_push.errors.missing_attribute", + attribute: "script", + ) + end + + if !missing?(@script) && !missing?(@inline) + errors << I18n.t("local_exec_push.errors.cannot_specify_script_and_inline") + end + + { "Local Exec push" => errors } + end + + private + + # Determine if the given string is "missing" (blank) + # @return [true, false] + def missing?(obj) + obj.to_s.strip.empty? + end + end + end +end diff --git a/plugins/pushes/local-exec/errors.rb b/plugins/pushes/local-exec/errors.rb new file mode 100644 index 000000000..5e5b71cca --- /dev/null +++ b/plugins/pushes/local-exec/errors.rb @@ -0,0 +1,13 @@ +module VagrantPlugins + module LocalExecPush + module Errors + class Error < Vagrant::Errors::VagrantError + error_namespace("local_exec_push.errors") + end + + class CommandFailed < Error + error_key(:command_failed) + end + end + end +end diff --git a/plugins/pushes/local-exec/locales/en.yml b/plugins/pushes/local-exec/locales/en.yml new file mode 100644 index 000000000..08808a879 --- /dev/null +++ b/plugins/pushes/local-exec/locales/en.yml @@ -0,0 +1,22 @@ +en: + local_exec_push: + errors: + cannot_specify_script_and_inline: |- + You have specified both the 'script' and 'inline' attributes for the + Vagrant Local Exec Push plugin. You may only specify one of these + attributes. + command_failed: |- + The following command exited with a non-zero exit status: + + %{cmd} + + stdout: %{stdout} + stderr: %{stderr} + missing_attribute: |- + Missing required attribute '%{attribute}'. The Vagrant Local Exec Push + plugin requires you set this attribute. Please set this attribute in + your Vagrantfile, for example: + + config.push.define "local-exec" do |push| + push.%{attribute} = "..." + end diff --git a/plugins/pushes/local-exec/plugin.rb b/plugins/pushes/local-exec/plugin.rb new file mode 100644 index 000000000..9f8be467b --- /dev/null +++ b/plugins/pushes/local-exec/plugin.rb @@ -0,0 +1,33 @@ +require "vagrant" + +module VagrantPlugins + module LocalExecPush + class Plugin < Vagrant.plugin("2") + name "local-exec" + description <<-DESC + Run a local command or script to push + DESC + + config(:local_exec, :push) do + require File.expand_path("../config", __FILE__) + init! + Config + end + + push(:local_exec) do + require File.expand_path("../push", __FILE__) + init! + Push + 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/pushes/local-exec/push.rb b/plugins/pushes/local-exec/push.rb new file mode 100644 index 000000000..be74e68d5 --- /dev/null +++ b/plugins/pushes/local-exec/push.rb @@ -0,0 +1,54 @@ +require "fileutils" +require "tempfile" +require "vagrant/util/subprocess" + +require_relative "errors" + +module VagrantPlugins + module LocalExecPush + class Push < Vagrant.plugin("2", :push) + def push + if config.inline + execute_inline!(config.inline) + else + execute_script!(config.script) + end + end + + # Execute the inline script by writing it to a tempfile and executing. + def execute_inline!(inline) + script = Tempfile.new(["vagrant-local-exec-script", ".sh"]) + script.write(inline) + script.rewind + + execute_script!(script.path) + ensure + if script + script.close + script.unlink + end + end + + # Execute the script, expanding the path relative to the current env root. + def execute_script!(path) + path = File.expand_path(path, env.root_path) + FileUtils.chmod("+x", path) + execute!(path) + end + + # Execute the script, raising an exception if it fails. + def execute!(*cmd) + result = Vagrant::Util::Subprocess.execute(*cmd) + + if result.exit_code != 0 + raise Errors::CommandFailed, + cmd: cmd.join(" "), + stdout: result.stdout, + stderr: result.stderr + end + + result + end + end + end +end diff --git a/plugins/pushes/noop/config.rb b/plugins/pushes/noop/config.rb new file mode 100644 index 000000000..9b23fb450 --- /dev/null +++ b/plugins/pushes/noop/config.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module NoopDeploy + class Config < Vagrant.plugin("2", :config) + def initialize + end + + def finalize! + end + + def validate(machine) + errors = _detected_errors + { "Noop push" => errors } + end + end + end +end diff --git a/plugins/pushes/noop/plugin.rb b/plugins/pushes/noop/plugin.rb new file mode 100644 index 000000000..d3dc6994d --- /dev/null +++ b/plugins/pushes/noop/plugin.rb @@ -0,0 +1,22 @@ +require "vagrant" + +module VagrantPlugins + module NoopDeploy + class Plugin < Vagrant.plugin("2") + name "noop" + description <<-DESC + Literally do nothing + DESC + + config(:noop, :push) do + require File.expand_path("../config", __FILE__) + Config + end + + push(:noop) do + require File.expand_path("../push", __FILE__) + Push + end + end + end +end diff --git a/plugins/pushes/noop/push.rb b/plugins/pushes/noop/push.rb new file mode 100644 index 000000000..537ea1c7d --- /dev/null +++ b/plugins/pushes/noop/push.rb @@ -0,0 +1,9 @@ +module VagrantPlugins + module NoopDeploy + class Push < Vagrant.plugin("2", :push) + def push + puts "pushed" + end + end + end +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 72124f7e9..e28106a20 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -369,7 +369,7 @@ en: provider. Double-check your requested provider to verify you didn't simply misspell it. - If you're adding a box from Vagrant Cloud, make sure the box is + If you're adding a box from HashiCorp's Atlas, make sure the box is released. Name: %{name} @@ -388,7 +388,7 @@ en: box_add_short_not_found: |- The box '%{name}' could not be found or could not be accessed in the remote catalog. If this is a private - box on Vagrant Cloud, please verify you're logged in via + box on HashiCorp's Atlas, please verify you're logged in via `vagrant login`. Also, please double-check the name. The expanded URL and error message are shown below: @@ -550,16 +550,14 @@ en: %{versions} box_server_not_set: |- - A URL to a Vagrant Cloud server is not set, so boxes cannot - be added with a shorthand ("mitchellh/precise64") format. - You may also be seeing this error if you meant to type in - a path to a box file which doesn't exist locally on your - system. + A URL to an Atlas server is not set, so boxes cannot be added with a + shorthand ("mitchellh/precise64") format. You may also be seeing this + error if you meant to type in a path to a box file which doesn't exist + locally on your system. - To set a URL to a Vagrant Cloud server, set the - `VAGRANT_SERVER_URL` environmental variable. Or, if you - meant to use a file path, make sure the path to the file - is valid. + To set a URL to an Atlas server, set the `VAGRANT_SERVER_URL` + environmental variable. Or, if you meant to use a file path, make sure + the path to the file is valid. box_update_multi_provider: |- You requested to update the box '%{name}'. This box has multiple providers. You must explicitly select a single @@ -946,6 +944,29 @@ en: You can however, install a plugin with the same name to replace these plugins. User-installed plugins take priority over system-installed plugins. + pushes_not_defined: |- + The Vagrantfile does not define any 'push' strategies. In order to use + `vagrant push`, you must define at least one push strategy: + + config.push.define "ftp" do |push| + # ... push-specific options + end + push_strategy_not_defined: |- + The push strategy '%{name}' is not defined in the Vagrantfile. Defined + strategy names are: + + %{pushes} + push_strategy_not_loaded: |- + There are no push strategies named '%{name}'. Please make sure you + spelled it correctly. If you are using an external push strategy, you + may need to install a plugin. Loaded push strategies are: + + %{pushes} + push_strategy_not_provided: |- + The Vagrantfile defines more than one 'push' strategy. Please specify a + strategy. Defined strategy names are: + + %{pushes} package_include_symlink: |- A file or directory you're attempting to include with your packaged box has symlinks in it. Vagrant cannot include symlinks in the @@ -1892,3 +1913,7 @@ en: You must include both public and private keys. must_accept_keys: |- You must accept keys when running highstate with master! + + pushes: + file: + no_destination: "File destination must be specified." diff --git a/test/unit/base.rb b/test/unit/base.rb index c7b11b958..e3c68c099 100644 --- a/test/unit/base.rb +++ b/test/unit/base.rb @@ -4,6 +4,7 @@ require "rubygems" # Gems require "checkpoint" require "rspec/autorun" +require "webmock/rspec" # Require Vagrant itself so we can reference the proper # classes to test. diff --git a/test/unit/plugins/commands/login/client_test.rb b/test/unit/plugins/commands/login/client_test.rb new file mode 100644 index 000000000..7c9480442 --- /dev/null +++ b/test/unit/plugins/commands/login/client_test.rb @@ -0,0 +1,109 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/login/command") + +describe VagrantPlugins::LoginCommand::Client do + include_context "unit" + + let(:env) { isolated_environment.create_vagrant_env } + + subject { described_class.new(env) } + + describe "#logged_in?" do + it "quickly returns false if no token is set" do + expect(subject).to_not be_logged_in + end + + it "returns true if the endpoint returns 200" do + subject.store_token("foo") + + response = { + "token" => "baz", + } + + headers = { "Content-Type" => "application/json" } + url = "#{Vagrant.server_url}/api/v1/authenticate?access_token=foo" + stub_request(:get, url). + with(headers: headers). + to_return(status: 200, body: JSON.dump(response)) + + expect(subject).to be_logged_in + end + + it "returns false if 401 is returned" do + subject.store_token("foo") + + url = "#{Vagrant.server_url}/api/v1/authenticate?access_token=foo" + stub_request(:get, url). + to_return(status: 401, body: "") + + expect(subject).to_not be_logged_in + end + + it "raises an exception if it can't reach the sever" do + subject.store_token("foo") + + url = "#{Vagrant.server_url}/api/v1/authenticate?access_token=foo" + stub_request(:get, url).to_raise(SocketError) + + expect { subject.logged_in? }. + to raise_error(VagrantPlugins::LoginCommand::Errors::ServerUnreachable) + end + end + + describe "#login" do + it "returns the access token after successful login" do + request = { + "user" => { + "login" => "foo", + "password" => "bar", + }, + } + + response = { + "token" => "baz", + } + + headers = { "Content-Type" => "application/json" } + + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + with(body: JSON.dump(request), headers: headers). + to_return(status: 200, body: JSON.dump(response)) + + expect(subject.login("foo", "bar")).to eq("baz") + end + + it "returns nil on bad login" do + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + to_return(status: 401, body: "") + + expect(subject.login("foo", "bar")).to be(false) + end + + it "raises an exception if it can't reach the sever" do + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + to_raise(SocketError) + + expect { subject.login("foo", "bar") }. + to raise_error(VagrantPlugins::LoginCommand::Errors::ServerUnreachable) + end + end + + describe "#token, #store_token, #clear_token" do + it "returns nil if there is no token" do + expect(subject.token).to be_nil + end + + 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/login/middleware/add_authentication_test.rb b/test/unit/plugins/commands/login/middleware/add_authentication_test.rb new file mode 100644 index 000000000..826e5d46b --- /dev/null +++ b/test/unit/plugins/commands/login/middleware/add_authentication_test.rb @@ -0,0 +1,64 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/login/middleware/add_authentication") + +describe VagrantPlugins::LoginCommand::AddAuthentication do + include_context "unit" + + let(:app) { lambda { |env| } } + let(:env) { { + env: iso_env, + } } + + let(:iso_env) { isolated_environment.create_vagrant_env } + let(:server_url) { "http://foo.com" } + + subject { described_class.new(app, env) } + + before do + allow(Vagrant).to receive(:server_url).and_return(server_url) + end + + describe "#call" do + it "does nothing if we have no server set" do + allow(Vagrant).to receive(:server_url).and_return(nil) + VagrantPlugins::LoginCommand::Client.new(iso_env).store_token("foo") + + original = ["foo", "#{server_url}/bar"] + env[:box_urls] = original.dup + + subject.call(env) + + expect(env[:box_urls]).to eq(original) + end + + it "does nothing if we aren't logged in" do + original = ["foo", "#{server_url}/bar"] + env[:box_urls] = original.dup + + subject.call(env) + + expect(env[:box_urls]).to eq(original) + end + + it "appends the access token to the URL of server URLs" do + token = "foobarbaz" + VagrantPlugins::LoginCommand::Client.new(iso_env).store_token(token) + + original = [ + "http://google.com/box.box", + "#{server_url}/foo.box", + "#{server_url}/bar.box?arg=true", + ] + + expected = original.dup + expected[1] = "#{original[1]}?access_token=#{token}" + expected[2] = "#{original[2]}&access_token=#{token}" + + env[:box_urls] = original.dup + subject.call(env) + + expect(env[:box_urls]).to eq(expected) + end + end +end diff --git a/test/unit/plugins/commands/push/command_test.rb b/test/unit/plugins/commands/push/command_test.rb new file mode 100644 index 000000000..06995d51d --- /dev/null +++ b/test/unit/plugins/commands/push/command_test.rb @@ -0,0 +1,118 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/push/command") + +describe VagrantPlugins::CommandPush::Command do + include_context "unit" + include_context "command plugin helpers" + + let(:env) do + isolated_environment.tap do |env| + env.vagrantfile("") + env.create_vagrant_env + end + end + + let(:argv) { [] } + let(:pushes) { {} } + + subject { described_class.new(argv, env) } + + before do + Vagrant.plugin("2").manager.stub(pushes: pushes) + end + + describe "#execute" do + before do + allow(subject).to receive(:validate_pushes!) + .and_return(:noop) + allow(env).to receive(:pushes) + allow(env).to receive(:push) + end + + it "validates the pushes" do + expect(subject).to receive(:validate_pushes!).once + subject.execute + end + + it "delegates to Environment#push" do + expect(env).to receive(:push).once + subject.execute + end + end + + describe "#validate_pushes!" do + context "when there are no pushes defined" do + let(:pushes) { [] } + + context "when a strategy is given" do + it "raises an exception" do + expect { subject.validate_pushes!(pushes, :noop) } + .to raise_error(Vagrant::Errors::PushesNotDefined) + end + end + + context "when no strategy is given" do + it "raises an exception" do + expect { subject.validate_pushes!(pushes) } + .to raise_error(Vagrant::Errors::PushesNotDefined) + end + end + end + + context "when there is one push defined" do + let(:noop) { double("noop") } + let(:pushes) { [:noop] } + + context "when a strategy is given" do + context "when that strategy is not defined" do + it "raises an exception" do + expect { subject.validate_pushes!(pushes, :bacon) } + .to raise_error(Vagrant::Errors::PushStrategyNotDefined) + end + end + + context "when that strategy is defined" do + it "returns that push" do + expect(subject.validate_pushes!(pushes, :noop)).to eq(:noop) + end + end + end + + context "when no strategy is given" do + it "returns the strategy" do + expect(subject.validate_pushes!(pushes)).to eq(:noop) + end + end + end + + context "when there are multiple pushes defined" do + let(:noop) { double("noop") } + let(:ftp) { double("ftp") } + let(:pushes) { [:noop, :ftp] } + + context "when a strategy is given" do + context "when that strategy is not defined" do + it "raises an exception" do + expect { subject.validate_pushes!(pushes, :bacon) } + .to raise_error(Vagrant::Errors::PushStrategyNotDefined) + end + end + + context "when that strategy is defined" do + it "returns the strategy" do + expect(subject.validate_pushes!(pushes, :noop)).to eq(:noop) + expect(subject.validate_pushes!(pushes, :ftp)).to eq(:ftp) + end + end + end + + context "when no strategy is given" do + it "raises an exception" do + expect { subject.validate_pushes!(pushes) } + .to raise_error(Vagrant::Errors::PushStrategyNotProvided) + end + end + end + end +end diff --git a/test/unit/plugins/kernel_v2/config/push_test.rb b/test/unit/plugins/kernel_v2/config/push_test.rb new file mode 100644 index 000000000..28430d171 --- /dev/null +++ b/test/unit/plugins/kernel_v2/config/push_test.rb @@ -0,0 +1,300 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/kernel_v2/config/push") + +describe VagrantPlugins::Kernel_V2::PushConfig do + include_context "unit" + + subject { described_class.new } + + describe "#define" do + let(:pushes) { subject.instance_variable_get(:@__defined_pushes) } + + it "pushes the strategy and block onto the defined pushes array" do + subject.define("foo") { "bar" } + subject.define("foo") { "zip" } + subject.define("foo") { "zap" } + + expect(pushes.size).to eq(1) + expect(pushes[:foo].size).to eq(3) + expect(pushes[:foo][0]).to be_a(Array) + expect(pushes[:foo][0][0]).to eq(:foo) + expect(pushes[:foo][0][1]).to be_a(Proc) + end + + context "when no strategy is given" do + it "defaults to the name" do + subject.define("foo") { "bar" } + + expect(pushes.size).to eq(1) + expect(pushes[:foo].size).to eq(1) + expect(pushes[:foo][0]).to be_a(Array) + expect(pushes[:foo][0][0]).to eq(:foo) + expect(pushes[:foo][0][1]).to be_a(Proc) + end + end + + context "when a strategy is given" do + it "uses the strategy" do + subject.define("foo", strategy: "bacon") { "bar" } + + expect(pushes.size).to eq(1) + expect(pushes[:foo].size).to eq(1) + expect(pushes[:foo][0]).to be_a(Array) + expect(pushes[:foo][0][0]).to eq(:bacon) + expect(pushes[:foo][0][1]).to be_a(Proc) + end + end + end + + describe "#merge" do + it "appends defined pushes" do + a = described_class.new.tap do |i| + i.define("foo") { "bar" } + i.define("bar") { "bar" } + end + b = described_class.new.tap do |i| + i.define("foo") { "zip" } + end + + result = a.merge(b) + pushes = result.instance_variable_get(:@__defined_pushes) + + expect(pushes[:foo]).to be_a(Array) + expect(pushes[:foo].size).to eq(2) + + expect(pushes[:bar]).to be_a(Array) + expect(pushes[:bar].size).to eq(1) + end + end + + describe "#__compiled_pushes" do + it "raises an exception if not finalized" do + subject.instance_variable_set(:@__finalized, false) + expect { subject.__compiled_pushes }.to raise_error + end + + it "returns a copy of the compiled pushes" do + pushes = { foo: "bar" } + subject.instance_variable_set(:@__finalized, true) + subject.instance_variable_set(:@__compiled_pushes, pushes) + + expect(subject.__compiled_pushes).to_not be(pushes) + expect(subject.__compiled_pushes).to eq(pushes) + end + end + + describe "#finalize!" do + let(:pushes) { a.merge(b).tap { |r| r.finalize! }.__compiled_pushes } + let(:key) { pushes[:foo][0] } + let(:config) { pushes[:foo][1] } + let(:unset) { Vagrant.plugin("2", :config).const_get(:UNSET_VALUE) } + + before do + register_plugin("2") do |plugin| + plugin.name "foo" + + plugin.push(:foo) do + Class.new(Vagrant.plugin("2", :push)) + end + + plugin.config(:foo, :push) do + Class.new(Vagrant.plugin("2", :config)) do + attr_accessor :bar + attr_accessor :zip + + def initialize + @bar = self.class.const_get(:UNSET_VALUE) + @zip = self.class.const_get(:UNSET_VALUE) + end + end + end + end + end + + context "with the same name but different strategy" do + context "with no block" do + let(:a) do + described_class.new.tap do |i| + i.define("foo", strategy: "bar") + end + end + + let(:b) do + described_class.new.tap do |i| + i.define("foo", strategy: "zip") + end + end + + it "chooses the last config" do + expect(key).to eq(:zip) + expect(config.bar).to be(unset) + expect(config.zip).to be(unset) + end + end + + context "with a block" do + let(:a) do + described_class.new.tap do |i| + i.define("foo", strategy: "bar") do |p| + p.bar = "a" + end + end + end + + let(:b) do + described_class.new.tap do |i| + i.define("foo", strategy: "zip") do |p| + p.zip = "b" + end + end + end + + it "chooses the last config" do + expect(key).to eq(:zip) + expect(config.bar).to eq(unset) + expect(config.zip).to eq("b") + end + end + + context "with a block, then no block" do + let(:a) do + described_class.new.tap do |i| + i.define("foo", strategy: "bar") do |p| + p.bar, p.zip = "a", "a" + end + end + end + + let(:b) do + described_class.new.tap do |i| + i.define("foo", strategy: "zip") + end + end + + it "chooses the last config" do + expect(key).to eq(:zip) + expect(config.bar).to be(unset) + expect(config.zip).to be(unset) + end + end + + context "with no block, then a block" do + let(:a) do + described_class.new.tap do |i| + i.define("foo", strategy: "bar") + end + end + + let(:b) do + described_class.new.tap do |i| + i.define("foo", strategy: "zip") do |p| + p.bar, p.zip = "b", "b" + end + end + end + + it "chooses the last config" do + expect(key).to eq(:zip) + expect(config.bar).to eq("b") + expect(config.zip).to eq("b") + end + end + end + + context "with the same name twice" do + context "with no block" do + let(:a) do + described_class.new.tap do |i| + i.define("foo") + end + end + + let(:b) do + described_class.new.tap do |i| + i.define("foo") + end + end + + it "merges the configs" do + expect(key).to eq(:foo) + expect(config.bar).to be(unset) + expect(config.zip).to be(unset) + end + end + + context "with a block" do + let(:a) do + described_class.new.tap do |i| + i.define("foo") do |p| + p.bar = "a" + end + end + end + + let(:b) do + described_class.new.tap do |i| + i.define("foo") do |p| + p.zip = "b" + end + end + end + + it "merges the configs" do + expect(key).to eq(:foo) + expect(config.bar).to eq("a") + expect(config.zip).to eq("b") + end + end + + context "with a block, then no block" do + let(:a) do + described_class.new.tap do |i| + i.define("foo") do |p| + p.bar = "a" + end + end + end + + let(:b) do + described_class.new.tap do |i| + i.define("foo") + end + end + + it "merges the configs" do + expect(key).to eq(:foo) + expect(config.bar).to eq("a") + expect(config.zip).to be(unset) + end + end + + context "with no block, then a block" do + let(:a) do + described_class.new.tap do |i| + i.define("foo", strategy: "bar") + end + end + + let(:b) do + described_class.new.tap do |i| + i.define("foo", strategy: "zip") do |p| + p.zip = "b" + end + end + end + + it "merges the configs" do + expect(key).to eq(:zip) + expect(config.bar).to eq(unset) + expect(config.zip).to eq("b") + end + end + end + + it "sets @__finalized to true" do + subject.finalize! + expect(subject.instance_variable_get(:@__finalized)).to be(true) + end + end +end diff --git a/test/unit/plugins/pushes/atlas/config_test.rb b/test/unit/plugins/pushes/atlas/config_test.rb new file mode 100644 index 000000000..1600e967a --- /dev/null +++ b/test/unit/plugins/pushes/atlas/config_test.rb @@ -0,0 +1,223 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/pushes/atlas/config") + +describe VagrantPlugins::AtlasPush::Config do + include_context "unit" + + before(:all) do + I18n.load_path << Vagrant.source_root.join("plugins/pushes/atlas/locales/en.yml") + I18n.reload! + end + + let(:machine) { double("machine") } + + describe "#address" do + it "defaults to nil" do + subject.finalize! + expect(subject.address).to be(nil) + end + end + + describe "#token" do + it "defaults to nil" do + subject.finalize! + expect(subject.token).to be(nil) + end + end + + describe "#app" do + it "defaults to nil" do + subject.finalize! + expect(subject.app).to be(nil) + end + end + + describe "#dir" do + it "defaults to ." do + subject.finalize! + expect(subject.dir).to eq(".") + end + end + + describe "#vcs" do + it "defaults to true" do + subject.finalize! + expect(subject.vcs).to be(true) + end + end + + describe "#uploader_path" do + it "defaults to nil" do + subject.finalize! + expect(subject.uploader_path).to be(nil) + end + end + + describe "#validate" do + before do + allow(machine).to receive(:env) + .and_return(double("env", + root_path: "", + data_dir: Pathname.new(""), + )) + + subject.app = "sethvargo/bacon" + subject.dir = "." + subject.vcs = true + subject.uploader_path = "uploader" + end + + let(:result) { subject.validate(machine) } + let(:errors) { result["Atlas push"] } + + context "when the token is missing" do + context "when a vagrant-login token exists" do + before do + allow(subject).to receive(:token_from_vagrant_login) + .and_return("token_from_vagrant_login") + + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]) + .with("ATLAS_TOKEN").and_return("token_from_env") + end + + it "uses the token in the Vagrantfile" do + subject.token = "" + subject.finalize! + expect(errors).to be_empty + expect(subject.token).to eq("token_from_vagrant_login") + end + end + + context "when ATLAS_TOKEN is set in the environment" do + before do + allow(subject).to receive(:token_from_vagrant_login) + .and_return(nil) + + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]) + .with("ATLAS_TOKEN").and_return("token_from_env") + end + + it "uses the token in the environment" do + subject.token = "" + subject.finalize! + expect(errors).to be_empty + expect(subject.token).to eq("token_from_env") + end + end + + context "when a token is given in the Vagrantfile" do + before do + allow(subject).to receive(:token_from_vagrant_login) + .and_return("token_from_vagrant_login") + + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]) + .with("ATLAS_TOKEN").and_return("token_from_env") + end + + it "uses the token in the Vagrantfile" do + subject.token = "token_from_vagrantfile" + subject.finalize! + expect(errors).to be_empty + expect(subject.token).to eq("token_from_vagrantfile") + end + end + + context "when no token is given" do + before do + allow(subject).to receive(:token_from_vagrant_login) + .and_return(nil) + + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]) + .with("ATLAS_TOKEN").and_return(nil) + end + + it "returns an error" do + subject.token = "" + subject.finalize! + expect(errors).to include(I18n.t("atlas_push.errors.missing_token")) + end + end + end + + context "when the app is missing" do + it "returns an error" do + subject.app = "" + subject.finalize! + expect(errors).to include(I18n.t("atlas_push.errors.missing_attribute", + attribute: "app", + )) + end + end + + context "when the dir is missing" do + it "returns an error" do + subject.dir = "" + subject.finalize! + expect(errors).to include(I18n.t("atlas_push.errors.missing_attribute", + attribute: "dir", + )) + end + end + + context "when the vcs is missing" do + it "does not return an error" do + subject.vcs = "" + subject.finalize! + expect(errors).to be_empty + end + end + + context "when the uploader_path is missing" do + it "returns an error" do + subject.uploader_path = "" + subject.finalize! + expect(errors).to be_empty + end + end + end + + describe "#merge" do + context "when includes are given" do + let(:one) { described_class.new } + let(:two) { described_class.new } + + it "merges the result" do + one.includes = %w(a b c) + two.includes = %w(c d e) + result = one.merge(two) + expect(result.includes).to eq(%w(a b c d e)) + end + end + + context "when excludes are given" do + let(:one) { described_class.new } + let(:two) { described_class.new } + + it "merges the result" do + one.excludes = %w(a b c) + two.excludes = %w(c d e) + result = one.merge(two) + expect(result.excludes).to eq(%w(a b c d e)) + end + end + end + + describe "#include" do + it "adds the item to the list" do + subject.include("me") + expect(subject.includes).to include("me") + end + end + + describe "#exclude" do + it "adds the item to the list" do + subject.exclude("not me") + expect(subject.excludes).to include("not me") + end + end +end diff --git a/test/unit/plugins/pushes/atlas/push_test.rb b/test/unit/plugins/pushes/atlas/push_test.rb new file mode 100644 index 000000000..e7ffdebd5 --- /dev/null +++ b/test/unit/plugins/pushes/atlas/push_test.rb @@ -0,0 +1,153 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/pushes/atlas/config") +require Vagrant.source_root.join("plugins/pushes/atlas/push") + +describe VagrantPlugins::AtlasPush::Push do + include_context "unit" + + let(:bin) { VagrantPlugins::AtlasPush::Push::UPLOADER_BIN } + + let(:env) do + double("env", + root_path: File.expand_path("..", __FILE__) + ) + end + + let(:config) do + VagrantPlugins::AtlasPush::Config.new.tap do |c| + c.finalize! + end + end + + subject { described_class.new(env, config) } + + before do + # Stub this right away to avoid real execs + allow(Vagrant::Util::SafeExec).to receive(:exec) + end + + describe "#push" do + it "pushes with the uploader" do + allow(subject).to receive(:uploader_path).and_return("foo") + + expect(subject).to receive(:execute).with("foo") + + subject.push + end + + it "raises an exception if the uploader couldn't be found" do + expect(subject).to receive(:uploader_path).and_return(nil) + + expect { subject.push }.to raise_error( + VagrantPlugins::AtlasPush::Errors::UploaderNotFound) + end + end + + describe "#execute" do + let(:app) { "foo/bar" } + + before do + config.app = app + end + + it "sends the basic flags" do + expect(Vagrant::Util::SafeExec).to receive(:exec). + with("foo", "-vcs", app, env.root_path.to_s) + + subject.execute("foo") + end + + it "doesn't send VCS if disabled" do + expect(Vagrant::Util::SafeExec).to receive(:exec). + with("foo", app, env.root_path.to_s) + + config.vcs = false + subject.execute("foo") + end + + it "sends includes" do + expect(Vagrant::Util::SafeExec).to receive(:exec). + with("foo", "-vcs", "-include", "foo", "-include", + "bar", app, env.root_path.to_s) + + config.includes = ["foo", "bar"] + subject.execute("foo") + end + + it "sends excludes" do + expect(Vagrant::Util::SafeExec).to receive(:exec). + with("foo", "-vcs", "-exclude", "foo", "-exclude", + "bar", app, env.root_path.to_s) + + config.excludes = ["foo", "bar"] + subject.execute("foo") + end + + it "sends custom server address" do + expect(Vagrant::Util::SafeExec).to receive(:exec). + with("foo", "-vcs", "-address", "foo", app, env.root_path.to_s) + + config.address = "foo" + subject.execute("foo") + end + + it "sends custom token" do + expect(Vagrant::Util::SafeExec).to receive(:exec). + with("foo", "-vcs", "-token", "atlas_token", app, env.root_path.to_s) + + config.token = "atlas_token" + subject.execute("foo") + end + end + + describe "#uploader_path" do + it "should return the configured path if set" do + config.uploader_path = "foo" + expect(subject.uploader_path).to eq("foo") + end + + it "should look up the uploader via PATH if not set" do + allow(Vagrant).to receive(:in_installer?).and_return(false) + + expect(Vagrant::Util::Which).to receive(:which). + with(described_class.const_get(:UPLOADER_BIN)). + and_return("bar") + + expect(subject.uploader_path).to eq("bar") + end + + it "should look up the uploader in the embedded dir if installer" do + dir = temporary_dir + + allow(Vagrant).to receive(:in_installer?).and_return(true) + allow(Vagrant).to receive(:installer_embedded_dir).and_return(dir.to_s) + + bin_path = dir.join("bin", bin) + bin_path.dirname.mkpath + bin_path.open("w+") { |f| f.write("hi") } + + expect(subject.uploader_path).to eq(bin_path.to_s) + end + + it "should look up the uploader in the PATH if not in the installer" do + dir = temporary_dir + + allow(Vagrant).to receive(:in_installer?).and_return(true) + allow(Vagrant).to receive(:installer_embedded_dir).and_return(dir.to_s) + + expect(Vagrant::Util::Which).to receive(:which). + with(described_class.const_get(:UPLOADER_BIN)). + and_return("bar") + + expect(subject.uploader_path).to eq("bar") + end + + it "should return nil if its not found anywhere" do + allow(Vagrant).to receive(:in_installer?).and_return(false) + allow(Vagrant::Util::Which).to receive(:which).and_return(nil) + + expect(subject.uploader_path).to be_nil + end + end +end diff --git a/test/unit/plugins/pushes/ftp/adapter_test.rb b/test/unit/plugins/pushes/ftp/adapter_test.rb new file mode 100644 index 000000000..54bacc1d1 --- /dev/null +++ b/test/unit/plugins/pushes/ftp/adapter_test.rb @@ -0,0 +1,111 @@ +require_relative "../../../base" +require "fake_ftp" + +require Vagrant.source_root.join("plugins/pushes/ftp/adapter") + +describe VagrantPlugins::FTPPush::Adapter do + include_context "unit" + + subject do + described_class.new("127.0.0.1:2345", "sethvargo", "bacon", + foo: "bar", + ) + end + + describe "#initialize" do + it "sets the instance variables" do + expect(subject.host).to eq("127.0.0.1") + expect(subject.port).to eq(2345) + expect(subject.username).to eq("sethvargo") + expect(subject.password).to eq("bacon") + expect(subject.options).to eq(foo: "bar") + expect(subject.server).to be(nil) + end + end + + describe "#parse_host" do + it "has a default value" do + allow(subject).to receive(:default_port) + .and_return(5555) + + result = subject.parse_host("127.0.0.1") + expect(result[0]).to eq("127.0.0.1") + expect(result[1]).to eq(5555) + end + end +end + +describe VagrantPlugins::FTPPush::FTPAdapter do + include_context "unit" + + before(:all) do + @server = FakeFtp::Server.new(21212, 21213) + @server.start + end + + after(:all) { @server.stop } + + let(:server) { @server } + + before { server.reset } + + subject do + described_class.new("127.0.0.1:#{server.port}", "sethvargo", "bacon") + end + + describe "#default_port" do + it "is 20" do + expect(subject.default_port).to eq(20) + end + end + + describe "#upload" do + before do + @dir = Dir.mktmpdir + FileUtils.touch("#{@dir}/file") + end + + after do + FileUtils.rm_rf(@dir) + end + + it "uploads the file" do + subject.connect do |ftp| + ftp.upload("#{@dir}/file", "/file") + end + + expect(server.files).to include("file") + end + + it "uploads in passive mode" do + subject.options[:passive] = true + subject.connect do |ftp| + ftp.upload("#{@dir}/file", "/file") + end + + expect(server.file("file")).to be_passive + end + end +end + +describe VagrantPlugins::FTPPush::SFTPAdapter do + include_context "unit" + + subject do + described_class.new("127.0.0.1:2345", "sethvargo", "bacon", + foo: "bar", + ) + end + + describe "#default_port" do + it "is 22" do + expect(subject.default_port).to eq(22) + end + end + + describe "#upload" do + it "uploads the file" do + pending "a way to mock an SFTP server" + end + end +end diff --git a/test/unit/plugins/pushes/ftp/config_test.rb b/test/unit/plugins/pushes/ftp/config_test.rb new file mode 100644 index 000000000..f66eeb791 --- /dev/null +++ b/test/unit/plugins/pushes/ftp/config_test.rb @@ -0,0 +1,171 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/pushes/ftp/config") + +describe VagrantPlugins::FTPPush::Config do + include_context "unit" + + before(:all) do + I18n.load_path << Vagrant.source_root.join("plugins/pushes/ftp/locales/en.yml") + I18n.reload! + end + + subject { described_class.new } + + let(:machine) { double("machine") } + + describe "#host" do + it "defaults to nil" do + subject.finalize! + expect(subject.host).to be(nil) + end + end + + describe "#username" do + it "defaults to nil" do + subject.finalize! + expect(subject.username).to be(nil) + end + end + + describe "#password" do + it "defaults to nil" do + subject.finalize! + expect(subject.password).to be(nil) + end + end + + describe "#passive" do + it "defaults to true" do + subject.finalize! + expect(subject.passive).to be(true) + end + end + + describe "#secure" do + it "defaults to false" do + subject.finalize! + expect(subject.secure).to be(false) + end + end + + describe "#destination" do + it "defaults to /" do + subject.finalize! + expect(subject.destination).to eq("/") + end + end + + describe "#dir" do + it "defaults to ." do + subject.finalize! + expect(subject.dir).to eq(".") + end + end + + describe "#merge" do + context "when includes are given" do + let(:one) { described_class.new } + let(:two) { described_class.new } + + it "merges the result" do + one.includes = %w(a b c) + two.includes = %w(c d e) + result = one.merge(two) + expect(result.includes).to eq(%w(a b c d e)) + end + end + + context "when excludes are given" do + let(:one) { described_class.new } + let(:two) { described_class.new } + + it "merges the result" do + one.excludes = %w(a b c) + two.excludes = %w(c d e) + result = one.merge(two) + expect(result.excludes).to eq(%w(a b c d e)) + end + end + end + + describe "#validate" do + before do + allow(machine).to receive(:env) + .and_return(double("env", + root_path: "", + )) + + subject.host = "ftp.example.com" + subject.username = "sethvargo" + subject.password = "bacon" + subject.destination = "/" + subject.dir = "." + end + + let(:result) { subject.validate(machine) } + let(:errors) { result["FTP push"] } + + context "when the host is missing" do + it "returns an error" do + subject.host = "" + subject.finalize! + expect(errors).to include(I18n.t("ftp_push.errors.missing_attribute", + attribute: "host", + )) + end + end + + context "when the username is missing" do + it "returns an error" do + subject.username = "" + subject.finalize! + expect(errors).to include(I18n.t("ftp_push.errors.missing_attribute", + attribute: "username", + )) + end + end + + context "when the password is missing" do + it "does not return an error" do + subject.password = "" + subject.finalize! + expect(errors).to be_empty + end + end + + context "when the destination is missing" do + it "returns an error" do + subject.destination = "" + subject.finalize! + expect(errors).to include(I18n.t("ftp_push.errors.missing_attribute", + attribute: "destination", + )) + end + end + + context "when the dir is missing" do + it "returns an error" do + subject.dir = "" + subject.finalize! + expect(errors).to include(I18n.t("ftp_push.errors.missing_attribute", + attribute: "dir", + )) + end + end + end + + describe "#include" do + it "adds the item to the list" do + subject.include("me") + expect(subject.includes).to include("me") + end + end + + describe "#exclude" do + it "adds the item to the list" do + subject.exclude("not me") + expect(subject.excludes).to include("not me") + end + end +end diff --git a/test/unit/plugins/pushes/ftp/push_test.rb b/test/unit/plugins/pushes/ftp/push_test.rb new file mode 100644 index 000000000..ca8ee2752 --- /dev/null +++ b/test/unit/plugins/pushes/ftp/push_test.rb @@ -0,0 +1,313 @@ +require_relative "../../../base" +require "fake_ftp" + +require Vagrant.source_root.join("plugins/pushes/ftp/push") + +describe VagrantPlugins::FTPPush::Push do + include_context "unit" + + let(:env) { isolated_environment } + let(:config) do + double("config", + host: "127.0.0.1:21212", + username: "sethvargo", + password: "bacon", + passive: false, + secure: false, + destination: "/var/www/site", + ) + end + + subject { described_class.new(env, config) } + + describe "#push" do + before(:all) do + @server = FakeFtp::Server.new(21212, 21213) + @server.start + + @dir = Dir.mktmpdir + + FileUtils.touch("#{@dir}/.hidden.rb") + FileUtils.touch("#{@dir}/application.rb") + FileUtils.touch("#{@dir}/config.rb") + FileUtils.touch("#{@dir}/Gemfile") + FileUtils.touch("#{@dir}/data.txt") + FileUtils.mkdir("#{@dir}/empty_folder") + end + + after(:all) do + FileUtils.rm_rf(@dir) + @server.stop + end + + let(:server) { @server } + + before do + allow(config).to receive(:dir) + .and_return(@dir) + + allow(config).to receive(:includes) + .and_return([]) + + allow(config).to receive(:excludes) + .and_return(%w(*.rb)) + end + + + it "pushes the files to the server" do + subject.push + expect(server.files).to eq(%w(Gemfile data.txt)) + end + end + + describe "#connect" do + before do + allow_any_instance_of(VagrantPlugins::FTPPush::FTPAdapter) + .to receive(:connect) + .and_yield(:ftp) + allow_any_instance_of(VagrantPlugins::FTPPush::SFTPAdapter) + .to receive(:connect) + .and_yield(:sftp) + end + + context "when secure is requested" do + before do + allow(config).to receive(:secure) + .and_return(true) + end + + it "yields a new SFTPAdapter" do + expect { |b| subject.connect(&b) }.to yield_with_args(:sftp) + end + end + + context "when secure is not requested" do + before do + allow(config).to receive(:secure) + .and_return(false) + end + + it "yields a new FTPAdapter" do + expect { |b| subject.connect(&b) }.to yield_with_args(:ftp) + end + end + end + + describe "#parse_host" do + let(:result) { subject.parse_host(host) } + + context "when no port is given" do + let(:host) { "127.0.0.1" } + + it "returns the url and port 22" do + expect(result).to eq(["127.0.0.1", "22"]) + end + end + + context "when a port is given" do + let(:host) { "127.0.0.1:23456" } + + it "returns the url and port 23456" do + expect(result).to eq(["127.0.0.1", "23456"]) + end + end + + context "when more than more port is given" do + let(:host) { "127.0.0.1:22:33:44" } + + it "returns the url and everything after" do + expect(result).to eq(["127.0.0.1", "22:33:44"]) + end + end + end + + describe "#all_files" do + before(:all) do + @dir = Dir.mktmpdir + + FileUtils.touch("#{@dir}/.hidden.rb") + FileUtils.touch("#{@dir}/application.rb") + FileUtils.touch("#{@dir}/config.rb") + FileUtils.touch("#{@dir}/Gemfile") + FileUtils.mkdir("#{@dir}/empty_folder") + FileUtils.mkdir("#{@dir}/folder") + FileUtils.mkdir("#{@dir}/folder/.git") + FileUtils.touch("#{@dir}/folder/.git/config") + FileUtils.touch("#{@dir}/folder/server.rb") + end + + after(:all) do + FileUtils.rm_rf(@dir) + end + + let(:files) do + subject.all_files.map do |file| + file.sub("#{@dir}/", "") + end + end + + before do + allow(config).to receive(:dir) + .and_return(@dir) + + allow(config).to receive(:includes) + .and_return(%w(not_a_file.rb still_not_a_file.rb)) + + allow(config).to receive(:excludes) + .and_return(%w(*.rb)) + end + + it "returns the list of real files + includes, without excludes" do + expect(files).to eq(%w( + Gemfile + folder/.git/config + )) + end + end + + describe "includes_files" do + before(:all) do + @dir = Dir.mktmpdir + + FileUtils.touch("#{@dir}/.hidden.rb") + FileUtils.touch("#{@dir}/application.rb") + FileUtils.touch("#{@dir}/config.rb") + FileUtils.touch("#{@dir}/Gemfile") + FileUtils.mkdir("#{@dir}/folder") + FileUtils.mkdir("#{@dir}/folder/.git") + FileUtils.touch("#{@dir}/folder/.git/config") + FileUtils.touch("#{@dir}/folder/server.rb") + end + + after(:all) do + FileUtils.rm_rf(@dir) + end + + let(:files) do + subject.includes_files.map do |file| + file.sub("#{@dir}/", "") + end + end + + before do + allow(config).to receive(:dir) + .and_return(@dir) + end + + def set_includes(value) + allow(config).to receive(:includes) + .and_return(value) + end + + it "includes the file" do + set_includes(["Gemfile"]) + expect(files).to eq(%w( + Gemfile + )) + end + + it "includes the files that are subdirectories" do + set_includes(["folder"]) + expect(files).to eq(%w( + folder + folder/.git + folder/.git/config + folder/server.rb + )) + end + + it "includes files that match a pattern" do + set_includes(["*.rb"]) + expect(files).to eq(%w( + .hidden.rb + application.rb + config.rb + )) + end + end + + describe "#filter_excludes" do + let(:dir) { "/root/dir" } + + let(:list) do + %W( + #{dir}/.hidden.rb + #{dir}/application.rb + #{dir}/config.rb + #{dir}/Gemfile + #{dir}/folder + #{dir}/folder/.git + #{dir}/folder/.git/config + #{dir}/folder/server.rb + + /path/outside/you.rb + /path/outside/me.rb + /path/outside/folder/bacon.rb + ) + end + + before do + allow(config).to receive(:dir) + .and_return(dir) + end + + it "excludes files" do + subject.filter_excludes!(list, %w(*.rb)) + + expect(list).to eq(%W( + #{dir}/Gemfile + #{dir}/folder + #{dir}/folder/.git + #{dir}/folder/.git/config + )) + end + + it "excludes files in a directory" do + subject.filter_excludes!(list, %w(folder)) + + expect(list).to eq(%W( + #{dir}/.hidden.rb + #{dir}/application.rb + #{dir}/config.rb + #{dir}/Gemfile + + /path/outside/you.rb + /path/outside/me.rb + /path/outside/folder/bacon.rb + )) + end + + it "excludes specific files in a directory" do + subject.filter_excludes!(list, %w(/path/outside/folder/*.rb)) + + expect(list).to eq(%W( + #{dir}/.hidden.rb + #{dir}/application.rb + #{dir}/config.rb + #{dir}/Gemfile + #{dir}/folder + #{dir}/folder/.git + #{dir}/folder/.git/config + #{dir}/folder/server.rb + + /path/outside/you.rb + /path/outside/me.rb + )) + end + + it "excludes files outside the #dir" do + subject.filter_excludes!(list, %w(/path/outside)) + + expect(list).to eq(%W( + #{dir}/.hidden.rb + #{dir}/application.rb + #{dir}/config.rb + #{dir}/Gemfile + #{dir}/folder + #{dir}/folder/.git + #{dir}/folder/.git/config + #{dir}/folder/server.rb + )) + end + end +end diff --git a/test/unit/plugins/pushes/heroku/config_test.rb b/test/unit/plugins/pushes/heroku/config_test.rb new file mode 100644 index 000000000..e451ad152 --- /dev/null +++ b/test/unit/plugins/pushes/heroku/config_test.rb @@ -0,0 +1,99 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/pushes/heroku/config") + +describe VagrantPlugins::HerokuPush::Config do + include_context "unit" + + before(:all) do + I18n.load_path << Vagrant.source_root.join("plugins/pushes/heroku/locales/en.yml") + I18n.reload! + end + + subject { described_class.new } + + let(:machine) { double("machine") } + + describe "#app" do + it "defaults to nil" do + subject.finalize! + expect(subject.app).to be(nil) + end + end + + describe "#dir" do + it "defaults to ." do + subject.finalize! + expect(subject.dir).to eq(".") + end + end + + describe "#git_bin" do + it "defaults to git" do + subject.finalize! + expect(subject.git_bin).to eq("git") + end + end + + describe "#remote" do + it "defaults to git" do + subject.finalize! + expect(subject.remote).to eq("heroku") + end + end + + describe "#validate" do + before do + allow(machine).to receive(:env) + .and_return(double("env", + root_path: "", + )) + + subject.app = "bacon" + subject.dir = "." + subject.git_bin = "git" + subject.remote = "heroku" + end + + let(:result) { subject.validate(machine) } + let(:errors) { result["Heroku push"] } + + context "when the app is missing" do + it "does not return an error" do + subject.app = "" + subject.finalize! + expect(errors).to be_empty + end + end + + context "when the git_bin is missing" do + it "returns an error" do + subject.git_bin = "" + subject.finalize! + expect(errors).to include(I18n.t("heroku_push.errors.missing_attribute", + attribute: "git_bin", + )) + end + end + + context "when the remote is missing" do + it "returns an error" do + subject.remote = "" + subject.finalize! + expect(errors).to include(I18n.t("heroku_push.errors.missing_attribute", + attribute: "remote", + )) + end + end + + context "when the dir is missing" do + it "returns an error" do + subject.dir = "" + subject.finalize! + expect(errors).to include(I18n.t("heroku_push.errors.missing_attribute", + attribute: "dir", + )) + end + end + end +end diff --git a/test/unit/plugins/pushes/heroku/push_test.rb b/test/unit/plugins/pushes/heroku/push_test.rb new file mode 100644 index 000000000..c0337e41f --- /dev/null +++ b/test/unit/plugins/pushes/heroku/push_test.rb @@ -0,0 +1,324 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/pushes/heroku/push") + +describe VagrantPlugins::HerokuPush::Push do + include_context "unit" + + before(:all) do + I18n.load_path << Vagrant.source_root.join("plugins/pushes/heroku/locales/en.yml") + I18n.reload! + end + + let(:env) { isolated_environment } + let(:config) do + double("config", + app: "bacon", + dir: "lib", + git_bin: "git", + remote: "heroku", + ) + end + + subject { described_class.new(env, config) } + + describe "#push" do + let(:branch) { "master" } + + let(:root_path) { "/handy/dandy" } + let(:dir) { "#{root_path}/#{config.dir}" } + + before do + allow(subject).to receive(:git_branch) + .and_return(branch) + allow(subject).to receive(:verify_git_bin!) + allow(subject).to receive(:verify_git_repo!) + allow(subject).to receive(:has_git_remote?) + allow(subject).to receive(:add_heroku_git_remote) + allow(subject).to receive(:git_push_heroku) + allow(subject).to receive(:execute!) + + allow(env).to receive(:root_path) + .and_return(root_path) + end + + it "verifies the git bin is present" do + expect(subject).to receive(:verify_git_bin!) + .with(config.git_bin) + subject.push + end + + it "verifies the directory is a git repo" do + expect(subject).to receive(:verify_git_repo!) + .with(dir) + subject.push + end + + context "when the heroku remote exists" do + before do + allow(subject).to receive(:has_git_remote?) + .and_return(true) + end + + it "does not add the heroku remote" do + expect(subject).to_not receive(:add_heroku_git_remote) + subject.push + end + end + + context "when the heroku remote does not exist" do + before do + allow(subject).to receive(:has_git_remote?) + .and_return(false) + end + + it "adds the heroku remote" do + expect(subject).to receive(:add_heroku_git_remote) + .with(config.remote, config.app, dir) + subject.push + end + end + + it "pushes to heroku" do + expect(subject).to receive(:git_push_heroku) + .with(config.remote, branch, dir) + subject.push + end + end + + describe "#verify_git_bin!" do + context "when git does not exist" do + before do + allow(Vagrant::Util::Which).to receive(:which) + .with("git") + .and_return(nil) + end + + it "raises an exception" do + expect { + subject.verify_git_bin!("git") + } .to raise_error(VagrantPlugins::HerokuPush::Errors::GitNotFound) { |error| + expect(error.message).to eq(I18n.t("heroku_push.errors.git_not_found", + bin: "git", + )) + } + end + end + + context "when git exists" do + before do + allow(Vagrant::Util::Which).to receive(:which) + .with("git") + .and_return("git") + end + + it "does not raise an exception" do + expect { subject.verify_git_bin!("git") }.to_not raise_error + end + end + end + + describe "#verify_git_repo!" do + context "when the path is a git repo" do + before do + allow(File).to receive(:directory?) + .with("/repo/path/.git") + .and_return(false) + end + + it "raises an exception" do + expect { + subject.verify_git_repo!("/repo/path") + } .to raise_error(VagrantPlugins::HerokuPush::Errors::NotAGitRepo) { |error| + expect(error.message).to eq(I18n.t("heroku_push.errors.not_a_git_repo", + path: "/repo/path", + )) + } + end + end + + context "when the path is not a git repo" do + before do + allow(File).to receive(:directory?) + .with("/repo/path/.git") + .and_return(true) + end + + it "does not raise an exception" do + expect { subject.verify_git_repo!("/repo/path") }.to_not raise_error + end + end + end + + describe "#git_push_heroku" do + let(:dir) { "." } + + before { allow(subject).to receive(:execute!) } + + it "executes the proper command" do + expect(subject).to receive(:execute!) + .with("git", + "--git-dir", "#{dir}/.git", + "--work-tree", dir, + "push", "bacon", "hamlet:master", + ) + subject.git_push_heroku("bacon", "hamlet", dir) + end + end + + describe "#has_git_remote?" do + let(:dir) { "." } + + let(:process) do + double("process", + stdout: "origin\r\nbacon\nhello" + ) + end + + before do + allow(subject).to receive(:execute!) + .and_return(process) + end + + it "executes the proper command" do + expect(subject).to receive(:execute!) + .with("git", + "--git-dir", "#{dir}/.git", + "--work-tree", dir, + "remote", + ) + subject.has_git_remote?("bacon", dir) + end + + it "returns true when the remote exists" do + expect(subject.has_git_remote?("origin", dir)).to be(true) + expect(subject.has_git_remote?("bacon", dir)).to be(true) + expect(subject.has_git_remote?("hello", dir)).to be(true) + end + + it "returns false when the remote does not exist" do + expect(subject.has_git_remote?("nope", dir)).to be(false) + end + end + + describe "#add_heroku_git_remote" do + let(:dir) { "." } + + before do + allow(subject).to receive(:execute!) + allow(subject).to receive(:heroku_git_url) + .with("app") + .and_return("HEROKU_URL") + end + + it "executes the proper command" do + expect(subject).to receive(:execute!) + .with("git", + "--git-dir", "#{dir}/.git", + "--work-tree", dir, + "remote", "add", "bacon", "HEROKU_URL", + ) + subject.add_heroku_git_remote("bacon", "app", dir) + end + end + + describe "#interpret_app" do + it "returns the basename of the directory" do + expect(subject.interpret_app("/foo/bar/blitz")).to eq("blitz") + end + end + + describe "#heroku_git_url" do + it "returns the proper string" do + expect(subject.heroku_git_url("bacon")) + .to eq("git@heroku.com:bacon.git") + end + end + + describe "#git_dir" do + it "returns the .git directory for the path" do + expect(subject.git_dir("/path")).to eq("/path/.git") + end + end + + describe "#git_branch" do + let(:stdout) { "" } + let(:process) { double("process", stdout: stdout) } + + before do + allow(subject).to receive(:execute!) + .and_return(process) + end + + let(:branch) { subject.git_branch("/path") } + + context "when the branch is prefixed with a star" do + let(:stdout) { "*bacon" } + + it "returns the correct name" do + expect(branch).to eq("bacon") + end + end + + context "when the branch is prefixed with a star space" do + let(:stdout) { "* bacon" } + + it "returns the correct name" do + expect(branch).to eq("bacon") + end + end + + context "when the branch is not prefixed" do + let(:stdout) { "bacon" } + + it "returns the correct name" do + expect(branch).to eq("bacon") + end + end + end + + describe "#execute!" do + let(:exit_code) { 0 } + let(:stdout) { "This is the output" } + let(:stderr) { "This is the errput" } + + let(:process) do + double("process", + exit_code: exit_code, + stdout: stdout, + stderr: stderr, + ) + end + + before do + allow(Vagrant::Util::Subprocess).to receive(:execute) + .and_return(process) + end + + it "creates a subprocess" do + expect(Vagrant::Util::Subprocess).to receive(:execute) + expect { subject.execute! }.to_not raise_error + end + + it "returns the resulting process" do + expect(subject.execute!).to be(process) + end + + context "when the exit code is non-zero" do + let(:exit_code) { 1 } + + it "raises an exception" do + klass = VagrantPlugins::HerokuPush::Errors::CommandFailed + cmd = ["foo", "bar"] + + expect { subject.execute!(*cmd) }.to raise_error(klass) { |error| + expect(error.message).to eq(I18n.t("heroku_push.errors.command_failed", + cmd: cmd.join(" "), + stdout: stdout, + stderr: stderr, + )) + } + end + end + end +end diff --git a/test/unit/plugins/pushes/local-exec/config_test.rb b/test/unit/plugins/pushes/local-exec/config_test.rb new file mode 100644 index 000000000..045872d2f --- /dev/null +++ b/test/unit/plugins/pushes/local-exec/config_test.rb @@ -0,0 +1,85 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/pushes/local-exec/config") + +describe VagrantPlugins::LocalExecPush::Config do + include_context "unit" + + before(:all) do + I18n.load_path << Vagrant.source_root.join("plugins/pushes/local-exec/locales/en.yml") + I18n.reload! + end + + let(:machine) { double("machine") } + + describe "#script" do + it "defaults to nil" do + subject.finalize! + expect(subject.script).to be(nil) + end + end + + describe "#inline" do + it "defaults to nil" do + subject.finalize! + expect(subject.inline).to be(nil) + end + end + + describe "#validate" do + before do + allow(machine).to receive(:env) + .and_return(double("env", + root_path: "", + )) + subject.finalize! + end + + let(:result) { subject.validate(machine) } + let(:errors) { result["Local Exec push"] } + + context "when script is present" do + before { subject.script = "foo.sh" } + + context "when inline is present" do + before { subject.inline = "echo" } + + it "returns an error" do + expect(errors).to include( + I18n.t("local_exec_push.errors.cannot_specify_script_and_inline") + ) + end + end + + context "when inline is not present" do + before { subject.inline = "" } + + it "does not return an error" do + expect(errors).to be_empty + end + end + end + + context "when script is not present" do + before { subject.script = "" } + + context "when inline is present" do + before { subject.inline = "echo" } + + it "does not return an error" do + expect(errors).to be_empty + end + end + + context "when inline is not present" do + before { subject.inline = "" } + + it "returns an error" do + expect(errors).to include(I18n.t("local_exec_push.errors.missing_attribute", + attribute: "script", + )) + end + end + end + end +end diff --git a/test/unit/plugins/pushes/local-exec/push_test.rb b/test/unit/plugins/pushes/local-exec/push_test.rb new file mode 100644 index 000000000..6896428d1 --- /dev/null +++ b/test/unit/plugins/pushes/local-exec/push_test.rb @@ -0,0 +1,139 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/pushes/local-exec/push") + +describe VagrantPlugins::LocalExecPush::Push do + include_context "unit" + + before(:all) do + I18n.load_path << Vagrant.source_root.join("plugins/pushes/local-exec/locales/en.yml") + I18n.reload! + end + + let(:env) { isolated_environment } + let(:config) do + double("config", + script: nil, + inline: nil, + ) + end + + subject { described_class.new(env, config) } + + before do + allow(env).to receive(:root_path) + .and_return(File.expand_path("..", __FILE__)) + end + + describe "#push" do + before do + allow(subject).to receive(:execute_inline!) + allow(subject).to receive(:execute_script!) + allow(subject).to receive(:execute!) + end + + context "when inline is given" do + before { allow(config).to receive(:inline).and_return("echo") } + + it "executes the inline script" do + expect(subject).to receive(:execute_inline!) + .with(config.inline) + subject.push + end + end + + context "when script is given" do + before { allow(config).to receive(:script).and_return("foo.sh") } + + it "executes the script" do + expect(subject).to receive(:execute_script!) + .with(config.script) + subject.push + end + end + end + + describe "#execute_inline!" do + before { allow(subject).to receive(:execute_script!) } + + it "writes the script to a tempfile" do + expect(Tempfile).to receive(:new).and_call_original + subject.execute_inline!("echo") + end + + it "executes the script" do + expect(subject).to receive(:execute_script!) + subject.execute_inline!("echo") + end + end + + describe "#execute_script!" do + before do + allow(subject).to receive(:execute!) + allow(FileUtils).to receive(:chmod) + end + + it "expands the path relative to the machine root" do + expect(subject).to receive(:execute!) + .with(File.expand_path("foo.sh", env.root_path)) + subject.execute_script!("./foo.sh") + end + + it "makes the file executable" do + expect(FileUtils).to receive(:chmod) + .with("+x", File.expand_path("foo.sh", env.root_path)) + subject.execute_script!("./foo.sh") + end + + it "calls execute!" do + expect(subject).to receive(:execute!) + .with(File.expand_path("foo.sh", env.root_path)) + subject.execute_script!("./foo.sh") + end + end + + describe "#execute!" do + let(:exit_code) { 0 } + let(:stdout) { "This is the output" } + let(:stderr) { "This is the errput" } + + let(:process) do + double("process", + exit_code: exit_code, + stdout: stdout, + stderr: stderr, + ) + end + + before do + allow(Vagrant::Util::Subprocess).to receive(:execute) + .and_return(process) + end + + it "creates a subprocess" do + expect(Vagrant::Util::Subprocess).to receive(:execute) + expect { subject.execute! }.to_not raise_error + end + + it "returns the resulting process" do + expect(subject.execute!).to be(process) + end + + context "when the exit code is non-zero" do + let(:exit_code) { 1 } + + it "raises an exception" do + klass = VagrantPlugins::LocalExecPush::Errors::CommandFailed + cmd = ["foo", "bar"] + + expect { subject.execute!(*cmd) }.to raise_error(klass) { |error| + expect(error.message).to eq(I18n.t("local_exec_push.errors.command_failed", + cmd: cmd.join(" "), + stdout: stdout, + stderr: stderr, + )) + } + end + end + end +end diff --git a/test/unit/plugins/pushes/noop/config_test.rb b/test/unit/plugins/pushes/noop/config_test.rb new file mode 100644 index 000000000..05be6c3f8 --- /dev/null +++ b/test/unit/plugins/pushes/noop/config_test.rb @@ -0,0 +1,14 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/pushes/noop/config") + +describe VagrantPlugins::NoopDeploy::Config do + include_context "unit" + + subject { described_class.new } + + let(:machine) { double("machine") } + + describe "#validate" do + end +end diff --git a/test/unit/vagrant/environment_test.rb b/test/unit/vagrant/environment_test.rb index 2bb52e939..02effd7db 100644 --- a/test/unit/vagrant/environment_test.rb +++ b/test/unit/vagrant/environment_test.rb @@ -968,6 +968,76 @@ VF end end + describe "#pushes" do + it "returns the pushes from the Vagrantfile config" do + environment = isolated_environment do |env| + env.vagrantfile(<<-VF.gsub(/^ {10}/, '')) + Vagrant.configure("2") do |config| + config.push.define "noop" + end + VF + end + + env = environment.create_vagrant_env + expect(env.pushes).to eq([:noop]) + end + end + + describe "#push" do + let(:push_class) do + Class.new(Vagrant.plugin("2", :push)) do + def self.pushed? + !!class_variable_get(:@@pushed) + end + + def push + !!self.class.class_variable_set(:@@pushed, true) + end + end + end + + it "raises an exception when the push does not exist" do + expect { instance.push("lolwatbacon") } + .to raise_error(Vagrant::Errors::PushStrategyNotDefined) + end + + it "raises an exception if the strategy does not exist" do + environment = isolated_environment do |env| + env.vagrantfile(<<-VF.gsub(/^ {10}/, '')) + Vagrant.configure("2") do |config| + config.push.define "lolwatbacon" + end + VF + end + + env = environment.create_vagrant_env + expect { env.push("lolwatbacon") } + .to raise_error(Vagrant::Errors::PushStrategyNotLoaded) + end + + it "executes the push action" do + register_plugin("2") do |plugin| + plugin.name "foo" + + plugin.push(:foo) do + push_class + end + end + + environment = isolated_environment do |env| + env.vagrantfile(<<-VF.gsub(/^ {10}/, '')) + Vagrant.configure("2") do |config| + config.push.define "foo" + end + VF + end + + env = environment.create_vagrant_env + env.push("foo") + expect(push_class.pushed?).to be_true + end + end + describe "#hook" do it "should call the action runner with the proper hook" do hook_name = :foo diff --git a/test/unit/vagrant/plugin/v2/manager_test.rb b/test/unit/vagrant/plugin/v2/manager_test.rb index 45ebd5a0c..3ddbf00e9 100644 --- a/test/unit/vagrant/plugin/v2/manager_test.rb +++ b/test/unit/vagrant/plugin/v2/manager_test.rb @@ -189,6 +189,42 @@ describe Vagrant::Plugin::V2::Manager do expect(instance.provider_configs[:bar]).to eq("bar") end + it "should enumerate registered push classes" do + pA = plugin do |p| + p.push("foo") { "bar" } + end + + pB = plugin do |p| + p.push("bar", foo: "bar") { "baz" } + end + + instance.register(pA) + instance.register(pB) + + expect(instance.pushes.to_hash.length).to eq(2) + expect(instance.pushes[:foo]).to eq(["bar", nil]) + expect(instance.pushes[:bar]).to eq(["baz", { foo: "bar" }]) + end + + it "provides the collection of registered push configs" do + pA = plugin do |p| + p.config("foo", :push) { "foo" } + end + + pB = plugin do |p| + p.config("bar", :push) { "bar" } + p.config("baz") { "baz" } + end + + instance.register(pA) + instance.register(pB) + + expect(instance.push_configs.to_hash.length).to eq(2) + expect(instance.push_configs[:foo]).to eq("foo") + expect(instance.push_configs[:bar]).to eq("bar") + end + + it "should enumerate all registered synced folder implementations" do pA = plugin do |p| p.synced_folder("foo") { "bar" } diff --git a/test/unit/vagrant/plugin/v2/plugin_test.rb b/test/unit/vagrant/plugin/v2/plugin_test.rb index 944d38f9f..76ad40993 100644 --- a/test/unit/vagrant/plugin/v2/plugin_test.rb +++ b/test/unit/vagrant/plugin/v2/plugin_test.rb @@ -322,6 +322,42 @@ describe Vagrant::Plugin::V2::Plugin do end end + describe "pushes" do + it "should register implementations" do + plugin = Class.new(described_class) do + push("foo") { "bar" } + end + + expect(plugin.components.pushes[:foo]).to eq(["bar", nil]) + end + + it "should be able to specify priorities" do + plugin = Class.new(described_class) do + push("foo", bar: 1) { "bar" } + end + + expect(plugin.components.pushes[:foo]).to eq(["bar", bar: 1]) + end + + it "should lazily register implementations" do + # Below would raise an error if the value of the config class was + # evaluated immediately. By asserting that this does not raise an + # error, we verify that the value is actually lazily loaded + plugin = nil + expect { + plugin = Class.new(described_class) do + push("foo") { raise StandardError, "FAIL!" } + end + }.to_not raise_error + + # Now verify when we actually get the configuration key that + # a proper error is raised. + expect { + plugin.components.pushes[:foo] + }.to raise_error(StandardError) + end + end + describe "synced folders" do it "should register implementations" do plugin = Class.new(described_class) do diff --git a/test/unit/vagrant/registry_test.rb b/test/unit/vagrant/registry_test.rb index d12f46ffe..f177a6a1c 100644 --- a/test/unit/vagrant/registry_test.rb +++ b/test/unit/vagrant/registry_test.rb @@ -90,6 +90,39 @@ describe Vagrant::Registry do expect(result["bar"]).to eq("barvalue") end + describe "#length" do + it "should return 0 when the registry is empty" do + expect(instance.length).to eq(0) + end + + it "should return the number of items in the registry" do + instance.register("foo") { } + instance.register("bar") { } + + expect(instance.length).to eq(2) + end + end + + describe "#size" do + it "should be an alias to #length" do + size = described_class.instance_method(:size) + length = described_class.instance_method(:length) + + expect(size).to eq(length) + end + end + + describe "#empty" do + it "should return true when the registry is empty" do + expect(instance.empty?).to be(true) + end + + it "should return false when there is at least one element" do + instance.register("foo") { } + expect(instance.empty?).to be(false) + end + end + describe "merging" do it "should merge in another registry" do one = described_class.new diff --git a/vagrant.gemspec b/vagrant.gemspec index 1981493ff..a2115b055 100644 --- a/vagrant.gemspec +++ b/vagrant.gemspec @@ -23,8 +23,10 @@ Gem::Specification.new do |s| s.add_dependency "hashicorp-checkpoint", "~> 0.1.1" s.add_dependency "log4r", "~> 1.1.9", "< 1.1.11" s.add_dependency "net-ssh", ">= 2.6.6", "< 2.10.0" + s.add_dependency "net-sftp", "~> 2.1" s.add_dependency "net-scp", "~> 1.1.0" s.add_dependency "rb-kqueue", "~> 0.2.0" + s.add_dependency "rest-client", "~> 1.7" s.add_dependency "wdm", "~> 0.1.0" s.add_dependency "winrm", "~> 1.1.3" @@ -33,6 +35,8 @@ Gem::Specification.new do |s| s.add_development_dependency "rake" s.add_development_dependency "rspec", "~> 2.14.0" + s.add_development_dependency "webmock", "~> 1.20" + s.add_development_dependency "fake_ftp", "~> 0.1" # The following block of code determines the files that should be included # in the gem. It does this by reading all the files in the directory where diff --git a/website/docs/source/layouts/layout.erb b/website/docs/source/layouts/layout.erb index 688b7507d..8a1883920 100644 --- a/website/docs/source/layouts/layout.erb +++ b/website/docs/source/layouts/layout.erb @@ -288,6 +288,17 @@ <% end %> + >Push + + <% if sidebar_section == "push" %> + + <% end %> + >Other <% if sidebar_section == "other" %> diff --git a/website/docs/source/v2/boxes.html.md b/website/docs/source/v2/boxes.html.md index e3c1661de..491a5a631 100644 --- a/website/docs/source/v2/boxes.html.md +++ b/website/docs/source/v2/boxes.html.md @@ -14,7 +14,7 @@ boxes. You can read the documentation on the [vagrant box](/v2/cli/box.html) command for more information. The easiest way to use a box is to add a box from the -[publicly available catalog of Vagrant boxes](https://vagrantcloud.com). +[publicly available catalog of Vagrant boxes](https://atlas.hashicorp.com). You can also add and share your own customized boxes on this website. Boxes also support versioning so that members of your team using Vagrant @@ -27,7 +27,7 @@ sub-pages in the navigation to the left. ## Discovering Boxes The easiest way to find boxes is to look on the -[public Vagrant box catalog](https://vagrantcloud.com) +[public Vagrant box catalog](https://atlas.hashicorp.com) for a box matching your use case. The catalog contains most major operating systems as bases, as well as specialized boxes to get you up and running quickly with LAMP stacks, Ruby, Python, etc. diff --git a/website/docs/source/v2/boxes/base.html.md b/website/docs/source/v2/boxes/base.html.md index e771187a0..61007ce92 100644 --- a/website/docs/source/v2/boxes/base.html.md +++ b/website/docs/source/v2/boxes/base.html.md @@ -239,7 +239,7 @@ provider-specific guides are linked to towards the top of this page. You can distribute the box file however you'd like. However, if you want to support versioning, putting multiple providers at a single URL, pushing updates, analytics, and more, we recommend you add the box to -[Vagrant Cloud](https://vagrantcloud.com). +[HashiCorp's Atlas](https://atlas.hashicorp.com). You can upload both public and private boxes to this service. diff --git a/website/docs/source/v2/boxes/format.html.md b/website/docs/source/v2/boxes/format.html.md index 0eb584026..3fa50479b 100644 --- a/website/docs/source/v2/boxes/format.html.md +++ b/website/docs/source/v2/boxes/format.html.md @@ -23,7 +23,7 @@ Today, there are two different components: box file and so on. * Box Catalog Metadata - This is a JSON document (typically exchanged - during interactions with [Vagrant Cloud](https://vagrantcloud.com)) + during interactions with [HashiCorp's Atlas](https://atlas.hashicorp.com)) that specifies the name of the box, a description, available versions, available providers, and URLs to the actual box files (next component) for each provider and version. If this catalog @@ -78,8 +78,8 @@ providers from a single file, and more.
You don't need to manually make the metadata. If you -have an account with Vagrant Cloud, you -can create boxes there, and Vagrant Cloud automatically creates +have an account with HashiCorp's Atlas, you +can create boxes there, and HashiCorp's Atlas automatically creates the metadata for you. The format is still documented here.
diff --git a/website/docs/source/v2/boxes/versioning.html.md b/website/docs/source/v2/boxes/versioning.html.md index 61af2345a..dc4cf3e28 100644 --- a/website/docs/source/v2/boxes/versioning.html.md +++ b/website/docs/source/v2/boxes/versioning.html.md @@ -24,10 +24,10 @@ to update your own custom boxes with versions. That is covered in `vagrant box list` only shows _installed_ versions of boxes. If you want to see all available versions of a box, you'll have to find the box -on [Vagrant Cloud](https://vagrantcloud.com). An easy way to find a box -is to use the url `https://vagrantcloud.com/USER/BOX`. For example, for +on [HashiCorp's Atlas](https://atlas.hashicorp.com). An easy way to find a box +is to use the url `https://atlas.hashicorp.com/USER/BOX`. For example, for the `hashicorp/precise64` box, you can find information about it at -`https://vagrantcloud.com/hashicorp/precise64`. +`https://atlas.hashicorp.com/hashicorp/precise64`. You can check if the box you're using is outdated with `vagrant box outdated`. This can check if the box in your current Vagrant environment is outdated diff --git a/website/docs/source/v2/cli/box.html.md b/website/docs/source/v2/cli/box.html.md index 0a5cff9fb..e3b39b4cb 100644 --- a/website/docs/source/v2/cli/box.html.md +++ b/website/docs/source/v2/cli/box.html.md @@ -26,10 +26,10 @@ This adds a box with the given address to Vagrant. The address can be one of three things: * A shorthand name from the -[public catalog of available Vagrant images](https://vagrantcloud.com), +[public catalog of available Vagrant images](https://atlas.hashicorp.com), such as "hashicorp/precise64". -* File path or HTTP URL to a box in a [catalog](https://vagrantcloud.com). +* File path or HTTP URL to a box in a [catalog](https://atlas.hashicorp.com). For HTTP, basic authentication is supported and `http_proxy` environmental variables are respected. HTTPS is also supported. @@ -93,8 +93,8 @@ you're not using a catalog). to be specified.
-Checksums for versioned boxes or boxes from Vagrant Cloud: -For boxes from Vagrant Cloud, the checksums are embedded in the metadata +Checksums for versioned boxes or boxes from HashiCorp's Atlas: +For boxes from HashiCorp's Atlas, the checksums are embedded in the metadata of the box. The metadata itself is served over TLS and its format is validated.
diff --git a/website/docs/source/v2/cli/login.html.md b/website/docs/source/v2/cli/login.html.md index 5e2e56065..a984cc77d 100644 --- a/website/docs/source/v2/cli/login.html.md +++ b/website/docs/source/v2/cli/login.html.md @@ -8,7 +8,7 @@ sidebar_current: "cli-login" **Command: `vagrant login`** The login command is used to authenticate with a -[Vagrant Cloud](https://vagrantcloud.com) server. Logging is only +[HashiCorp's Atlas](https://atlas.hashicorp.com) server. Logging is only necessary if you're accessing protected boxes or using [Vagrant Share](/v2/share/index.html). diff --git a/website/docs/source/v2/getting-started/boxes.html.md b/website/docs/source/v2/getting-started/boxes.html.md index b16aa893e..6a64f8a71 100644 --- a/website/docs/source/v2/getting-started/boxes.html.md +++ b/website/docs/source/v2/getting-started/boxes.html.md @@ -27,8 +27,8 @@ $ vagrant box add hashicorp/precise32 ``` This will download the box named "hashicorp/precise32" from -[Vagrant Cloud](https://vagrantcloud.com), a place where you can find -and host boxes. While it is easiest to download boxes from Vagrant Cloud +[HashiCorp's Atlas](https://atlas.hashicorp.com), a place where you can find +and host boxes. While it is easiest to download boxes from HashiCorp's Atlas you can also add boxes from a local file, custom URL, etc. Added boxes can be re-used by multiple projects. Each project uses a box @@ -64,11 +64,11 @@ For the remainder of this getting started guide, we'll only use the this getting started guide, the first question you'll probably have is "where do I find more boxes?" -The best place to find more boxes is [Vagrant Cloud](https://vagrantcloud.com). -Vagrant Cloud has a public directory of freely available boxes that -run various platforms and technologies. Vagrant Cloud also has a great search +The best place to find more boxes is [HashiCorp's Atlas](https://atlas.hashicorp.com). +HashiCorp's Atlas has a public directory of freely available boxes that +run various platforms and technologies. HashiCorp's Atlas also has a great search feature to allow you to find the box you care about. -In addition to finding free boxes, Vagrant Cloud lets you host your own +In addition to finding free boxes, HashiCorp's Atlas lets you host your own boxes, as well as private boxes if you intend on creating boxes for your own organization. diff --git a/website/docs/source/v2/getting-started/share.html.md b/website/docs/source/v2/getting-started/share.html.md index 96c6ffe6a..694bd5d15 100644 --- a/website/docs/source/v2/getting-started/share.html.md +++ b/website/docs/source/v2/getting-started/share.html.md @@ -15,10 +15,10 @@ Vagrant Share lets you share your Vagrant environment to anyone around the world. It will give you a URL that will route directly to your Vagrant environment from any device in the world that is connected to the internet. -## Login to Vagrant Cloud +## Login to HashiCorp's Atlas Before being able to share your Vagrant environment, you'll need an account on -[Vagrant Cloud](https://vagrantcloud.com). Don't worry, it's free. +[HashiCorp's Atlas](https://atlas.hashicorp.com). Don't worry, it's free. Once you have an account, log in using `vagrant login`: diff --git a/website/docs/source/v2/hyperv/usage.html.md b/website/docs/source/v2/hyperv/usage.html.md index 7f3d9ccbe..e91c7e92e 100644 --- a/website/docs/source/v2/hyperv/usage.html.md +++ b/website/docs/source/v2/hyperv/usage.html.md @@ -17,5 +17,5 @@ admin rights. Vagrant will show you an error if it doesn't have the proper permissions. Boxes for Hyper-V can be easily found on -[Vagrant Cloud](https://vagrantcloud.com). To get started, you might +[HashiCorp's Atlas](https://atlas.hashicorp.com). To get started, you might want to try the `hashicorp/precise64` box. diff --git a/website/docs/source/v2/push/atlas.html.md b/website/docs/source/v2/push/atlas.html.md new file mode 100644 index 000000000..e5679bbf2 --- /dev/null +++ b/website/docs/source/v2/push/atlas.html.md @@ -0,0 +1,67 @@ +--- +page_title: "Vagrant Push - Atlas Strategy" +sidebar_current: "push-atlas" +description: |- + Atlas is HashiCorp's commercial offering to bring your Vagrant development + environments to production. The Vagrant Push Atlas strategy pushes your + application's code to HashiCorp's Atlas service. +--- + +# Vagrant Push + +## Atlas Strategy + +[Atlas][] is HashiCorp's commercial offering to bring your Vagrant development +environments to production. You can read more about HashiCorp's Atlas and all +its features on [the Atlas homepage][Atlas]. The Vagrant Push Atlas strategy +pushes your application's code to HashiCorp's Atlas service. + +The Vagrant Push Atlas strategy supports the following configuration options: + +- `app` - The name of the application in [HashiCorp's Atlas][Atlas]. If the + application does not exist, it will be created with user confirmation. + +- `exclude` - Add a file or file pattern to exclude from the upload, relative to + the `dir`. This value may be specified multiple times and is additive. + `exclude` take precedence over `include` values. + +- `include` - Add a file or file pattern to include in the upload, relative to + the `dir`. This value may be specified multiple times and is additive. + +- `dir` - The base directory containing the files to upload. By default this is + the same directory as the Vagrantfile, but you can specify this if you have + a `src` folder or `bin` folder or some other folder you want to upload. + +- `vcs` - If set to true, Vagrant will automatically use VCS data to determine + the files to upload. Uncommitted changes will not be deployed. + +Additionally, the following options are exposed for power users of the Vagrant +Atlas push strategy. Most users will not require these options: + +- `address` - The address of the Atlas server to upload to. By default this will + be the public Atlas server. + +- `token` - The Atlas token to use. If the user has run `vagrant login`, this + will the token generated by that command. If the environment variable + `ATLAS_TOKEN` is set, the uploader will use this value. By default, this is + nil. + + +### Usage + +The Vagrant Push Atlas strategy is defined in the `Vagrantfile` using the +`atlas` key: + +```ruby +config.push.define "atlas" do |push| + push.app = "username/application" +end +``` + +And then push the application to Atlas: + +```shell +$ vagrant push +``` + +[Atlas]: https://atlas.hashicorp.com/ "HashiCorp's Atlas Service" diff --git a/website/docs/source/v2/push/ftp.html.md b/website/docs/source/v2/push/ftp.html.md new file mode 100644 index 000000000..54a2e1427 --- /dev/null +++ b/website/docs/source/v2/push/ftp.html.md @@ -0,0 +1,62 @@ +--- +page_title: "Vagrant Push - FTP & SFTP Strategy" +sidebar_current: "push-ftp" +description: |- + +--- + +# Vagrant Push + +## FTP & SFTP Strategy + +Vagrant Push FTP and SFTP strategy pushes the code in your Vagrant development +environment to a remote FTP or SFTP server. + +The Vagrant Push FTP And SFTP strategy supports the following configuration +options: + +- `host` - The address of the remote (S)FTP server. If the (S)FTP server is + running on a non-standard port, you can specify the port after the address + (`host:port`). + +- `username` - The username to use for authentication with the (S)FTP server. + +- `password` - The password to use for authentication with the (S)FTP server. + +- `passive` - Use passive FTP (default is true). + +- `secure` - Use secure (SFTP) (default is false). + +- `destination` - The root destination on the target system to sync the files + (default is `/`). + +- `exclude` - Add a file or file pattern to exclude from the upload, relative to + the `dir`. This value may be specified multiple times and is additive. + `exclude` take precedence over `include` values. + +- `include` - Add a file or file pattern to include in the upload, relative to + the `dir`. This value may be specified multiple times and is additive. + +- `dir` - The base directory containing the files to upload. By default this is + the same directory as the Vagrantfile, but you can specify this if you have + a `src` folder or `bin` folder or some other folder you want to upload. + + +### Usage + +The Vagrant Push FTP and SFTP strategy is defined in the `Vagrantfile` using the +`ftp` key: + +```ruby +config.push.define "ftp" do |push| + push.host = "ftp.company.com" + push.username = "username" + push.password = "password" +end +``` + +And then push the application to the FTP or SFTP server: + +```shell +$ vagrant push +``` diff --git a/website/docs/source/v2/push/heroku.html.md b/website/docs/source/v2/push/heroku.html.md new file mode 100644 index 000000000..dc730dc62 --- /dev/null +++ b/website/docs/source/v2/push/heroku.html.md @@ -0,0 +1,63 @@ +--- +page_title: "Vagrant Push - Heroku Strategy" +sidebar_current: "push-heroku" +description: |- + The Vagrant Push Heroku strategy pushes your application's code to Heroku. + Only files which are committed to the Git repository are pushed to Heroku. +--- + +# Vagrant Push + +## Heroku Strategy + +[Heroku][] is a public IAAS provider that makes it easy to deploy an +application. The Vagrant Push Heroku strategy pushes your application's code to +Heroku. + +
+

+ Warning: The Vagrant Push Heroku strategy requires you + have configured your Heroku credentials and created the Heroku application. + This documentation will not cover these prerequisites, but you can read more + about them in the Heroku documentation. +

+
+ +Only files which are committed to the Git repository will be pushed to Heroku. +Additionally, the current working branch is always pushed to the Heroku, even if +it is not the "master" branch. + +The Vagrant Push Heroku strategy supports the following configuration options: + +- `app` - The name of the Heroku application. If the Heroku application does not + exist, an exception will be raised. If this value is not specified, the + basename of the directory containing the `Vagrantfile` is assumed to be the + name of the Heroku application. Since this value can change between users, it + is highly recommended that you add the `app` setting to your `Vagrantfile`. + +- `dir` - The base directory containing the Git repository to upload to Heroku. + By default this is the same directory as the Vagrantfile, but you can specify + this if you have a nested Git directory. + +- `remote` - The name of the Git remote where Heroku is configured. The default + value is "heroku". + + +### Usage + +The Vagrant Push Heroku strategy is defined in the `Vagrantfile` using the +`heroku` key: + +```ruby +config.push.define "heroku" do |push| + push.app = "my_application" +end +``` + +And then push the application to Heroku: + +```shell +$ vagrant push +``` + +[Heroku]: https://heroku.com/ "Heroku" diff --git a/website/docs/source/v2/push/index.html.md b/website/docs/source/v2/push/index.html.md new file mode 100644 index 000000000..310039176 --- /dev/null +++ b/website/docs/source/v2/push/index.html.md @@ -0,0 +1,59 @@ +--- +page_title: "Vagrant Push" +sidebar_current: "push" +description: |- + Vagrant Push is a revolutionary +--- + +# Vagrant Push + +As of version 1.8, Vagrant is capable of deploying or "pushing" application code +in the same directory as your Vagrantfile to a remote such as an FTP server or +[HashiCorp's Atlas][Atlas]. + +Pushes are defined in an application's `Vagrantfile` and are invoked using the +`vagrant push` subcommand. Much like other components of Vagrant, each Vagrant +Push plugin has its own configuration options. Please consult the documentation +for your Vagrant Push plugin for more information. Here is an example Vagrant +Push configuration section in a `Vagrantfile`: + +```ruby +config.push.define "ftp" do |push| + push.host = "ftp.company.com" + push.username = "..." + # ... +end +``` + +When the application is ready to be deployed to the FTP server, just run a +single command: + +```shell +$ vagrant push +``` + +Much like [Vagrant Providers][], Vagrant Push also supports multiple backend +declarations. Consider the common scenario of a staging and QA environment: + +```ruby +config.push.define "staging", strategy: "ftp" do |push| + # ... +end + +config.push.define "qa", strategy: "ftp" do |push| + # ... +end +``` + +In this scenario, the user must pass the name of the Vagrant Push to the +subcommand: + +```shell +$ vagrant push staging +``` + +Vagrant Push is the easiest way to deploy your application. You can read more +in the documentation links on the sidebar. + +[Atlas]: https://atlas.hashicorp.com/ "HashiCorp's Atlas Service" +[Vagrant Providers]: /v2/providers/index.html "Vagrant Providers" diff --git a/website/docs/source/v2/push/local-exec.html.md b/website/docs/source/v2/push/local-exec.html.md new file mode 100644 index 000000000..536a5eab9 --- /dev/null +++ b/website/docs/source/v2/push/local-exec.html.md @@ -0,0 +1,60 @@ +--- +page_title: "Vagrant Push - Local Exec Strategy" +sidebar_current: "push-local-exec" +description: |- + The Vagrant Push Local Exec strategy pushes your application's code using a + user-defined script. +--- + +# Vagrant Push + +## Local Exec Strategy + +The Vagrant Push Local Exec strategy allows the user to invoke an arbitrary +shell command or script as part of a push. + +
+

+ Warning: The Vagrant Push Local Exec strategy does not + perform any validation on the correctness of the shell script. +

+
+ +The Vagrant Push Local Exec strategy supports the following configuration +options: + +- `script` - The path to a script on disk (relative to the `Vagrantfile`) to + execute. Vagrant will attempt to convert this script to an executable, but an + exception will be raised if that fails. +- `inline` - The inline script to execute (as a string). + +Please note - only one of the `script` and `inline` options may be specified in +a single push definition. + +### Usage + +The Vagrant Push Local Exec strategy is defined in the `Vagrantfile` using the +`local-exec` key: + +```ruby +config.push.define "local-exec" do |push| + push.inline = <<-SCRIPT + scp . /var/www/website + SCRIPT +end +``` + +For more complicated scripts, you may store them in a separate file and read +them from the `Vagrantfile` like so: + +```ruby +config.push.define "local-exec" do |push| + push.script = "my-script.sh" +end +``` + +And then invoke the push with Vagrant: + +```shell +$ vagrant push +``` diff --git a/website/docs/source/v2/share/index.html.md b/website/docs/source/v2/share/index.html.md index 078fd5e59..20fde2781 100644 --- a/website/docs/source/v2/share/index.html.md +++ b/website/docs/source/v2/share/index.html.md @@ -34,4 +34,4 @@ to the left. We also have a section where we go into detail about the security implications of this feature. Vagrant Share requires an account with -[Vagrant Cloud](https://vagrantcloud.com) to be used. +[HashiCorp's Atlas](https://atlas.hashicorp.com) to be used. diff --git a/website/docs/source/v2/vagrantfile/machine_settings.html.md b/website/docs/source/v2/vagrantfile/machine_settings.html.md index a6f69c194..8ea060de5 100644 --- a/website/docs/source/v2/vagrantfile/machine_settings.html.md +++ b/website/docs/source/v2/vagrantfile/machine_settings.html.md @@ -20,7 +20,7 @@ for the machine to boot and be accessible. By default this is 300 seconds. `config.vm.box` - This configures what [box](/v2/boxes.html) the machine will be brought up against. The value here should be the name of an installed box or a shorthand name of a box in -[Vagrant Cloud](https://vagrantcloud.com). +[HashiCorp's Atlas](https://atlas.hashicorp.com).
@@ -28,7 +28,7 @@ of an installed box or a shorthand name of a box in the configured box on every `vagrant up`. If an update is found, Vagrant will tell the user. By default this is true. Updates will only be checked for boxes that properly support updates (boxes from -[Vagrant Cloud](https://vagrantcloud.com) +[HashiCorp's Atlas](https://atlas.hashicorp.com) or some other versioned box).
@@ -74,7 +74,7 @@ URL, then SSL certs will be verified.
`config.vm.box_url` - The URL that the configured box can be found at. -If `config.vm.box` is a shorthand to a box in [Vagrant Cloud](https://vagrantcloud.com) +If `config.vm.box` is a shorthand to a box in [HashiCorp's Atlas](https://atlas.hashicorp.com) then this value doesn't need to be specified. Otherwise, it should point to the proper place where the box can be found if it isn't installed.