Merge pull request #10037 from chrisroberts/e-vagrant-plugins-local

Define project specific plugins
This commit is contained in:
Chris Roberts 2018-07-27 09:57:41 -07:00 committed by GitHub
commit a993cbce4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1001 additions and 141 deletions

View File

@ -44,7 +44,9 @@ argv.each_index do |i|
# Do not load plugins when performing plugin operations # Do not load plugins when performing plugin operations
if arg == "plugin" if arg == "plugin"
opts[:vagrantfile_name] = "" if argv.none?{|a| a == "--local" } && !ENV["VAGRANT_LOCAL_PLUGINS_LOAD"]
opts[:vagrantfile_name] = ""
end
ENV['VAGRANT_NO_PLUGINS'] = "1" ENV['VAGRANT_NO_PLUGINS'] = "1"
# Only initialize plugins when listing installed plugins # Only initialize plugins when listing installed plugins
if argv[i+1] != "list" if argv[i+1] != "list"

View File

@ -3,6 +3,7 @@ require "vagrant/shared_helpers"
require "rubygems" require "rubygems"
require "log4r" require "log4r"
require "vagrant/util" require "vagrant/util"
require "vagrant/plugin/manager"
# Enable logging if it is requested. We do this before # Enable logging if it is requested. We do this before
# anything else so that we can setup the output before # anything else so that we can setup the output before
@ -262,35 +263,6 @@ else
global_logger.warn("resolv replacement has not been enabled!") global_logger.warn("resolv replacement has not been enabled!")
end end
# Setup the plugin manager and load any defined plugins
require_relative "vagrant/plugin/manager"
plugins = Vagrant::Plugin::Manager.instance.installed_plugins
global_logger.info("Plugins:")
plugins.each do |plugin_name, plugin_info|
installed_version = plugin_info["installed_gem_version"]
version_constraint = plugin_info["gem_version"]
installed_version = 'undefined' if installed_version.to_s.empty?
version_constraint = '> 0' if version_constraint.to_s.empty?
global_logger.info(
" - #{plugin_name} = [installed: " \
"#{installed_version} constraint: " \
"#{version_constraint}]"
)
end
if Vagrant.plugins_init?
begin
Vagrant::Bundler.instance.init!(plugins)
rescue StandardError, ScriptError => e
global_logger.error("Plugin initialization error - #{e.class}: #{e}")
e.backtrace.each do |backtrace_line|
global_logger.debug(backtrace_line)
end
raise Vagrant::Errors::PluginInitError, message: e.to_s
end
end
# A lambda that knows how to load plugins from a single directory. # A lambda that knows how to load plugins from a single directory.
plugin_load_proc = lambda do |directory| plugin_load_proc = lambda do |directory|
# We only care about directories # We only care about directories
@ -320,43 +292,3 @@ Vagrant.source_root.join("plugins").children(true).each do |directory|
# Otherwise, attempt to load from sub-directories # Otherwise, attempt to load from sub-directories
directory.children(true).each(&plugin_load_proc) directory.children(true).each(&plugin_load_proc)
end end
# If we have plugins enabled, then load those
if Vagrant.plugins_enabled?
begin
global_logger.info("Loading plugins!")
plugins.each do |plugin_name, plugin_info|
if plugin_info["require"].to_s.empty?
begin
global_logger.info("Loading plugin `#{plugin_name}` with default require: `#{plugin_name}`")
require plugin_name
rescue LoadError => err
if plugin_name.include?("-")
plugin_slash = plugin_name.gsub("-", "/")
global_logger.error("Failed to load plugin `#{plugin_name}` with default require. - #{err.class}: #{err}")
global_logger.info("Loading plugin `#{plugin_name}` with slash require: `#{plugin_slash}`")
require plugin_slash
else
raise
end
end
else
global_logger.debug("Loading plugin `#{plugin_name}` with custom require: `#{plugin_info["require"]}`")
require plugin_info["require"]
end
global_logger.debug("Successfully loaded plugin `#{plugin_name}`.")
end
if defined?(::Bundler)
global_logger.debug("Bundler detected in use. Loading `:plugins` group.")
::Bundler.require(:plugins)
end
rescue ScriptError, StandardError => err
global_logger.error("Plugin loading error: #{err.class} - #{err}")
err.backtrace.each do |backtrace_line|
global_logger.debug(backtrace_line)
end
raise Vagrant::Errors::PluginLoadError, message: err.to_s
end
else
global_logger.debug("Plugin loading is currently disabled.")
end

View File

