diff --git a/bin/vagrant b/bin/vagrant index 83fc5a887..7372a726d 100755 --- a/bin/vagrant +++ b/bin/vagrant @@ -44,7 +44,9 @@ argv.each_index do |i| # Do not load plugins when performing plugin operations 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" # Only initialize plugins when listing installed plugins if argv[i+1] != "list" diff --git a/lib/vagrant.rb b/lib/vagrant.rb index c775b5f37..64183c092 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -3,6 +3,7 @@ require "vagrant/shared_helpers" require "rubygems" require "log4r" require "vagrant/util" +require "vagrant/plugin/manager" # Enable logging if it is requested. We do this 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!") 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. plugin_load_proc = lambda do |directory| # 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 directory.children(true).each(&plugin_load_proc) 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 diff --git a/lib/vagrant/bundler.rb b/lib/vagrant/bundler.rb index 301e40e37..c1613f612 100644 --- a/lib/vagrant/bundler.rb +++ b/lib/vagrant/bundler.rb @@ -32,22 +32,42 @@ module Vagrant @bundler ||= self.new end + # @return [Pathname] Global plugin path attr_reader :plugin_gem_path + # @return [Pathname] Vagrant environment specific plugin path + attr_reader :env_plugin_gem_path def initialize @plugin_gem_path = Vagrant.user_data_path.join("gems", RUBY_VERSION).freeze @logger = Log4r::Logger.new("vagrant::bundler") 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 - # loading gems. This must only be called once. + # loading gems. 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 - Gem.sources << HASHICORP_GEMSTORE + if !Gem.sources.include?(HASHICORP_GEMSTORE) + Gem.sources << HASHICORP_GEMSTORE + end # Generate dependencies for all registered plugins 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 @logger.debug("Current generated plugin dependency list: #{plugin_deps}") @@ -78,7 +98,7 @@ module Vagrant # Activate the gems activate_solution(solution) - full_vagrant_spec_list = Gem::Specification.find_all{true} + + full_vagrant_spec_list = @initial_specifications + solution.map(&:full_spec) if(defined?(::Bundler)) @@ -91,6 +111,7 @@ module Vagrant end Gem::Specification.reset + nil end # Removes any temporary files created by init @@ -101,9 +122,10 @@ module Vagrant # Installs the list of plugins. # # @param [Hash] plugins + # @param [Boolean] env_local Environment local plugin install # @return [Array] - def install(plugins, local=false) - internal_install(plugins, nil, local: local) + def install(plugins, env_local=false) + internal_install(plugins, nil, env_local: env_local) end # Installs a local '*.gem' file so that Bundler can find it. @@ -120,7 +142,7 @@ module Vagrant } } @logger.debug("Installing local plugin - #{plugin_info}") - internal_install(plugin_info, {}) + internal_install(plugin_info, nil, env_local: opts[:env_local]) plugin_source.spec end @@ -129,14 +151,14 @@ module Vagrant # @param [Hash] plugins # @param [Array] specific Specific plugin names to update. If # empty or nil, all plugins will be updated. - def update(plugins, specific) + def update(plugins, specific, **opts) specific ||= [] - update = {gems: specific.empty? ? true : specific} + update = opts.merge({gems: specific.empty? ? true : specific}) internal_install(plugins, update) end # Clean removes any unused gems. - def clean(plugins) + def clean(plugins, **opts) @logger.debug("Cleaning Vagrant plugins of stale gems.") # Generate dependencies for all registered plugins plugin_deps = plugins.map do |name, info| @@ -163,6 +185,13 @@ module Vagrant Gem::Specification.load(spec_path) 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.") # Resolve the request set to ensure proper activation order @@ -171,11 +200,27 @@ module Vagrant solution_full_names = solution_specs.map(&:full_name) # 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| solution_full_names.include?(spec.full_name) 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)}") # 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 # dependencies are satisfied by gems in the install directory (which will likely not # 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, prerelease: Vagrant.prerelease?, wrappers: true ) 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 end # Generate the composite resolver set totally all of vagrant (builtin + plugin 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 # @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 # full) that should be ignored - def generate_plugin_set(skip=[]) + # + # @param [Pathname] path to plugins + # @param [Array] 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 @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) 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 diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 60eb4a685..5fcc7654d 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -8,6 +8,7 @@ require 'log4r' require 'vagrant/util/file_mode' require 'vagrant/util/platform' +require 'vagrant/util/hash_with_indifferent_access' require "vagrant/util/silence_warnings" require "vagrant/vagrantfile" require "vagrant/version" @@ -146,6 +147,7 @@ module Vagrant if opts[:local_data_path] @local_data_path = Pathname.new(File.expand_path(opts[:local_data_path], @cwd)) end + @logger.debug("Effective local data path: #{@local_data_path}") # 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") 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 # by using a "clean" action runner hook(:environment_plugins_loaded, runner: Action::Runner.new(env: self)) @@ -898,6 +913,64 @@ module Vagrant 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 # doesn't already exist. # diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 7eef2d6d9..8fe7d9e71 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -640,6 +640,14 @@ module Vagrant error_key(:plugin_source_error) end + class PluginNoLocalError < VagrantError + error_key(:plugin_no_local_error) + end + + class PluginMissingLocalError < VagrantError + error_key(:plugin_missing_local_error) + end + class PushesNotDefined < VagrantError error_key(:pushes_not_defined) end diff --git a/lib/vagrant/plugin/manager.rb b/lib/vagrant/plugin/manager.rb index 64891bd37..d409ba788 100644 --- a/lib/vagrant/plugin/manager.rb +++ b/lib/vagrant/plugin/manager.rb @@ -27,13 +27,78 @@ module Vagrant @instance ||= self.new(user_plugins_file) end + attr_reader :user_file + attr_reader :system_file + attr_reader :local_file + # @param [Pathname] user_file def initialize(user_file) + @logger = Log4r::Logger.new("vagrant::plugin::manager") @user_file = StateFile.new(user_file) system_path = self.class.system_plugins_file @system_file = nil @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 # Installs another plugin into our gem directory. @@ -41,7 +106,10 @@ module Vagrant # @param [String] name Name of the plugin (gem) # @return [Gem::Specification] def install_plugin(name, **opts) - local = false + if opts[:env_local] && @local_file.nil? + raise Errors::PluginNoLocalError + end + if name =~ /\.gem$/ # If this is a gem file, then we install that gem locally. local_spec = Vagrant::Bundler.instance.install_local(name, opts) @@ -59,7 +127,7 @@ module Vagrant if local_spec.nil? result = nil 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 result && result.version >= spec.version result = spec @@ -75,18 +143,20 @@ module Vagrant result = local_spec end # 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, version: opts[:version], require: opts[:require], sources: opts[:sources], + env_local: !!opts[:env_local], installed_gem_version: result.version.to_s ) # After install clean plugin gems to remove any cruft. This is useful # for removing outdated dependencies or other versions of an installed # plugin if the plugin is upgraded/downgraded - Vagrant::Bundler.instance.clean(installed_plugins) + Vagrant::Bundler.instance.clean(installed_plugins, local: !!opts[:local]) result rescue Gem::GemNotFoundException raise Errors::PluginGemNotFound, name: name @@ -97,7 +167,7 @@ module Vagrant # Uninstalls the plugin with the given name. # # @param [String] name - def uninstall_plugin(name) + def uninstall_plugin(name, **opts) if @system_file if !@user_file.has_plugin?(name) && @system_file.has_plugin?(name) raise Errors::PluginUninstallSystem, @@ -105,7 +175,18 @@ module Vagrant 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 Vagrant::Bundler.instance.clean(installed_plugins) @@ -114,9 +195,15 @@ module Vagrant end # Updates all or a specific set of plugins. - def update_plugins(specific) - result = Vagrant::Bundler.instance.update(installed_plugins, specific) - installed_plugins.each do |name, info| + def update_plugins(specific, **opts) + if opts[:env_local] && @local_file.nil? + 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} info = Hash[ info.map do |key, value| @@ -124,7 +211,7 @@ module Vagrant end ] if matching_spec - @user_file.add_plugin(name, **info.merge( + plugin_file.add_plugin(name, **info.merge( version: "> 0", installed_gem_version: matching_spec.version.to_s )) @@ -148,6 +235,11 @@ module Vagrant end 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 Hash[ plugin_list.map{|plugin_name, plugin_info| @@ -191,6 +283,58 @@ module Vagrant installed_map.values 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 diff --git a/lib/vagrant/plugin/state_file.rb b/lib/vagrant/plugin/state_file.rb index 85db50b92..c6872d4fd 100644 --- a/lib/vagrant/plugin/state_file.rb +++ b/lib/vagrant/plugin/state_file.rb @@ -7,6 +7,10 @@ module 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. class StateFile + + # @return [Pathname] path to file + attr_reader :path + def initialize(path) @path = path @@ -36,7 +40,8 @@ module Vagrant "gem_version" => opts[:version] || "", "require" => opts[:require] || "", "sources" => opts[:sources] || [], - "installed_gem_version" => opts[:installed_gem_version] + "installed_gem_version" => opts[:installed_gem_version], + "env_local" => !!opts[:env_local] } save! diff --git a/lib/vagrant/shared_helpers.rb b/lib/vagrant/shared_helpers.rb index 0bdcd7fa5..ada85f3ba 100644 --- a/lib/vagrant/shared_helpers.rb +++ b/lib/vagrant/shared_helpers.rb @@ -130,6 +130,18 @@ module Vagrant 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 # # @return [boolean] enabled or not diff --git a/plugins/commands/plugin/action.rb b/plugins/commands/plugin/action.rb index 5412b460f..d0f9bf8e9 100644 --- a/plugins/commands/plugin/action.rb +++ b/plugins/commands/plugin/action.rb @@ -41,6 +41,13 @@ module VagrantPlugins 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. def self.action_uninstall Vagrant::Action::Builder.new.tap do |b| @@ -64,6 +71,7 @@ module VagrantPlugins autoload :ListPlugins, action_root.join("list_plugins") autoload :PluginExistsCheck, action_root.join("plugin_exists_check") autoload :RepairPlugins, action_root.join("repair_plugins") + autoload :RepairPluginsLocal, action_root.join("repair_plugins") autoload :UninstallPlugin, action_root.join("uninstall_plugin") autoload :UpdateGems, action_root.join("update_gems") end diff --git a/plugins/commands/plugin/action/expunge_plugins.rb b/plugins/commands/plugin/action/expunge_plugins.rb index 072ef81a6..98b7f6014 100644 --- a/plugins/commands/plugin/action/expunge_plugins.rb +++ b/plugins/commands/plugin/action/expunge_plugins.rb @@ -42,17 +42,25 @@ module VagrantPlugins end if !abort_action - plugins_json = File.join(env[:home_path], "plugins.json") - plugins_gems = env[:gems_path] + files = [] + dirs = [] - if File.exist?(plugins_json) - FileUtils.rm(plugins_json) + # Do not include global paths if local only + if !env[:env_local_only] || env[:global_only] + files << Vagrant::Plugin::Manager.instance.user_file.path + dirs << Vagrant::Bundler.instance.plugin_gem_path end - if File.directory?(plugins_gems) - FileUtils.rm_rf(plugins_gems) + # Add local paths if they exist + 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 + # 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")) @app.call(env) diff --git a/plugins/commands/plugin/action/install_gem.rb b/plugins/commands/plugin/action/install_gem.rb index 320f95e83..6ffabb5ab 100644 --- a/plugins/commands/plugin/action/install_gem.rb +++ b/plugins/commands/plugin/action/install_gem.rb @@ -18,6 +18,7 @@ module VagrantPlugins plugin_name = env[:plugin_name] sources = env[:plugin_sources] version = env[:plugin_version] + env_local = env[:plugin_env_local] # Install the gem plugin_name_label = plugin_name @@ -28,10 +29,11 @@ module VagrantPlugins manager = Vagrant::Plugin::Manager.instance plugin_spec = manager.install_plugin( plugin_name, - version: version, - require: entrypoint, - sources: sources, - verbose: !!env[:plugin_verbose], + version: version, + require: entrypoint, + sources: sources, + verbose: !!env[:plugin_verbose], + env_local: env_local ) # Record it so we can uninstall if something goes wrong diff --git a/plugins/commands/plugin/action/list_plugins.rb b/plugins/commands/plugin/action/list_plugins.rb index f5f8d41bf..4ab8f3982 100644 --- a/plugins/commands/plugin/action/list_plugins.rb +++ b/plugins/commands/plugin/action/list_plugins.rb @@ -35,13 +35,16 @@ module VagrantPlugins spec = specs[plugin_name] next if spec.nil? - system = "" - system = ", system" if plugin && plugin["system"] - env[:ui].info "#{spec.name} (#{spec.version}#{system})" + meta = ", global" + if plugin + 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-version", - "#{spec.version}#{system}", + "#{spec.version}#{meta}", target: spec.name) if plugin["gem_version"] && plugin["gem_version"] != "" diff --git a/plugins/commands/plugin/action/repair_plugins.rb b/plugins/commands/plugin/action/repair_plugins.rb index 9745d378b..5421f000d 100644 --- a/plugins/commands/plugin/action/repair_plugins.rb +++ b/plugins/commands/plugin/action/repair_plugins.rb @@ -19,11 +19,13 @@ module VagrantPlugins def call(env) env[:ui].info(I18n.t("vagrant.commands.plugin.repairing")) - plugins = Vagrant::Plugin::Manager.instance.installed_plugins + plugins = Vagrant::Plugin::Manager.instance.globalize! begin + ENV["VAGRANT_DISABLE_PLUGIN_INIT"] = nil Vagrant::Bundler.instance.init!(plugins, :repair) + ENV["VAGRANT_DISABLE_PLUGIN_INIT"] = "1" 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}") e.backtrace.each do |backtrace_line| @logger.debug(backtrace_line) @@ -34,6 +36,29 @@ module VagrantPlugins @app.call(env) 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 diff --git a/plugins/commands/plugin/action/uninstall_plugin.rb b/plugins/commands/plugin/action/uninstall_plugin.rb index 0aa9b4a9a..5518581f0 100644 --- a/plugins/commands/plugin/action/uninstall_plugin.rb +++ b/plugins/commands/plugin/action/uninstall_plugin.rb @@ -15,7 +15,7 @@ module VagrantPlugins name: env[:plugin_name])) 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) end diff --git a/plugins/commands/plugin/command/expunge.rb b/plugins/commands/plugin/command/expunge.rb index 6ec16ea9d..721a44169 100644 --- a/plugins/commands/plugin/command/expunge.rb +++ b/plugins/commands/plugin/command/expunge.rb @@ -16,6 +16,18 @@ module VagrantPlugins options[:force] = force 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| options[:reinstall] = reinstall end diff --git a/plugins/commands/plugin/command/install.rb b/plugins/commands/plugin/command/install.rb index 441500387..3e3e907ac 100644 --- a/plugins/commands/plugin/command/install.rb +++ b/plugins/commands/plugin/command/install.rb @@ -9,6 +9,8 @@ module VagrantPlugins class Install < Base include MixinInstallOpts + LOCAL_INSTALL_PAUSE = 3 + def execute options = { verbose: false } @@ -17,6 +19,10 @@ module VagrantPlugins o.separator "" 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| options[:verbose] = v end @@ -25,17 +31,51 @@ module VagrantPlugins # Parse the options argv = parse_options(opts) return if !argv - raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length < 1 - # 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] - }) + if argv.length < 1 + raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if !options[:env_local] + + errors = @env.vagrantfile.config.vagrant.validate(nil) + if !errors["vagrant"].empty? + raise Errors::ConfigInvalid, + errors: Util::TemplateRenderer.render( + "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 # Success, exit status 0 diff --git a/plugins/commands/plugin/command/list.rb b/plugins/commands/plugin/command/list.rb index 7949b5db8..8e176d6b2 100644 --- a/plugins/commands/plugin/command/list.rb +++ b/plugins/commands/plugin/command/list.rb @@ -9,6 +9,9 @@ module VagrantPlugins def execute opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin list [-h]" + + # Stub option to allow Vagrantfile loading + o.on("--local", "Include local project plugins"){|_|} end # Parse the options diff --git a/plugins/commands/plugin/command/repair.rb b/plugins/commands/plugin/command/repair.rb index 6deff6ae2..57f90cbb2 100644 --- a/plugins/commands/plugin/command/repair.rb +++ b/plugins/commands/plugin/command/repair.rb @@ -7,8 +7,14 @@ module VagrantPlugins module Command class Repair < Base def execute + options = {} + opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin repair [-h]" + + o.on("--local", "Repair plugins in local project") do |l| + options[:env_local] = l + end end # Parse the options @@ -16,8 +22,12 @@ module VagrantPlugins return if !argv 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 - action(Action.action_repair) + action(Action.action_repair, options) # Success, exit status 0 0 diff --git a/plugins/commands/plugin/command/uninstall.rb b/plugins/commands/plugin/command/uninstall.rb index ec032a5d9..341f4f893 100644 --- a/plugins/commands/plugin/command/uninstall.rb +++ b/plugins/commands/plugin/command/uninstall.rb @@ -7,8 +7,13 @@ module VagrantPlugins module Command class Uninstall < Base def execute + options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin uninstall [ ...] [-h]" + + o.on("--local", "Remove plugin from local project") do |l| + options[:env_local] = l + end end # Parse the options @@ -18,7 +23,7 @@ module VagrantPlugins # Uninstall the gems argv.each do |gem| - action(Action.action_uninstall, plugin_name: gem) + action(Action.action_uninstall, plugin_name: gem, env_local: options[:env_local]) end # Success, exit status 0 diff --git a/plugins/commands/plugin/command/update.rb b/plugins/commands/plugin/command/update.rb index 96bd1f678..03abf12f3 100644 --- a/plugins/commands/plugin/command/update.rb +++ b/plugins/commands/plugin/command/update.rb @@ -10,9 +10,14 @@ module VagrantPlugins include MixinInstallOpts def execute + options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin update [names...] [-h]" o.separator "" + + o.on("--local", "Update plugin in local project") do |l| + options[:env_local] = l + end end # Parse the options @@ -22,6 +27,7 @@ module VagrantPlugins # Update the gem action(Action.action_update, { plugin_name: argv, + env_local: options[:env_local] }) # Success, exit status 0 diff --git a/plugins/kernel_v2/config/vagrant.rb b/plugins/kernel_v2/config/vagrant.rb index 951383db7..2e097d61b 100644 --- a/plugins/kernel_v2/config/vagrant.rb +++ b/plugins/kernel_v2/config/vagrant.rb @@ -5,16 +5,25 @@ module VagrantPlugins class VagrantConfig < Vagrant.plugin("2", :config) attr_accessor :host attr_accessor :sensitive + attr_accessor :plugins + + VALID_PLUGIN_KEYS = [:sources, :version, :entry_point].freeze def initialize @host = UNSET_VALUE @sensitive = UNSET_VALUE + @plugins = UNSET_VALUE end def finalize! @host = :detect if @host == UNSET_VALUE @host = @host.to_sym if @host @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) Array(@sensitive).each do |value| @@ -23,18 +32,48 @@ module VagrantPlugins end end + # Validate the configuration + # + # @param [Vagrant::Machine, NilClass] machine Machine instance or nil + # @return [Hash] def validate(machine) errors = _detected_errors if @sensitive && (!@sensitive.is_a?(Array) && !@sensitive.is_a?(String)) errors << I18n.t("vagrant.config.root.sensitive_bad_type") 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} end def to_s "Vagrant" 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 diff --git a/templates/locales/en.yml b/templates/locales/en.yml index c2b412112..167f7f7a1 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -410,6 +410,23 @@ en: 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 #------------------------------------------------------------------------------- @@ -1092,7 +1109,7 @@ en: %{message} 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: |- Failed to parse the state file "%{path}": %{message} @@ -1117,6 +1134,20 @@ en: %{error_msg} 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: |- Failed to locate the powershell executable on the available PATH. Please ensure powershell is installed and available on the local PATH, then @@ -1711,6 +1742,10 @@ en: sensitive_bad_type: |- Invalid type provided for `sensitive`. The sensitive option expects a string or an array of strings. + plugins_bad_key: |- + Invalid plugin configuration detected for `%{plugin_name}` plugin. + + Unknown keys: %{plugin_key} bad_key: |- Unknown configuration section '%{key}'. ssh: @@ -1889,9 +1924,13 @@ en: %{message} 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: |- Installed plugins successfully repaired! + repair_local_complete: |- + Local project plugins successfully repaired! repair_failed: |- Failed to automatically repair installed Vagrant plugins. To fix this problem remove all user installed plugins and reinstall. Vagrant can diff --git a/test/unit/bin/vagrant_test.rb b/test/unit/bin/vagrant_test.rb index 4b5dfbe5b..08edcb20e 100644 --- a/test/unit/bin/vagrant_test.rb +++ b/test/unit/bin/vagrant_test.rb @@ -121,7 +121,10 @@ describe "vagrant bin" do context "plugin commands" do let(:argv) { ["plugin"] } - before { allow(ENV).to receive(:[]=) } + before do + allow(ENV).to receive(:[]=) + allow(ENV).to receive(:[]) + end it "should unset vagrantfile" do 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") 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 diff --git a/test/unit/plugins/commands/plugin/action/expunge_plugins_test.rb b/test/unit/plugins/commands/plugin/action/expunge_plugins_test.rb index a4b0a4644..a58f38d33 100644 --- a/test/unit/plugins/commands/plugin/action/expunge_plugins_test.rb +++ b/test/unit/plugins/commands/plugin/action/expunge_plugins_test.rb @@ -5,21 +5,33 @@ describe VagrantPlugins::CommandPlugin::Action::ExpungePlugins do let(:home_path){ '/fake/file/path/.vagrant.d' } let(:gems_path){ "#{home_path}/gems" } let(:force){ true } + let(:env_local){ false } + let(:env_local_only){ nil } + let(:global_only){ nil } let(:env) {{ ui: Vagrant::UI::Silent.new, home_path: home_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 lambda do 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) - 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 end end @@ -28,6 +40,7 @@ describe VagrantPlugins::CommandPlugin::Action::ExpungePlugins do before do allow(Vagrant::Plugin::Manager).to receive(:instance).and_return(manager) + allow(Vagrant::Bundler).to receive(:instance).and_return(bundler) end describe "#call" do @@ -36,6 +49,8 @@ describe VagrantPlugins::CommandPlugin::Action::ExpungePlugins do end it "should delete all plugins" do + expect(user_file_pathname).to receive(:delete) + expect(plugin_gem_path).to receive(:rmtree) subject.call(env) end @@ -60,5 +75,94 @@ describe VagrantPlugins::CommandPlugin::Action::ExpungePlugins do 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 diff --git a/test/unit/plugins/commands/plugin/action/install_gem_test.rb b/test/unit/plugins/commands/plugin/action/install_gem_test.rb index 908473577..435595041 100644 --- a/test/unit/plugins/commands/plugin/action/install_gem_test.rb +++ b/test/unit/plugins/commands/plugin/action/install_gem_test.rb @@ -18,7 +18,7 @@ describe VagrantPlugins::CommandPlugin::Action::InstallGem do it "should install the plugin" do spec = Gem::Specification.new 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 @@ -29,7 +29,7 @@ describe VagrantPlugins::CommandPlugin::Action::InstallGem do it "should specify the version if given" do spec = Gem::Specification.new 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 @@ -41,7 +41,7 @@ describe VagrantPlugins::CommandPlugin::Action::InstallGem do it "should specify the entrypoint if given" do spec = Gem::Specification.new 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 @@ -54,7 +54,7 @@ describe VagrantPlugins::CommandPlugin::Action::InstallGem do it "should specify the sources if given" do spec = Gem::Specification.new 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 diff --git a/test/unit/plugins/commands/plugin/action/uninstall_plugin_test.rb b/test/unit/plugins/commands/plugin/action/uninstall_plugin_test.rb index 757d75be0..39b2790aa 100644 --- a/test/unit/plugins/commands/plugin/action/uninstall_plugin_test.rb +++ b/test/unit/plugins/commands/plugin/action/uninstall_plugin_test.rb @@ -15,7 +15,7 @@ describe VagrantPlugins::CommandPlugin::Action::UninstallPlugin do end 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 env[:plugin_name] = "bar" diff --git a/test/unit/vagrant/bundler_test.rb b/test/unit/vagrant/bundler_test.rb new file mode 100644 index 000000000..d27474349 --- /dev/null +++ b/test/unit/vagrant/bundler_test.rb @@ -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 diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index 0e22b8a06..2cc904d88 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -398,7 +398,13 @@ describe Vagrant::Machine do callable = lambda { |_env| } 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"}) expect(instance.instance_variable_get(:@triggers)).not_to receive(:fire_triggers) diff --git a/test/unit/vagrant/plugin/manager_test.rb b/test/unit/vagrant/plugin/manager_test.rb index de42c7ec8..aa6b11b7c 100644 --- a/test/unit/vagrant/plugin/manager_test.rb +++ b/test/unit/vagrant/plugin/manager_test.rb @@ -26,13 +26,98 @@ describe Vagrant::Plugin::Manager do 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 it "installs the plugin and adds it to the state file" do specs = Array.new(5) { Gem::Specification.new } specs[3].name = "foo" expect(bundler).to receive(:install).once.with(any_args) { |plugins, local| expect(plugins).to have_key("foo") - expect(local).to be(false) + expect(local).to be_falsey }.and_return(specs) 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(plugins).to have_key("foo") expect(plugins["foo"]["gem_version"]).to eql(">= 0.1.0") - expect(local).to be(false) + expect(local).to be_falsey }.and_return(specs) 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(plugins).to have_key("foo") expect(plugins["foo"]["gem_version"]).to eql("0.1.0") - expect(local).to be(false) + expect(local).to be_falsey }.and_return(specs) expect(bundler).to receive(:clean) @@ -140,6 +225,8 @@ describe Vagrant::Plugin::Manager do end 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 { subject.uninstall_plugin("foo") }. diff --git a/test/unit/vagrant/plugin/state_file_test.rb b/test/unit/vagrant/plugin/state_file_test.rb index 57818064d..9546db7bc 100644 --- a/test/unit/vagrant/plugin/state_file_test.rb +++ b/test/unit/vagrant/plugin/state_file_test.rb @@ -32,6 +32,7 @@ describe Vagrant::Plugin::StateFile do "require" => "", "sources" => [], "installed_gem_version" => nil, + "env_local" => false, }) end diff --git a/website/source/docs/cli/plugin.html.md b/website/source/docs/cli/plugin.html.md index dcadb73b8..59a388742 100644 --- a/website/source/docs/cli/plugin.html.md +++ b/website/source/docs/cli/plugin.html.md @@ -45,6 +45,9 @@ $ vagrant plugin expunge --reinstall This command accepts optional command-line flags: * `--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 # 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 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 far. This is an advanced feature. The use case is primarily for corporate 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 be shown, too. +This command accepts optional command-line flags: + +* `--local` - Include local project plugins. + # Plugin Repair 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) command +This command accepts optional command-line flags: + +* `--local` - Repair local project plugins. + # Plugin Uninstall **Command: `vagrant plugin uninstall [ ...]`** @@ -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. +This command accepts optional command-line flags: + +* `--local` - Uninstall plugin from local project. + # Plugin Update **Command: `vagrant plugin update []`** @@ -142,3 +159,7 @@ the plugin using `vagrant plugin install`. 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 install it. + +This command accepts optional command-line flags: + +* `--local` - Update plugin from local project. diff --git a/website/source/docs/other/environmental-variables.html.md b/website/source/docs/other/environmental-variables.html.md index 93ceeb26d..6db684e23 100644 --- a/website/source/docs/other/environmental-variables.html.md +++ b/website/source/docs/other/environmental-variables.html.md @@ -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 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` If this is set, Vagrant will not perform any parallel operations (such as diff --git a/website/source/docs/vagrantfile/vagrant_settings.html.md b/website/source/docs/vagrantfile/vagrant_settings.html.md index e0a9de414..e8ff18eb7 100644 --- a/website/source/docs/vagrantfile/vagrant_settings.html.md +++ b/website/source/docs/vagrantfile/vagrant_settings.html.md @@ -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. 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 should not be displayed in Vagrant's output. Value(s) will be removed from Vagrant's normal UI output as well as logger output.