@ -32,22 +32,42 @@ module Vagrant
@bundler ||= self.new @bundler ||= self.new
end end
# @return [Pathname] Global plugin path
attr_reader :plugin_gem_path attr_reader :plugin_gem_path
# @return [Pathname] Vagrant environment specific plugin path
attr_reader :env_plugin_gem_path
def initialize def initialize
@plugin_gem_path = Vagrant.user_data_path.join("gems", RUBY_VERSION).freeze @plugin_gem_path = Vagrant.user_data_path.join("gems", RUBY_VERSION).freeze
@logger = Log4r::Logger.new("vagrant::bundler") @logger = Log4r::Logger.new("vagrant::bundler")
end end
# Enable Vagrant environment specific plugins at given data path
#
# @param [Pathname] Path to Vagrant::Environment data directory
# @return [Pathname] Path to environment specific gem directory
def environment_path=(env_data_path)
@env_plugin_gem_path = env_data_path.join("plugins", "gems", RUBY_VERSION).freeze
end
# Initializes Bundler and the various gem paths so that we can begin # Initializes Bundler and the various gem paths so that we can begin
# loading gems. This must only be called once. # loading gems.
def init!(plugins, repair=false) def init!(plugins, repair=false)
if !@initial_specifications
@initial_specifications = Gem::Specification.find_all{true}
else
Gem::Specification.all = @initial_specifications
Gem::Specification.reset
end
# Add HashiCorp RubyGems source # Add HashiCorp RubyGems source
Gem.sources << HASHICORP_GEMSTORE if !Gem.sources.include?(HASHICORP_GEMSTORE)
Gem.sources << HASHICORP_GEMSTORE
end
# Generate dependencies for all registered plugins # Generate dependencies for all registered plugins
plugin_deps = plugins.map do |name, info| plugin_deps = plugins.map do |name, info|
Gem::Dependency.new(name, info['gem_version'].to_s.empty? ? '> 0' : info['gem_version']) Gem::Dependency.new(name, info['installed_gem_version'].to_s.empty? ? '> 0' : info['installed_gem_version'])
end end
@logger.debug("Current generated plugin dependency list: #{plugin_deps}") @logger.debug("Current generated plugin dependency list: #{plugin_deps}")
@ -78,7 +98,7 @@ module Vagrant
# Activate the gems # Activate the gems
activate_solution(solution) activate_solution(solution)
full_vagrant_spec_list = Gem::Specification.find_all{true} + full_vagrant_spec_list = @initial_specifications +
solution.map(&:full_spec) solution.map(&:full_spec)
if(defined?(::Bundler)) if(defined?(::Bundler))
@ -91,6 +111,7 @@ module Vagrant
end end
Gem::Specification.reset Gem::Specification.reset
nil
end end
# Removes any temporary files created by init # Removes any temporary files created by init
@ -101,9 +122,10 @@ module Vagrant
# Installs the list of plugins. # Installs the list of plugins.
# #
# @param [Hash] plugins # @param [Hash] plugins
# @param [Boolean] env_local Environment local plugin install
# @return [Array<Gem::Specification>] # @return [Array<Gem::Specification>]
def install(plugins, local=false) def install(plugins, env_local=false)
internal_install(plugins, nil, local: local) internal_install(plugins, nil, env_local: env_local)
end end
# Installs a local '*.gem' file so that Bundler can find it. # Installs a local '*.gem' file so that Bundler can find it.
@ -120,7 +142,7 @@ module Vagrant
} }
} }
@logger.debug("Installing local plugin - #{plugin_info}") @logger.debug("Installing local plugin - #{plugin_info}")
internal_install(plugin_info, {}) internal_install(plugin_info, nil, env_local: opts[:env_local])
plugin_source.spec plugin_source.spec
end end
@ -129,14 +151,14 @@ module Vagrant
# @param [Hash] plugins # @param [Hash] plugins
# @param [Array<String>] specific Specific plugin names to update. If # @param [Array<String>] specific Specific plugin names to update. If
# empty or nil, all plugins will be updated. # empty or nil, all plugins will be updated.
def update(plugins, specific) def update(plugins, specific, **opts)
specific ||= [] specific ||= []
update = {gems: specific.empty? ? true : specific} update = opts.merge({gems: specific.empty? ? true : specific})
internal_install(plugins, update) internal_install(plugins, update)
end end
# Clean removes any unused gems. # Clean removes any unused gems.
def clean(plugins) def clean(plugins, **opts)
@logger.debug("Cleaning Vagrant plugins of stale gems.") @logger.debug("Cleaning Vagrant plugins of stale gems.")
# Generate dependencies for all registered plugins # Generate dependencies for all registered plugins
plugin_deps = plugins.map do |name, info| plugin_deps = plugins.map do |name, info|
@ -163,6 +185,13 @@ module Vagrant
Gem::Specification.load(spec_path) Gem::Specification.load(spec_path)
end end
# Include environment specific specification if enabled
if env_plugin_gem_path
plugin_specs += Dir.glob(env_plugin_gem_path.join('specifications/*.gemspec').to_s).map do |spec_path|
Gem::Specification.load(spec_path)
end
end
@logger.debug("Generating current plugin state solution set.") @logger.debug("Generating current plugin state solution set.")
# Resolve the request set to ensure proper activation order # Resolve the request set to ensure proper activation order
@ -171,11 +200,27 @@ module Vagrant
solution_full_names = solution_specs.map(&:full_name) solution_full_names = solution_specs.map(&:full_name)
# Find all specs installed to plugins directory that are not # Find all specs installed to plugins directory that are not
# found within the solution set # found within the solution set.
plugin_specs.delete_if do |spec| plugin_specs.delete_if do |spec|
solution_full_names.include?(spec.full_name) solution_full_names.include?(spec.full_name)
end end
if env_plugin_gem_path
# If we are cleaning locally, remove any global specs. If
# not, remove any local specs
if opts[:env_local]
@logger.debug("Removing specifications that are not environment local")
plugin_specs.delete_if do |spec|
spec.full_gem_path.to_s.include?(plugin_gem_path.realpath.to_s)
end
else
@logger.debug("Removing specifications that are environment local")
plugin_specs.delete_if do |spec|
spec.full_gem_path.to_s.include?(env_plugin_gem_path.realpath.to_s)
end
end
end
@logger.debug("Specifications to be removed - #{plugin_specs.map(&:full_name)}") @logger.debug("Specifications to be removed - #{plugin_specs.map(&:full_name)}")
# Now delete all unused specs # Now delete all unused specs
@ -318,18 +363,37 @@ module Vagrant
# as we know the dependencies are satisfied and it will attempt to validate a gem's # as we know the dependencies are satisfied and it will attempt to validate a gem's
# dependencies are satisfied by gems in the install directory (which will likely not # dependencies are satisfied by gems in the install directory (which will likely not
# be true) # be true)
result = request_set.install_into(plugin_gem_path.to_s, true, install_path = extra[:env_local] ? env_plugin_gem_path : plugin_gem_path
result = request_set.install_into(install_path.to_s, true,
ignore_dependencies: true, ignore_dependencies: true,
prerelease: Vagrant.prerelease?, prerelease: Vagrant.prerelease?,
wrappers: true wrappers: true
) )
result = result.map(&:full_spec) result = result.map(&:full_spec)
result.each do |spec|
existing_paths = $LOAD_PATH.find_all{|s| s.include?(spec.full_name) }
if !existing_paths.empty?
@logger.debug("Removing existing LOAD_PATHs for #{spec.full_name} - " +
existing_paths.join(", "))
existing_paths.each{|s| $LOAD_PATH.delete(s) }
end
spec.full_require_paths.each do |r_path|
if !$LOAD_PATH.include?(r_path)
@logger.debug("Adding path to LOAD_PATH - #{r_path}")
$LOAD_PATH.unshift(r_path)
end
end
end
result result
end end
# Generate the composite resolver set totally all of vagrant (builtin + plugin set) # Generate the composite resolver set totally all of vagrant (builtin + plugin set)
def generate_vagrant_set def generate_vagrant_set
Gem::Resolver.compose_sets(generate_builtin_set, generate_plugin_set) sets = [generate_builtin_set, generate_plugin_set]
if env_plugin_gem_path && env_plugin_gem_path.exist?
sets << generate_plugin_set(env_plugin_gem_path)
end
Gem::Resolver.compose_sets(*sets)
end end
# @return [Array<[Gem::Specification, String]>] spec and directory pairs # @return [Array<[Gem::Specification, String]>] spec and directory pairs
@ -387,10 +451,16 @@ module Vagrant
# Generate the plugin resolver set. Optionally provide specification names (short or # Generate the plugin resolver set. Optionally provide specification names (short or
# full) that should be ignored # full) that should be ignored
def generate_plugin_set(skip=[]) #
# @param [Pathname] path to plugins
# @param [Array<String>] gems to skip
# @return [PluginSet]
def generate_plugin_set(*args)
plugin_path = args.detect{|i| i.is_a?(Pathname) } || plugin_gem_path
skip = args.detect{|i| i.is_a?(Array) } || []
plugin_set = PluginSet.new plugin_set = PluginSet.new
@logger.debug("Generating new plugin set instance. Skip gems - #{skip}") @logger.debug("Generating new plugin set instance. Skip gems - #{skip}")
Dir.glob(plugin_gem_path.join('specifications/*.gemspec').to_s).each do |spec_path| Dir.glob(plugin_path.join('specifications/*.gemspec').to_s).each do |spec_path|
spec = Gem::Specification.load(spec_path) spec = Gem::Specification.load(spec_path)
desired_spec_path = File.join(spec.gem_dir, "#{spec.name}.gemspec") desired_spec_path = File.join(spec.gem_dir, "#{spec.name}.gemspec")
# Vendor set requires the spec to be within the gem directory. Some gems will package their # Vendor set requires the spec to be within the gem directory. Some gems will package their

View File

@ -8,6 +8,7 @@ require 'log4r'
require 'vagrant/util/file_mode' require 'vagrant/util/file_mode'
require 'vagrant/util/platform' require 'vagrant/util/platform'
require 'vagrant/util/hash_with_indifferent_access'
require "vagrant/util/silence_warnings" require "vagrant/util/silence_warnings"
require "vagrant/vagrantfile" require "vagrant/vagrantfile"
require "vagrant/version" require "vagrant/version"
@ -146,6 +147,7 @@ module Vagrant
if opts[:local_data_path] if opts[:local_data_path]
@local_data_path = Pathname.new(File.expand_path(opts[:local_data_path], @cwd)) @local_data_path = Pathname.new(File.expand_path(opts[:local_data_path], @cwd))
end end
@logger.debug("Effective local data path: #{@local_data_path}") @logger.debug("Effective local data path: #{@local_data_path}")
# If we have a root path, load the ".vagrantplugins" file. # If we have a root path, load the ".vagrantplugins" file.
@ -163,6 +165,19 @@ module Vagrant
@default_private_key_path = @home_path.join("insecure_private_key") @default_private_key_path = @home_path.join("insecure_private_key")
copy_insecure_private_key copy_insecure_private_key
# Initialize localized plugins
plugins = Vagrant::Plugin::Manager.instance.localize!(self)
if !vagrantfile.config.vagrant.plugins.empty?
plugins = process_configured_plugins
end
# Load any environment local plugins
Vagrant::Plugin::Manager.instance.load_plugins(plugins)
plugins = Vagrant::Plugin::Manager.instance.globalize!
Vagrant::Plugin::Manager.instance.load_plugins(plugins)
# Call the hooks that does not require configurations to be loaded # Call the hooks that does not require configurations to be loaded
# by using a "clean" action runner # by using a "clean" action runner
hook(:environment_plugins_loaded, runner: Action::Runner.new(env: self)) hook(:environment_plugins_loaded, runner: Action::Runner.new(env: self))
@ -898,6 +913,64 @@ module Vagrant
protected protected
# Check for any local plugins defined within the Vagrantfile. If
# found, validate they are available. If they are not available,
# request to install them, or raise an exception
#
# @return [Hash] plugin list for loading
def process_configured_plugins
return if !Vagrant.plugins_enabled?
errors = vagrantfile.config.vagrant.validate(nil)
if !errors["vagrant"].empty?
raise Errors::ConfigInvalid,
errors: Util::TemplateRenderer.render(
"config/validation_failed",
errors: errors)
end
# Check if defined plugins are installed
installed = Plugin::Manager.instance.installed_plugins
needs_install = []
config_plugins = vagrantfile.config.vagrant.plugins
config_plugins.each do |name, info|
if !installed[name]
needs_install << name
end
end
if !needs_install.empty?
ui.warn(I18n.t("vagrant.plugins.local.uninstalled_plugins",
plugins: needs_install.sort.join(", ")))
if !Vagrant.auto_install_local_plugins?
answer = nil
until ["y", "n"].include?(answer)
answer = ui.ask(I18n.t("vagrant.plugins.local.request_plugin_install") + " [N]: ")
answer.strip!.downcase!
answer = "n" if answer.to_s.empty?
end
if answer == "n"
raise Errors::PluginMissingLocalError,
plugins: needs_install.sort.join(", ")
end
end
needs_install.each do |name|
pconfig = Util::HashWithIndifferentAccess.new(config_plugins[name])
ui.info(I18n.t("vagrant.commands.plugin.installing", name: name))
options = {sources: Vagrant::Bundler::DEFAULT_GEM_SOURCES.dup, env_local: true}
options[:sources] = pconfig[:sources] if pconfig[:sources]
options[:require] = pconfig[:entry_point] if pconfig[:entry_point]
options[:version] = pconfig[:version] if pconfig[:version]
spec = Plugin::Manager.instance.install_plugin(name, options)
ui.info(I18n.t("vagrant.commands.plugin.installed",
name: spec.name, version: spec.version.to_s))
end
ui.info("\n")
Vagrant::Plugin::Manager.instance.localize!(self)
end
Vagrant::Plugin::Manager.instance.local_file.installed_plugins
end
# This method copies the private key into the home directory if it # This method copies the private key into the home directory if it
# doesn't already exist. # doesn't already exist.
# #

View File

@ -640,6 +640,14 @@ module Vagrant
error_key(:plugin_source_error) error_key(:plugin_source_error)
end end
class PluginNoLocalError < VagrantError
error_key(:plugin_no_local_error)
end
class PluginMissingLocalError < VagrantError
error_key(:plugin_missing_local_error)
end
class PushesNotDefined < VagrantError class PushesNotDefined < VagrantError
error_key(:pushes_not_defined) error_key(:pushes_not_defined)
end end

View File

@ -27,13 +27,78 @@ module Vagrant
@instance ||= self.new(user_plugins_file) @instance ||= self.new(user_plugins_file)
end end
attr_reader :user_file
attr_reader :system_file
attr_reader :local_file
# @param [Pathname] user_file # @param [Pathname] user_file
def initialize(user_file) def initialize(user_file)
@logger = Log4r::Logger.new("vagrant::plugin::manager")
@user_file = StateFile.new(user_file) @user_file = StateFile.new(user_file)
system_path = self.class.system_plugins_file system_path = self.class.system_plugins_file
@system_file = nil @system_file = nil
@system_file = StateFile.new(system_path) if system_path && system_path.file? @system_file = StateFile.new(system_path) if system_path && system_path.file?
@local_file = nil
end
# Enable global plugins
#
# @return [Hash] list of plugins
def globalize!
@logger.debug("Enabling globalized plugins")
plugins = installed_plugins
bundler_init(plugins)
plugins
end
# Enable environment local plugins
#
# @param [Environment] env Vagrant environment
# @return [Hash] list of plugins
def localize!(env)
if env.local_data_path
@logger.debug("Enabling localized plugins")
@local_file = StateFile.new(env.local_data_path.join("plugins.json"))
Vagrant::Bundler.instance.environment_path = env.local_data_path
plugins = local_file.installed_plugins
bundler_init(plugins)
plugins
end
end
# Initialize bundler with given plugins
#
# @param [Hash] plugins List of plugins
# @return [nil]
def bundler_init(plugins)
if !Vagrant.plugins_init?
@logger.warn("Plugin initialization is disabled")
return nil
end
@logger.info("Plugins:")
plugins.each do |plugin_name, plugin_info|
installed_version = plugin_info["installed_gem_version"]
version_constraint = plugin_info["gem_version"]
installed_version = 'undefined' if installed_version.to_s.empty?
version_constraint = '> 0' if version_constraint.to_s.empty?
@logger.info(
" - #{plugin_name} = [installed: " \
"#{installed_version} constraint: " \
"#{version_constraint}]"
)
end
begin
Vagrant::Bundler.instance.init!(plugins)
rescue StandardError, ScriptError => err
@logger.error("Plugin initialization error - #{err.class}: #{err}")
err.backtrace.each do |backtrace_line|
@logger.debug(backtrace_line)
end
raise Vagrant::Errors::PluginInitError, message: err.to_s
end
end end
# Installs another plugin into our gem directory. # Installs another plugin into our gem directory.
@ -41,7 +106,10 @@ module Vagrant
# @param [String] name Name of the plugin (gem) # @param [String] name Name of the plugin (gem)
# @return [Gem::Specification] # @return [Gem::Specification]
def install_plugin(name, **opts) def install_plugin(name, **opts)
local = false if opts[:env_local] && @local_file.nil?
raise Errors::PluginNoLocalError
end
if name =~ /\.gem$/ if name =~ /\.gem$/
# If this is a gem file, then we install that gem locally. # If this is a gem file, then we install that gem locally.
local_spec = Vagrant::Bundler.instance.install_local(name, opts) local_spec = Vagrant::Bundler.instance.install_local(name, opts)
@ -59,7 +127,7 @@ module Vagrant
if local_spec.nil? if local_spec.nil?
result = nil result = nil
install_lambda = lambda do install_lambda = lambda do
Vagrant::Bundler.instance.install(plugins, local).each do |spec| Vagrant::Bundler.instance.install(plugins, opts[:env_local]).each do |spec|
next if spec.name != name next if spec.name != name
next if result && result.version >= spec.version next if result && result.version >= spec.version
result = spec result = spec
@ -75,18 +143,20 @@ module Vagrant
result = local_spec result = local_spec
end end
# Add the plugin to the state file # Add the plugin to the state file
@user_file.add_plugin( plugin_file = opts[:env_local] ? @local_file : @user_file
plugin_file.add_plugin(
result.name, result.name,
version: opts[:version], version: opts[:version],
require: opts[:require], require: opts[:require],
sources: opts[:sources], sources: opts[:sources],
env_local: !!opts[:env_local],
installed_gem_version: result.version.to_s installed_gem_version: result.version.to_s
) )
# After install clean plugin gems to remove any cruft. This is useful # After install clean plugin gems to remove any cruft. This is useful
# for removing outdated dependencies or other versions of an installed # for removing outdated dependencies or other versions of an installed
# plugin if the plugin is upgraded/downgraded # plugin if the plugin is upgraded/downgraded
Vagrant::Bundler.instance.clean(installed_plugins) Vagrant::Bundler.instance.clean(installed_plugins, local: !!opts[:local])
result result
rescue Gem::GemNotFoundException rescue Gem::GemNotFoundException
raise Errors::PluginGemNotFound, name: name raise Errors::PluginGemNotFound, name: name
@ -97,7 +167,7 @@ module Vagrant
# Uninstalls the plugin with the given name. # Uninstalls the plugin with the given name.
# #
# @param [String] name # @param [String] name
def uninstall_plugin(name) def uninstall_plugin(name, **opts)
if @system_file if @system_file
if !@user_file.has_plugin?(name) && @system_file.has_plugin?(name) if !@user_file.has_plugin?(name) && @system_file.has_plugin?(name)
raise Errors::PluginUninstallSystem, raise Errors::PluginUninstallSystem,
@ -105,7 +175,18 @@ module Vagrant
end end
end end
@user_file.remove_plugin(name) if opts[:env_local] && @local_file.nil?
raise Errors::PluginNoLocalError
end
plugin_file = opts[:env_local] ? @local_file : @user_file
if !plugin_file.has_plugin?(name)
raise Errors::PluginNotInstalled,
name: name
end
plugin_file.remove_plugin(name)
# Clean the environment, removing any old plugins # Clean the environment, removing any old plugins
Vagrant::Bundler.instance.clean(installed_plugins) Vagrant::Bundler.instance.clean(installed_plugins)
@ -114,9 +195,15 @@ module Vagrant
end end
# Updates all or a specific set of plugins. # Updates all or a specific set of plugins.
def update_plugins(specific) def update_plugins(specific, **opts)
result = Vagrant::Bundler.instance.update(installed_plugins, specific) if opts[:env_local] && @local_file.nil?
installed_plugins.each do |name, info| raise Errors::PluginNoLocalError
end
plugin_file = opts[:env_local] ? @local_file : @user_file
result = Vagrant::Bundler.instance.update(plugin_file.installed_plugins, specific)
plugin_file.installed_plugins.each do |name, info|
matching_spec = result.detect{|s| s.name == name} matching_spec = result.detect{|s| s.name == name}
info = Hash[ info = Hash[
info.map do |key, value| info.map do |key, value|
@ -124,7 +211,7 @@ module Vagrant
end end
] ]
if matching_spec if matching_spec
@user_file.add_plugin(name, **info.merge( plugin_file.add_plugin(name, **info.merge(
version: "> 0", version: "> 0",
installed_gem_version: matching_spec.version.to_s installed_gem_version: matching_spec.version.to_s
)) ))
@ -148,6 +235,11 @@ module Vagrant
end end
plugin_list = Util::DeepMerge.deep_merge(system, @user_file.installed_plugins) plugin_list = Util::DeepMerge.deep_merge(system, @user_file.installed_plugins)
if @local_file
plugin_list = Util::DeepMerge.deep_merge(plugin_list,
@local_file.installed_plugins)
end
# Sort plugins by name # Sort plugins by name
Hash[ Hash[
plugin_list.map{|plugin_name, plugin_info| plugin_list.map{|plugin_name, plugin_info|
@ -191,6 +283,58 @@ module Vagrant
installed_map.values installed_map.values
end end
# Loads the requested plugins into the Vagrant runtime
#
# @param [Hash] plugins List of plugins to load
# @return [nil]
def load_plugins(plugins)
if !Vagrant.plugins_enabled?
@logger.warn("Plugin loading is disabled")
return
end
if plugins.nil?
@logger.debug("No plugins provided for loading")
return
end
begin
@logger.info("Loading plugins...")
plugins.each do |plugin_name, plugin_info|
if plugin_info["require"].to_s.empty?
begin
@logger.info("Loading plugin `#{plugin_name}` with default require: `#{plugin_name}`")
require plugin_name
rescue LoadError => err
if plugin_name.include?("-")
plugin_slash = plugin_name.gsub("-", "/")
@logger.error("Failed to load plugin `#{plugin_name}` with default require. - #{err.class}: #{err}")
@logger.info("Loading plugin `#{plugin_name}` with slash require: `#{plugin_slash}`")
require plugin_slash
else
raise
end
end
else
@logger.debug("Loading plugin `#{plugin_name}` with custom require: `#{plugin_info["require"]}`")
require plugin_info["require"]
end
@logger.debug("Successfully loaded plugin `#{plugin_name}`.")
end
if defined?(::Bundler)
@logger.debug("Bundler detected in use. Loading `:plugins` group.")
::Bundler.require(:plugins)
end
rescue ScriptError, StandardError => err
@logger.error("Plugin loading error: #{err.class} - #{err}")
err.backtrace.each do |backtrace_line|
@logger.debug(backtrace_line)
end
raise Vagrant::Errors::PluginLoadError, message: err.to_s
end
nil
end
end end
end end
end end

View File

@ -7,6 +7,10 @@ module Vagrant
# This is a helper to deal with the plugin state file that Vagrant # This is a helper to deal with the plugin state file that Vagrant
# uses to track what plugins are installed and activated and such. # uses to track what plugins are installed and activated and such.
class StateFile class StateFile
# @return [Pathname] path to file
attr_reader :path
def initialize(path) def initialize(path)
@path = path @path = path
@ -36,7 +40,8 @@ module Vagrant
"gem_version" => opts[:version] || "", "gem_version" => opts[:version] || "",
"require" => opts[:require] || "", "require" => opts[:require] || "",
"sources" => opts[:sources] || [], "sources" => opts[:sources] || [],
"installed_gem_version" => opts[:installed_gem_version] "installed_gem_version" => opts[:installed_gem_version],
"env_local" => !!opts[:env_local]
} }
save! save!

View File

@ -130,6 +130,18 @@ module Vagrant
end end
end end
# Automatically install locally defined plugins instead of
# waiting for user confirmation.
#
# @return [Boolean]
def self.auto_install_local_plugins?
if ENV["VAGRANT_INSTALL_LOCAL_PLUGINS"]
true
else
false
end
end
# Use Ruby Resolv in place of libc # Use Ruby Resolv in place of libc
# #
# @return [boolean] enabled or not # @return [boolean] enabled or not

View File

@ -41,6 +41,13 @@ module VagrantPlugins
end end
end end
# This middleware sequence will repair installed local plugins.
def self.action_repair_local
Vagrant::Action::Builder.new.tap do |b|
b.use RepairPluginsLocal
end
end
# This middleware sequence will uninstall a plugin. # This middleware sequence will uninstall a plugin.
def self.action_uninstall def self.action_uninstall
Vagrant::Action::Builder.new.tap do |b| Vagrant::Action::Builder.new.tap do |b|
@ -64,6 +71,7 @@ module VagrantPlugins
autoload :ListPlugins, action_root.join("list_plugins") autoload :ListPlugins, action_root.join("list_plugins")
autoload :PluginExistsCheck, action_root.join("plugin_exists_check") autoload :PluginExistsCheck, action_root.join("plugin_exists_check")
autoload :RepairPlugins, action_root.join("repair_plugins") autoload :RepairPlugins, action_root.join("repair_plugins")
autoload :RepairPluginsLocal, action_root.join("repair_plugins")
autoload :UninstallPlugin, action_root.join("uninstall_plugin") autoload :UninstallPlugin, action_root.join("uninstall_plugin")
autoload :UpdateGems, action_root.join("update_gems") autoload :UpdateGems, action_root.join("update_gems")
end end

View File

@ -42,17 +42,25 @@ module VagrantPlugins
end end
if !abort_action if !abort_action
plugins_json = File.join(env[:home_path], "plugins.json") files = []
plugins_gems = env[:gems_path] dirs = []
if File.exist?(plugins_json) # Do not include global paths if local only
FileUtils.rm(plugins_json) if !env[:env_local_only] || env[:global_only]
files << Vagrant::Plugin::Manager.instance.user_file.path
dirs << Vagrant::Bundler.instance.plugin_gem_path
end end
if File.directory?(plugins_gems) # Add local paths if they exist
FileUtils.rm_rf(plugins_gems) if Vagrant::Plugin::Manager.instance.local_file && (env[:env_local_only] || !env[:global_only])
files << Vagrant::Plugin::Manager.instance.local_file.path
dirs << Vagrant::Bundler.instance.env_plugin_gem_path
end end
# Expunge files and directories
files.find_all(&:exist?).map(&:delete)
dirs.find_all(&:exist?).map(&:rmtree)
env[:ui].info(I18n.t("vagrant.commands.plugin.expunge_complete")) env[:ui].info(I18n.t("vagrant.commands.plugin.expunge_complete"))
@app.call(env) @app.call(env)

View File

@ -18,6 +18,7 @@ module VagrantPlugins
plugin_name = env[:plugin_name] plugin_name = env[:plugin_name]
sources = env[:plugin_sources] sources = env[:plugin_sources]
version = env[:plugin_version] version = env[:plugin_version]
env_local = env[:plugin_env_local]
# Install the gem # Install the gem
plugin_name_label = plugin_name plugin_name_label = plugin_name
@ -28,10 +29,11 @@ module VagrantPlugins
manager = Vagrant::Plugin::Manager.instance manager = Vagrant::Plugin::Manager.instance
plugin_spec = manager.install_plugin( plugin_spec = manager.install_plugin(
plugin_name, plugin_name,
version: version, version: version,
require: entrypoint, require: entrypoint,
sources: sources, sources: sources,
verbose: !!env[:plugin_verbose], verbose: !!env[:plugin_verbose],
env_local: env_local
) )
# Record it so we can uninstall if something goes wrong # Record it so we can uninstall if something goes wrong

View File

@ -35,13 +35,16 @@ module VagrantPlugins
spec = specs[plugin_name] spec = specs[plugin_name]
next if spec.nil? next if spec.nil?
system = "" meta = ", global"
system = ", system" if plugin && plugin["system"] if plugin
env[:ui].info "#{spec.name} (#{spec.version}#{system})" meta = ", system" if plugin["system"]
meta = ", local" if plugin["env_local"]
end
env[:ui].info "#{spec.name} (#{spec.version}#{meta})"
env[:ui].machine("plugin-name", spec.name) env[:ui].machine("plugin-name", spec.name)
env[:ui].machine( env[:ui].machine(
"plugin-version", "plugin-version",
"#{spec.version}#{system}", "#{spec.version}#{meta}",
target: spec.name) target: spec.name)
if plugin["gem_version"] && plugin["gem_version"] != "" if plugin["gem_version"] && plugin["gem_version"] != ""

View File

@ -19,11 +19,13 @@ module VagrantPlugins
def call(env) def call(env)
env[:ui].info(I18n.t("vagrant.commands.plugin.repairing")) env[:ui].info(I18n.t("vagrant.commands.plugin.repairing"))
plugins = Vagrant::Plugin::Manager.instance.installed_plugins plugins = Vagrant::Plugin::Manager.instance.globalize!
begin begin
ENV["VAGRANT_DISABLE_PLUGIN_INIT"] = nil
Vagrant::Bundler.instance.init!(plugins, :repair) Vagrant::Bundler.instance.init!(plugins, :repair)
ENV["VAGRANT_DISABLE_PLUGIN_INIT"] = "1"
env[:ui].info(I18n.t("vagrant.commands.plugin.repair_complete")) env[:ui].info(I18n.t("vagrant.commands.plugin.repair_complete"))
rescue Exception => e rescue => e
@logger.error("Failed to repair user installed plugins: #{e.class} - #{e}") @logger.error("Failed to repair user installed plugins: #{e.class} - #{e}")
e.backtrace.each do |backtrace_line| e.backtrace.each do |backtrace_line|
@logger.debug(backtrace_line) @logger.debug(backtrace_line)
@ -34,6 +36,29 @@ module VagrantPlugins
@app.call(env) @app.call(env)
end end
end end
class RepairPluginsLocal
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new("vagrant::plugins::plugincommand::repair_local")
end
def call(env)
env[:ui].info(I18n.t("vagrant.commands.plugin.repairing_local"))
Vagrant::Plugin::Manager.instance.localize!(env[:env]).each_pair do |pname, pinfo|
env[:env].action_runner.run(Action.action_install,
plugin_name: pname,
plugin_entry_point: pinfo["require"],
plugin_sources: pinfo["sources"],
plugin_version: pinfo["gem_version"],
plugin_env_local: true
)
end
env[:ui].info(I18n.t("vagrant.commands.plugin.repair_local_complete"))
# Continue
@app.call(env)
end
end
end end
end end
end end

View File

@ -15,7 +15,7 @@ module VagrantPlugins
name: env[:plugin_name])) name: env[:plugin_name]))
manager = Vagrant::Plugin::Manager.instance manager = Vagrant::Plugin::Manager.instance
manager.uninstall_plugin(env[:plugin_name]) manager.uninstall_plugin(env[:plugin_name], env_local: env[:env_local])
@app.call(env) @app.call(env)
end end

View File

@ -16,6 +16,18 @@ module VagrantPlugins
options[:force] = force options[:force] = force
end end
o.on("--local", "Include plugins from local project for expunge") do |l|
options[:env_local] = l
end
o.on("--local-only", "Only expunge local project plugins") do |l|
options[:env_local_only] = l
end
o.on("--global-only", "Only expunge global plugins") do |l|
options[:global_only] = l
end
o.on("--reinstall", "Reinstall current plugins after expunge") do |reinstall| o.on("--reinstall", "Reinstall current plugins after expunge") do |reinstall|
options[:reinstall] = reinstall options[:reinstall] = reinstall
end end

View File

@ -9,6 +9,8 @@ module VagrantPlugins
class Install < Base class Install < Base
include MixinInstallOpts include MixinInstallOpts
LOCAL_INSTALL_PAUSE = 3
def execute def execute
options = { verbose: false } options = { verbose: false }
@ -17,6 +19,10 @@ module VagrantPlugins
o.separator "" o.separator ""
build_install_opts(o, options) build_install_opts(o, options)
o.on("--local", "Install plugin for local project only") do |l|
options[:env_local] = l
end
o.on("--verbose", "Enable verbose output for plugin installation") do |v| o.on("--verbose", "Enable verbose output for plugin installation") do |v|
options[:verbose] = v options[:verbose] = v
end end
@ -25,17 +31,51 @@ module VagrantPlugins
# Parse the options # Parse the options
argv = parse_options(opts) argv = parse_options(opts)
return if !argv return if !argv
raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length < 1
# Install the gem if argv.length < 1
argv.each do |name| raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if !options[:env_local]
action(Action.action_install, {
plugin_entry_point: options[:entry_point], errors = @env.vagrantfile.config.vagrant.validate(nil)
plugin_version: options[:plugin_version], if !errors["vagrant"].empty?
plugin_sources: options[:plugin_sources], raise Errors::ConfigInvalid,
plugin_name: name, errors: Util::TemplateRenderer.render(
plugin_verbose: options[:verbose] "config/validation_failed",
}) errors: errors)
end
local_plugins = @env.vagrantfile.config.vagrant.plugins
plugin_list = local_plugins.map do |name, info|
"#{name} (#{info.fetch(:version, "> 0")})"
end.join("\n")
@env.ui.info(I18n.t("vagrant.plugins.local.install_all",
plugins: plugin_list) + "\n")
# Pause to allow user to cancel
sleep(LOCAL_INSTALL_PAUSE)
local_plugins.each do |name, info|
action(Action.action_install,
plugin_entry_point: info[:entry_point],
plugin_version: info[:version],
plugin_sources: info[:sources] || Vagrant::Bundler::DEFAULT_GEM_SOURCES.dup,
plugin_name: name,
plugin_env_local: true
)
end
else
# Install the gem
argv.each do |name|
action(Action.action_install,
plugin_entry_point: options[:entry_point],
plugin_version: options[:plugin_version],
plugin_sources: options[:plugin_sources],
plugin_name: name,
plugin_verbose: options[:verbose],
plugin_env_local: options[:env_local]
)
end
end end
# Success, exit status 0 # Success, exit status 0

View File

@ -9,6 +9,9 @@ module VagrantPlugins
def execute def execute
opts = OptionParser.new do |o| opts = OptionParser.new do |o|
o.banner = "Usage: vagrant plugin list [-h]" o.banner = "Usage: vagrant plugin list [-h]"
# Stub option to allow Vagrantfile loading
o.on("--local", "Include local project plugins"){|_|}
end end
# Parse the options # Parse the options

View File

@ -7,8 +7,14 @@ module VagrantPlugins
module Command module Command
class Repair < Base class Repair < Base
def execute def execute
options = {}
opts = OptionParser.new do |o| opts = OptionParser.new do |o|
o.banner = "Usage: vagrant plugin repair [-h]" o.banner = "Usage: vagrant plugin repair [-h]"
o.on("--local", "Repair plugins in local project") do |l|
options[:env_local] = l
end
end end
# Parse the options # Parse the options
@ -16,8 +22,12 @@ module VagrantPlugins
return if !argv return if !argv
raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length > 0 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length > 0
if Vagrant::Plugin::Manager.instance.local_file
action(Action.action_repair_local, env: @env)
end
# Attempt to repair installed plugins # Attempt to repair installed plugins
action(Action.action_repair) action(Action.action_repair, options)
# Success, exit status 0 # Success, exit status 0
0 0

View File

@ -7,8 +7,13 @@ module VagrantPlugins
module Command module Command
class Uninstall < Base class Uninstall < Base
def execute def execute
options = {}
opts = OptionParser.new do |o| opts = OptionParser.new do |o|
o.banner = "Usage: vagrant plugin uninstall <name> [<name2> <name3> ...] [-h]" o.banner = "Usage: vagrant plugin uninstall <name> [<name2> <name3> ...] [-h]"
o.on("--local", "Remove plugin from local project") do |l|
options[:env_local] = l
end
end end
# Parse the options # Parse the options
@ -18,7 +23,7 @@ module VagrantPlugins
# Uninstall the gems # Uninstall the gems
argv.each do |gem| argv.each do |gem|
action(Action.action_uninstall, plugin_name: gem) action(Action.action_uninstall, plugin_name: gem, env_local: options[:env_local])
end end
# Success, exit status 0 # Success, exit status 0

View File

@ -10,9 +10,14 @@ module VagrantPlugins
include MixinInstallOpts include MixinInstallOpts
def execute def execute
options = {}
opts = OptionParser.new do |o| opts = OptionParser.new do |o|
o.banner = "Usage: vagrant plugin update [names...] [-h]" o.banner = "Usage: vagrant plugin update [names...] [-h]"
o.separator "" o.separator ""
o.on("--local", "Update plugin in local project") do |l|
options[:env_local] = l
end
end end
# Parse the options # Parse the options
@ -22,6 +27,7 @@ module VagrantPlugins
# Update the gem # Update the gem
action(Action.action_update, { action(Action.action_update, {
plugin_name: argv, plugin_name: argv,
env_local: options[:env_local]
}) })
# Success, exit status 0 # Success, exit status 0

View File

@ -5,16 +5,25 @@ module VagrantPlugins
class VagrantConfig < Vagrant.plugin("2", :config) class VagrantConfig < Vagrant.plugin("2", :config)
attr_accessor :host attr_accessor :host
attr_accessor :sensitive attr_accessor :sensitive
attr_accessor :plugins
VALID_PLUGIN_KEYS = [:sources, :version, :entry_point].freeze
def initialize def initialize
@host = UNSET_VALUE @host = UNSET_VALUE
@sensitive = UNSET_VALUE @sensitive = UNSET_VALUE
@plugins = UNSET_VALUE
end end
def finalize! def finalize!
@host = :detect if @host == UNSET_VALUE @host = :detect if @host == UNSET_VALUE
@host = @host.to_sym if @host @host = @host.to_sym if @host
@sensitive = nil if @sensitive == UNSET_VALUE @sensitive = nil if @sensitive == UNSET_VALUE
if @plugins == UNSET_VALUE
@plugins = {}
else
@plugins = format_plugins(@plugins)
end
if @sensitive.is_a?(Array) || @sensitive.is_a?(String) if @sensitive.is_a?(Array) || @sensitive.is_a?(String)
Array(@sensitive).each do |value| Array(@sensitive).each do |value|
@ -23,18 +32,48 @@ module VagrantPlugins
end end
end end
# Validate the configuration
#
# @param [Vagrant::Machine, NilClass] machine Machine instance or nil
# @return [Hash]
def validate(machine) def validate(machine)
errors = _detected_errors errors = _detected_errors
if @sensitive && (!@sensitive.is_a?(Array) && !@sensitive.is_a?(String)) if @sensitive && (!@sensitive.is_a?(Array) && !@sensitive.is_a?(String))
errors << I18n.t("vagrant.config.root.sensitive_bad_type") errors << I18n.t("vagrant.config.root.sensitive_bad_type")
end end
@plugins.each do |plugin_name, plugin_info|
invalid_keys = plugin_info.keys - VALID_PLUGIN_KEYS
if !invalid_keys.empty?
errors << I18n.t("vagrant.config.root.plugins_bad_key",
plugin_name: plugin_name,
plugin_key: invalid_keys.join(", ")
)
end
end
{"vagrant" => errors} {"vagrant" => errors}
end end
def to_s def to_s
"Vagrant" "Vagrant"
end end
def format_plugins(val)
result = case val
when String
{val => {}}
when Array
Hash[val.map{|item| [item.to_s, {}]}]
else
val
end
result.keys.each do |key|
result[key] = Hash[result[key].map{|k,v| [k.to_sym, v]}]
end
result
end
end end
end end
end end

View File

@ -410,6 +410,23 @@ en:
Backup: %{backup_path} Backup: %{backup_path}
plugins:
local:
uninstalled_plugins: |-
Vagrant has detected project local plugins configured for this
project which are not installed.
%{plugins}
request_plugin_install: |-
Install local plugins (Y/N)
install_all: |-
Vagrant will now install the following plugins to the local project
which have been defined in current Vagrantfile:
%{plugins}
Press ctrl-c to cancel...
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
# Translations for exception classes # Translations for exception classes
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
@ -1092,7 +1109,7 @@ en:
%{message} %{message}
plugin_not_installed: |- plugin_not_installed: |-
The plugin '%{name}' is not installed. Please install it first. The plugin '%{name}' is not currently installed.
plugin_state_file_not_parsable: |- plugin_state_file_not_parsable: |-
Failed to parse the state file "%{path}": Failed to parse the state file "%{path}":
%{message} %{message}
@ -1117,6 +1134,20 @@ en:
%{error_msg} %{error_msg}
Source: %{source} Source: %{source}
plugin_no_local_error: |-
Vagrant is not currently working within a Vagrant project directory. Local
plugins are only supported within a Vagrant project directory.
plugin_missing_local_error: |-
Vagrant is missing plugins required by the currently loaded Vagrantfile.
Please install the configured plugins and run this command again. The
following plugins are currently missing:
%{plugins}
To install the plugins configured in the current Vagrantfile run the
following command:
vagrant plugin install --local
powershell_not_found: |- powershell_not_found: |-
Failed to locate the powershell executable on the available PATH. Please Failed to locate the powershell executable on the available PATH. Please
ensure powershell is installed and available on the local PATH, then ensure powershell is installed and available on the local PATH, then
@ -1711,6 +1742,10 @@ en:
sensitive_bad_type: |- sensitive_bad_type: |-
Invalid type provided for `sensitive`. The sensitive option expects a string Invalid type provided for `sensitive`. The sensitive option expects a string
or an array of strings. or an array of strings.
plugins_bad_key: |-
Invalid plugin configuration detected for `%{plugin_name}` plugin.
Unknown keys: %{plugin_key}
bad_key: |- bad_key: |-
Unknown configuration section '%{key}'. Unknown configuration section '%{key}'.
ssh: ssh:
@ -1889,9 +1924,13 @@ en:
%{message} %{message}
repairing: |- repairing: |-
Repairing currently installed plugins. This may take a few minutes... Repairing currently installed global plugins. This may take a few minutes...
repairing_local: |-
Repairing currently installed local project plugins. This may take a few minutes...
repair_complete: |- repair_complete: |-
Installed plugins successfully repaired! Installed plugins successfully repaired!
repair_local_complete: |-
Local project plugins successfully repaired!
repair_failed: |- repair_failed: |-
Failed to automatically repair installed Vagrant plugins. To fix this Failed to automatically repair installed Vagrant plugins. To fix this
problem remove all user installed plugins and reinstall. Vagrant can problem remove all user installed plugins and reinstall. Vagrant can

View File

@ -121,7 +121,10 @@ describe "vagrant bin" do
context "plugin commands" do context "plugin commands" do
let(:argv) { ["plugin"] } let(:argv) { ["plugin"] }
before { allow(ENV).to receive(:[]=) } before do
allow(ENV).to receive(:[]=)
allow(ENV).to receive(:[])
end
it "should unset vagrantfile" do it "should unset vagrantfile" do
expect(Vagrant::Environment).to receive(:new). expect(Vagrant::Environment).to receive(:new).
@ -143,5 +146,23 @@ describe "vagrant bin" do
expect(ENV).not_to receive(:[]=).with("VAGRANT_DISABLE_PLUGIN_INIT", "1") expect(ENV).not_to receive(:[]=).with("VAGRANT_DISABLE_PLUGIN_INIT", "1")
end end
end end
context "--local" do
let(:argv) { ["plugin", "install", "--local"] }
it "should not unset vagrantfile" do
expect(Vagrant::Environment).to receive(:new).
with(hash_excluding(vagrantfile_name: "")).and_return(env)
end
end
context "with VAGRANT_LOCAL_PLUGINS_LOAD enabled" do
before { expect(ENV).to receive(:[]).with("VAGRANT_LOCAL_PLUGINS_LOAD").and_return("1") }
it "should not unset vagrantfile" do
expect(Vagrant::Environment).to receive(:new).
with(hash_excluding(vagrantfile_name: "")).and_return(env)
end
end
end end
end end

View File

@ -5,21 +5,33 @@ describe VagrantPlugins::CommandPlugin::Action::ExpungePlugins do
let(:home_path){ '/fake/file/path/.vagrant.d' } let(:home_path){ '/fake/file/path/.vagrant.d' }
let(:gems_path){ "#{home_path}/gems" } let(:gems_path){ "#{home_path}/gems" }
let(:force){ true } let(:force){ true }
let(:env_local){ false }
let(:env_local_only){ nil }
let(:global_only){ nil }
let(:env) {{ let(:env) {{
ui: Vagrant::UI::Silent.new, ui: Vagrant::UI::Silent.new,
home_path: home_path, home_path: home_path,
gems_path: gems_path, gems_path: gems_path,
force: force force: force,
env_local: env_local,
env_local_only: env_local_only,
global_only: global_only
}} }}
let(:manager) { double("manager") } let(:user_file) { double("user_file", path: user_file_pathname) }
let(:user_file_pathname) { double("user_file_pathname", exist?: true, delete: true) }
let(:local_file) { nil }
let(:bundler) { double("bundler", plugin_gem_path: plugin_gem_path,
env_plugin_gem_path: env_plugin_gem_path) }
let(:plugin_gem_path) { double("plugin_gem_path", exist?: true, rmtree: true) }
let(:env_plugin_gem_path) { nil }
let(:manager) { double("manager", user_file: user_file, local_file: local_file) }
let(:expect_to_receive) do let(:expect_to_receive) do
lambda do lambda do
allow(File).to receive(:exist?).with(File.join(home_path, 'plugins.json')).and_return(true) allow(File).to receive(:exist?).with(File.join(home_path, 'plugins.json')).and_return(true)
allow(File).to receive(:directory?).with(gems_path).and_return(true) allow(File).to receive(:directory?).with(gems_path).and_return(true)
expect(FileUtils).to receive(:rm).with(File.join(home_path, 'plugins.json'))
expect(FileUtils).to receive(:rm_rf).with(gems_path)
expect(app).to receive(:call).with(env).once expect(app).to receive(:call).with(env).once
end end
end end
@ -28,6 +40,7 @@ describe VagrantPlugins::CommandPlugin::Action::ExpungePlugins do
before do before do
allow(Vagrant::Plugin::Manager).to receive(:instance).and_return(manager) allow(Vagrant::Plugin::Manager).to receive(:instance).and_return(manager)
allow(Vagrant::Bundler).to receive(:instance).and_return(bundler)
end end
describe "#call" do describe "#call" do
@ -36,6 +49,8 @@ describe VagrantPlugins::CommandPlugin::Action::ExpungePlugins do
end end
it "should delete all plugins" do it "should delete all plugins" do
expect(user_file_pathname).to receive(:delete)
expect(plugin_gem_path).to receive(:rmtree)
subject.call(env) subject.call(env)
end end
@ -60,5 +75,94 @@ describe VagrantPlugins::CommandPlugin::Action::ExpungePlugins do
end end
end end
end end
context "when local option is set" do
let(:env_local) { true }
it "should delete plugins" do
expect(user_file_pathname).to receive(:delete)
expect(plugin_gem_path).to receive(:rmtree)
subject.call(env)
end
end
context "when local plugins exist" do
let(:local_file) { double("local_file", path: local_file_pathname) }
let(:local_file_pathname) { double("local_file_pathname", exist?: true, delete: true) }
let(:env_plugin_gem_path) { double("env_plugin_gem_path", exist?: true, rmtree: true) }
it "should delete user and local plugins" do
expect(user_file_pathname).to receive(:delete)
expect(local_file_pathname).to receive(:delete)
expect(plugin_gem_path).to receive(:rmtree)
expect(env_plugin_gem_path).to receive(:rmtree)
subject.call(env)
end
context "when local option is set" do
let(:env_local) { true }
it "should delete local plugins" do
expect(local_file_pathname).to receive(:delete)
expect(env_plugin_gem_path).to receive(:rmtree)
subject.call(env)
end
it "should delete user plugins" do
expect(user_file_pathname).to receive(:delete)
expect(plugin_gem_path).to receive(:rmtree)
subject.call(env)
end
context "when local only option is set" do
let(:env_local_only) { true }
it "should delete local plugins" do
expect(local_file_pathname).to receive(:delete)
expect(env_plugin_gem_path).to receive(:rmtree)
subject.call(env)
end
it "should not delete user plugins" do
expect(user_file_pathname).not_to receive(:delete)
expect(plugin_gem_path).not_to receive(:rmtree)
subject.call(env)
end
end
context "when global only option is set" do
let(:global_only) { true }
it "should not delete local plugins" do
expect(local_file_pathname).not_to receive(:delete)
expect(env_plugin_gem_path).not_to receive(:rmtree)
subject.call(env)
end
it "should delete user plugins" do
expect(user_file_pathname).to receive(:delete)
expect(plugin_gem_path).to receive(:rmtree)
subject.call(env)
end
end
context "when global and local only options are set" do
let(:env_local_only) { true }
let(:global_only) { true }
it "should delete local plugins" do
expect(local_file_pathname).to receive(:delete)
expect(env_plugin_gem_path).to receive(:rmtree)
subject.call(env)
end
it "should delete user plugins" do
expect(user_file_pathname).to receive(:delete)
expect(plugin_gem_path).to receive(:rmtree)
subject.call(env)
end
end
end
end
end end
end end

View File

@ -18,7 +18,7 @@ describe VagrantPlugins::CommandPlugin::Action::InstallGem do
it "should install the plugin" do it "should install the plugin" do
spec = Gem::Specification.new spec = Gem::Specification.new
expect(manager).to receive(:install_plugin).with( expect(manager).to receive(:install_plugin).with(
"foo", version: nil, require: nil, sources: nil, verbose: false).once.and_return(spec) "foo", version: nil, require: nil, sources: nil, verbose: false, env_local: nil).once.and_return(spec)
expect(app).to receive(:call).with(env).once expect(app).to receive(:call).with(env).once
@ -29,7 +29,7 @@ describe VagrantPlugins::CommandPlugin::Action::InstallGem do
it "should specify the version if given" do it "should specify the version if given" do
spec = Gem::Specification.new spec = Gem::Specification.new
expect(manager).to receive(:install_plugin).with( expect(manager).to receive(:install_plugin).with(
"foo", version: "bar", require: nil, sources: nil, verbose: false).once.and_return(spec) "foo", version: "bar", require: nil, sources: nil, verbose: false, env_local: nil).once.and_return(spec)
expect(app).to receive(:call).with(env).once expect(app).to receive(:call).with(env).once
@ -41,7 +41,7 @@ describe VagrantPlugins::CommandPlugin::Action::InstallGem do
it "should specify the entrypoint if given" do it "should specify the entrypoint if given" do
spec = Gem::Specification.new spec = Gem::Specification.new
expect(manager).to receive(:install_plugin).with( expect(manager).to receive(:install_plugin).with(
"foo", version: "bar", require: "baz", sources: nil, verbose: false).once.and_return(spec) "foo", version: "bar", require: "baz", sources: nil, verbose: false, env_local: nil).once.and_return(spec)
expect(app).to receive(:call).with(env).once expect(app).to receive(:call).with(env).once
@ -54,7 +54,7 @@ describe VagrantPlugins::CommandPlugin::Action::InstallGem do
it "should specify the sources if given" do it "should specify the sources if given" do
spec = Gem::Specification.new spec = Gem::Specification.new
expect(manager).to receive(:install_plugin).with( expect(manager).to receive(:install_plugin).with(
"foo", version: nil, require: nil, sources: ["foo"], verbose: false).once.and_return(spec) "foo", version: nil, require: nil, sources: ["foo"], verbose: false, env_local: nil).once.and_return(spec)
expect(app).to receive(:call).with(env).once expect(app).to receive(:call).with(env).once

View File

@ -15,7 +15,7 @@ describe VagrantPlugins::CommandPlugin::Action::UninstallPlugin do
end end
it "uninstalls the specified plugin" do it "uninstalls the specified plugin" do
expect(manager).to receive(:uninstall_plugin).with("bar").ordered expect(manager).to receive(:uninstall_plugin).with("bar", any_args).ordered
expect(app).to receive(:call).ordered expect(app).to receive(:call).ordered
env[:plugin_name] = "bar" env[:plugin_name] = "bar"

View File

@ -0,0 +1,129 @@
require "tmpdir"
require_relative "../base"
require "vagrant/bundler"
describe Vagrant::Bundler do
include_context "unit"
let(:iso_env) { isolated_environment }
let(:env) { iso_env.create_vagrant_env }
before do
@tmpdir = Dir.mktmpdir("vagrant-bundler-test")
@vh = ENV["VAGRANT_HOME"]
ENV["VAGRANT_HOME"] = @tmpdir
end
after do
ENV["VAGRANT_HOME"] = @vh
FileUtils.rm_rf(@tmpdir)
end
it "should isolate gem path based on Ruby version" do
expect(subject.plugin_gem_path.to_s).to end_with(RUBY_VERSION)
end
it "should not have an env_plugin_gem_path by default" do
expect(subject.env_plugin_gem_path).to be_nil
end
describe "#deinit" do
it "should provide method for backwards compatibility" do
subject.deinit
end
end
describe "#install" do
let(:plugins){ {"my-plugin" => {"gem_version" => "> 0"}} }
it "should pass plugin information hash to internal install" do
expect(subject).to receive(:internal_install).with(plugins, any_args)
subject.install(plugins)
end
it "should not include any update plugins" do
expect(subject).to receive(:internal_install).with(anything, nil, any_args)
subject.install(plugins)
end
it "should flag local when local is true" do
expect(subject).to receive(:internal_install).with(any_args, env_local: true)
subject.install(plugins, true)
end
it "should not flag local when local is not set" do
expect(subject).to receive(:internal_install).with(any_args, env_local: false)
subject.install(plugins)
end
end
describe "#install_local" do
let(:plugin_source){ double("plugin_source", spec: plugin_spec) }
let(:plugin_spec){ double("plugin_spec", name: plugin_name, version: plugin_version) }
let(:plugin_name){ "PLUGIN_NAME" }
let(:plugin_version){ "1.0.0" }
let(:plugin_path){ "PLUGIN_PATH" }
let(:sources){ "SOURCES" }
before do
allow(Gem::Source::SpecificFile).to receive(:new).and_return(plugin_source)
allow(subject).to receive(:internal_install)
end
it "should return plugin gem specification" do
expect(subject.install_local(plugin_path)).to eq(plugin_spec)
end
it "should set custom sources" do
expect(subject).to receive(:internal_install) do |info, update, opts|
expect(info[plugin_name]["sources"]).to eq(sources)
end
subject.install_local(plugin_path, sources: sources)
end
it "should not set the update parameter" do
expect(subject).to receive(:internal_install) do |info, update, opts|
expect(update).to be_nil
end
subject.install_local(plugin_path)
end
it "should not set plugin as environment local by default" do
expect(subject).to receive(:internal_install) do |info, update, opts|
expect(opts[:env_local]).to be_falsey
end
subject.install_local(plugin_path)
end
it "should set if plugin is environment local" do
expect(subject).to receive(:internal_install) do |info, update, opts|
expect(opts[:env_local]).to be_truthy
end
subject.install_local(plugin_path, env_local: true)
end
end
describe "#update" do
let(:plugins){ :plugins }
let(:specific){ [] }
after{ subject.update(plugins, specific) }
it "should mark update as true" do
expect(subject).to receive(:internal_install) do |info, update, opts|
expect(update).to be_truthy
end
end
context "with specific plugins named" do
let(:specific){ ["PLUGIN_NAME"] }
it "should set update to specific names" do
expect(subject).to receive(:internal_install) do |info, update, opts|
expect(update[:gems]).to eq(specific)
end
end
end
end
end

View File

@ -398,7 +398,13 @@ describe Vagrant::Machine do
callable = lambda { |_env| } callable = lambda { |_env| }
allow(provider).to receive(:action).with(action_name).and_return(callable) allow(provider).to receive(:action).with(action_name).and_return(callable)
allow(Vagrant::Plugin::Manager.instance).to receive(:installed_plugins)
# The first call here is to allow the environment to setup with attempting
# to load a plugin that does not exist
expect(Vagrant::Plugin::Manager.instance).to receive(:installed_plugins)
.and_return({})
expect(Vagrant::Plugin::Manager.instance).to receive(:installed_plugins)
.and_return({"vagrant-triggers"=>"stuff"}) .and_return({"vagrant-triggers"=>"stuff"})
expect(instance.instance_variable_get(:@triggers)).not_to receive(:fire_triggers) expect(instance.instance_variable_get(:@triggers)).not_to receive(:fire_triggers)

View File

@ -26,13 +26,98 @@ describe Vagrant::Plugin::Manager do
subject { described_class.new(path) } subject { described_class.new(path) }
describe "#globalize!" do
let(:plugins) { double("plugins") }
before do
allow(subject).to receive(:bundler_init)
allow(subject).to receive(:installed_plugins).and_return(plugins)
end
it "should init bundler with installed plugins" do
expect(subject).to receive(:bundler_init).with(plugins)
subject.globalize!
end
it "should return installed plugins" do
expect(subject.globalize!).to eq(plugins)
end
end
describe "#localize!" do
let(:env) { double("env", local_data_path: local_data_path) }
let(:local_data_path) { double("local_data_path") }
let(:plugins) { double("plugins") }
let(:state_file) { double("state_file", installed_plugins: plugins) }
before do
allow(Vagrant::Plugin::StateFile).to receive(:new).and_return(state_file)
allow(bundler).to receive(:environment_path=)
allow(local_data_path).to receive(:join).and_return(local_data_path)
allow(subject).to receive(:bundler_init)
end
context "without local data path defined" do
let(:local_data_path) { nil }
it "should not do any initialization" do
expect(subject).not_to receive(:bundler_init)
subject.localize!(env)
end
it "should return nil" do
expect(subject.localize!(env)).to be_nil
end
end
it "should run bundler initialization" do
expect(subject).to receive(:bundler_init).with(plugins)
subject.localize!(env)
end
it "should return plugins" do
expect(subject.localize!(env)).to eq(plugins)
end
end
describe "#bundler_init" do
let(:plugins) { {"plugin_name" => {}} }
before do
allow(Vagrant).to receive(:plugins_init?).and_return(true)
allow(bundler).to receive(:init!)
end
it "should init the bundler instance with plugins" do
expect(bundler).to receive(:init!).with(plugins)
subject.bundler_init(plugins)
end
it "should return nil" do
expect(subject.bundler_init(plugins)).to be_nil
end
context "with plugin init disabled" do
before { expect(Vagrant).to receive(:plugins_init?).and_return(false) }
it "should return nil" do
expect(subject.bundler_init(plugins)).to be_nil
end
it "should not init the bundler instance" do
expect(bundler).not_to receive(:init!).with(plugins)
subject.bundler_init(plugins)
end
end
end
describe "#install_plugin" do describe "#install_plugin" do
it "installs the plugin and adds it to the state file" do it "installs the plugin and adds it to the state file" do
specs = Array.new(5) { Gem::Specification.new } specs = Array.new(5) { Gem::Specification.new }
specs[3].name = "foo" specs[3].name = "foo"
expect(bundler).to receive(:install).once.with(any_args) { |plugins, local| expect(bundler).to receive(:install).once.with(any_args) { |plugins, local|
expect(plugins).to have_key("foo") expect(plugins).to have_key("foo")
expect(local).to be(false) expect(local).to be_falsey
}.and_return(specs) }.and_return(specs)
expect(bundler).to receive(:clean) expect(bundler).to receive(:clean)
@ -95,7 +180,7 @@ describe Vagrant::Plugin::Manager do
expect(bundler).to receive(:install).once.with(any_args) { |plugins, local| expect(bundler).to receive(:install).once.with(any_args) { |plugins, local|
expect(plugins).to have_key("foo") expect(plugins).to have_key("foo")
expect(plugins["foo"]["gem_version"]).to eql(">= 0.1.0") expect(plugins["foo"]["gem_version"]).to eql(">= 0.1.0")
expect(local).to be(false) expect(local).to be_falsey
}.and_return(specs) }.and_return(specs)
expect(bundler).to receive(:clean) expect(bundler).to receive(:clean)
@ -110,7 +195,7 @@ describe Vagrant::Plugin::Manager do
expect(bundler).to receive(:install).once.with(any_args) { |plugins, local| expect(bundler).to receive(:install).once.with(any_args) { |plugins, local|
expect(plugins).to have_key("foo") expect(plugins).to have_key("foo")
expect(plugins["foo"]["gem_version"]).to eql("0.1.0") expect(plugins["foo"]["gem_version"]).to eql("0.1.0")
expect(local).to be(false) expect(local).to be_falsey
}.and_return(specs) }.and_return(specs)
expect(bundler).to receive(:clean) expect(bundler).to receive(:clean)
@ -140,6 +225,8 @@ describe Vagrant::Plugin::Manager do
end end
it "masks bundler errors with our own error" do it "masks bundler errors with our own error" do
sf = Vagrant::Plugin::StateFile.new(path)
sf.add_plugin("foo")
expect(bundler).to receive(:clean).and_raise(Gem::InstallError) expect(bundler).to receive(:clean).and_raise(Gem::InstallError)
expect { subject.uninstall_plugin("foo") }. expect { subject.uninstall_plugin("foo") }.

View File

@ -32,6 +32,7 @@ describe Vagrant::Plugin::StateFile do
"require" => "", "require" => "",
"sources" => [], "sources" => [],
"installed_gem_version" => nil, "installed_gem_version" => nil,
"env_local" => false,
}) })
end end

View File

@ -45,6 +45,9 @@ $ vagrant plugin expunge --reinstall
This command accepts optional command-line flags: This command accepts optional command-line flags:
* `--force` - Do not prompt for confirmation prior to removal * `--force` - Do not prompt for confirmation prior to removal
* `--global-only` - Only expunge global plugins
* `--local` - Include plugins in local project
* `--local-only` - Only expunge local project plugins
* `--reinstall` - Attempt to reinstall plugins after removal * `--reinstall` - Attempt to reinstall plugins after removal
# Plugin Install # Plugin Install
@ -79,6 +82,8 @@ This command accepts optional command-line flags:
Most of the time, this is correct. If the plugin you are installing has Most of the time, this is correct. If the plugin you are installing has
another entrypoint, this flag can be used to specify it. another entrypoint, this flag can be used to specify it.
* `--local` - Install plugin to the local Vagrant project only.
* `--plugin-clean-sources` - Clears all sources that have been defined so * `--plugin-clean-sources` - Clears all sources that have been defined so
far. This is an advanced feature. The use case is primarily for corporate far. This is an advanced feature. The use case is primarily for corporate
firewalls that prevent access to RubyGems.org. firewalls that prevent access to RubyGems.org.
@ -111,6 +116,10 @@ If a version constraint was specified for a plugin when installing it, the
constraint will be listed as well. Other plugin-specific information may constraint will be listed as well. Other plugin-specific information may
be shown, too. be shown, too.
This command accepts optional command-line flags:
* `--local` - Include local project plugins.
# Plugin Repair # Plugin Repair
Vagrant may fail to properly initialize user installed custom plugins. This can Vagrant may fail to properly initialize user installed custom plugins. This can
@ -121,6 +130,10 @@ to automatically repair the problem.
If automatic repair is not successful, refer to the [expunge](#plugin-expunge) If automatic repair is not successful, refer to the [expunge](#plugin-expunge)
command command
This command accepts optional command-line flags:
* `--local` - Repair local project plugins.
# Plugin Uninstall # Plugin Uninstall
**Command: `vagrant plugin uninstall <name> [<name2> <name3> ...]`** **Command: `vagrant plugin uninstall <name> [<name2> <name3> ...]`**
@ -130,6 +143,10 @@ plugin will also be uninstalled assuming no other plugin needs them.
If multiple plugins are given, multiple plugins will be uninstalled. If multiple plugins are given, multiple plugins will be uninstalled.
This command accepts optional command-line flags:
* `--local` - Uninstall plugin from local project.
# Plugin Update # Plugin Update
**Command: `vagrant plugin update [<name>]`** **Command: `vagrant plugin update [<name>]`**
@ -142,3 +159,7 @@ the plugin using `vagrant plugin install`.
If a name is specified, only that single plugin will be updated. If a If a name is specified, only that single plugin will be updated. If a
name is specified of a plugin that is not installed, this command will not name is specified of a plugin that is not installed, this command will not
install it. install it.
This command accepts optional command-line flags:
* `--local` - Update plugin from local project.

View File

@ -161,6 +161,19 @@ may be desirable to ignore inaccessible sources and continue with the
plugin installation. Enabling this value will cause Vagrant to simply log plugin installation. Enabling this value will cause Vagrant to simply log
the plugin source error and continue. the plugin source error and continue.
## `VAGRANT_INSTALL_LOCAL_PLUGINS`
If this is set to any value, Vagrant will not prompt for confirmation
prior to installing local plugins which have been defined within the
local Vagrantfile.
## `VAGRANT_LOCAL_PLUGINS_LOAD`
If this is set Vagrant will not stub the Vagrantfile when running
`vagrant plugin` commands. When this environment variable is set the
`--local` flag will not be required by `vagrant plugin` commands to
enable local project plugins.
## `VAGRANT_NO_PARALLEL` ## `VAGRANT_NO_PARALLEL`
If this is set, Vagrant will not perform any parallel operations (such as If this is set, Vagrant will not perform any parallel operations (such as

View File

@ -22,6 +22,38 @@ the host. Vagrant needs to know this information in order to perform some
host-specific things, such as preparing NFS folders if they're enabled. host-specific things, such as preparing NFS folders if they're enabled.
You should only manually set this if auto-detection fails. You should only manually set this if auto-detection fails.
`config.vagrant.plugins` - (string, array, hash) - Define plugin, list of
plugins, or definition of plugins to install for the local project. Vagrant
will require these plugins be installed and available for the project. If
the plugins are not available, it will attempt to automatically install
them into the local project. When requiring a single plugin, a string can
be provided:
```ruby
config.vagrant.plugins = "vagrant-plugin"
```
If multiple plugins are required, they can be provided as an array:
```ruby
config.vagrant.plugins = ["vagrant-plugin", "vagrant-other-plugin"]
```
Plugins can also be defined as a Hash, which supports setting extra options
for the plugins. When a Hash is used, the key is the name of the plugin, and
the value is a Hash of options for the plugin. For example, to set an explicit
version of a plugin to install:
```ruby
config.vagrant.plugins = {"vagrant-scp" => {"version" => "1.0.0"}}
```
Supported options are:
* `entry_point` - Path for Vagrant to load plugin
* `sources` - Custom sources for downloading plugin
* `version` - Version constraint for plugin
`config.vagrant.sensitive` - (string, array) - Value or list of values that `config.vagrant.sensitive` - (string, array) - Value or list of values that
should not be displayed in Vagrant's output. Value(s) will be removed from should not be displayed in Vagrant's output. Value(s) will be removed from
Vagrant's normal UI output as well as logger output. Vagrant's normal UI output as well as logger output.