diff --git a/lib/vagrant/action/builtin/after_trigger.rb b/lib/vagrant/action/builtin/after_trigger.rb new file mode 100644 index 000000000..106d10df1 --- /dev/null +++ b/lib/vagrant/action/builtin/after_trigger.rb @@ -0,0 +1,31 @@ +module Vagrant + module Action + module Builtin + # This class is intended to be used by the Action::Warden class for executing + # action triggers before any given action. + # + # @param [Symbol] action_name - name to fire trigger on + # @param [Vagrant::Plugin::V2::Triger] triggers - trigger object + class AfterTriggerAction + # @param [Symbol] action_name - The action class name to fire trigger on + # @param [Vagrant::Plugin::V2::Triger] triggers - trigger object + def initialize(app, env, action_name, triggers) + @app = app + @env = env + @triggers = triggers + @action_name = action_name + end + + def call(env) + machine = env[:machine] + machine_name = machine.name if machine + + @triggers.fire_triggers(@action_name, :after, machine_name, :action) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers"); + + # Carry on + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/before_trigger.rb b/lib/vagrant/action/builtin/before_trigger.rb new file mode 100644 index 000000000..fad2e5799 --- /dev/null +++ b/lib/vagrant/action/builtin/before_trigger.rb @@ -0,0 +1,28 @@ +module Vagrant + module Action + module Builtin + # This class is intended to be used by the Action::Warden class for executing + # action triggers before any given action. + class BeforeTriggerAction + # @param [Symbol] action_name - The action class name to fire trigger on + # @param [Vagrant::Plugin::V2::Triger] triggers - trigger object + def initialize(app, env, action_name, triggers) + @app = app + @env = env + @triggers = triggers + @action_name = action_name + end + + def call(env) + machine = env[:machine] + machine_name = machine.name if machine + + @triggers.fire_triggers(@action_name, :before, machine_name, :action) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers"); + + # Carry on + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/runner.rb b/lib/vagrant/action/runner.rb index 8799a8d7f..fb70fa317 100644 --- a/lib/vagrant/action/runner.rb +++ b/lib/vagrant/action/runner.rb @@ -2,6 +2,7 @@ require 'log4r' require 'vagrant/action/hook' require 'vagrant/util/busy' +require 'vagrant/util/experimental' module Vagrant module Action @@ -33,6 +34,19 @@ module Vagrant environment.merge!(@lazy_globals.call) if @lazy_globals environment.merge!(options || {}) + if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") + # NOTE: Triggers are initialized later in the Action::Runer because of + # how `@lazy_globals` are evaluated. Rather than trying to guess where + # the `env` is coming from, we can wait until they're merged into a single + # hash above. + env = environment[:env] + machine = environment[:machine] + machine_name = machine.name if machine + + ui = Vagrant::UI::Prefixed.new(env.ui, "vargant") + triggers = Vagrant::Plugin::V2::Trigger.new(env, env.vagrantfile.config.trigger, machine, ui) + end + # Setup the action hooks hooks = Vagrant.plugin("2").manager.action_hooks(environment[:action_name]) if !hooks.empty? @@ -61,10 +75,16 @@ module Vagrant @@reported_interrupt = true end + action_name = environment[:action_name] + + triggers.fire_triggers(action_name, :before, machine_name, :hook) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") + # We place a process lock around every action that is called @logger.info("Running action: #{environment[:action_name]} #{callable_id}") Util::Busy.busy(int_callback) { callable.call(environment) } + triggers.fire_triggers(action_name, :after, machine_name, :hook) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") + # Return the environment in case there are things in there that # the caller wants to use. environment diff --git a/lib/vagrant/action/warden.rb b/lib/vagrant/action/warden.rb index ddf3d83b0..f531be079 100644 --- a/lib/vagrant/action/warden.rb +++ b/lib/vagrant/action/warden.rb @@ -1,4 +1,7 @@ require "log4r" +require 'vagrant/util/experimental' +require 'vagrant/action/builtin/before_trigger' +require 'vagrant/action/builtin/after_trigger' module Vagrant module Action @@ -16,8 +19,21 @@ module Vagrant attr_accessor :actions, :stack def initialize(actions, env) + if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") + if env[:trigger_env] + @env = env[:trigger_env] + else + @env = env[:env] + end + + machine = env[:machine] + machine_name = machine.name if machine + ui = Vagrant::UI::Prefixed.new(@env.ui, "vargant") + @triggers = Vagrant::Plugin::V2::Trigger.new(@env, @env.vagrantfile.config.trigger, machine, ui) + end + @stack = [] - @actions = actions.map { |m| finalize_action(m, env) } + @actions = actions.map { |m| finalize_action(m, env) }.flatten @logger = Log4r::Logger.new("vagrant::action::warden") @last_error = nil end @@ -87,7 +103,17 @@ module Vagrant if klass.is_a?(Class) # A action klass which is to be instantiated with the # app, env, and any arguments given - klass.new(self, env, *args, &block) + + # We wrap the action class in two Trigger method calls so that + # action triggers can fire before and after each given action in the stack. + klass_name = klass.name + [Vagrant::Action::Builtin::BeforeTriggerAction.new(self, env, + klass_name, + @triggers), + klass.new(self, env, *args, &block), + Vagrant::Action::Builtin::AfterTriggerAction.new(self, env, + klass_name, + @triggers)] elsif klass.respond_to?(:call) # Make it a lambda which calls the item then forwards # up the chain diff --git a/lib/vagrant/cli.rb b/lib/vagrant/cli.rb index ed5154b46..187e3df2e 100644 --- a/lib/vagrant/cli.rb +++ b/lib/vagrant/cli.rb @@ -1,6 +1,8 @@ require 'log4r' require 'optparse' +require 'vagrant/util/experimental' + module Vagrant # Manages the command line interface to Vagrant. class CLI < Vagrant.plugin("2", :command) @@ -11,6 +13,11 @@ module Vagrant @logger = Log4r::Logger.new("vagrant::cli") @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) + if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") + ui = Vagrant::UI::Prefixed.new(env.ui, "vargant") + @triggers = Vagrant::Plugin::V2::Trigger.new(env, env.vagrantfile.config.trigger, nil, ui) + end + Util::CheckpointClient.instance.setup(env).check @logger.info("CLI: #{@main_args.inspect} #{@sub_command.inspect} #{@sub_args.inspect}") end @@ -55,7 +62,9 @@ module Vagrant # Initialize and execute the command class, returning the exit status. result = 0 begin + @triggers.fire_triggers(@sub_command.to_sym, :before, nil, :command) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") result = command_class.new(@sub_args, @env).execute + @triggers.fire_triggers(@sub_command.to_sym, :after, nil, :command) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") rescue Interrupt @env.ui.info(I18n.t("vagrant.cli_interrupt")) result = 1 diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index acda45278..de3b88066 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -210,7 +210,8 @@ module Vagrant home_path: home_path, root_path: root_path, tmp_path: tmp_path, - ui: @ui + ui: @ui, + env: self } end end diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index fe146dc46..44bed4d4c 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -804,6 +804,10 @@ module Vagrant error_key(:triggers_bad_exit_codes) end + class TriggersGuestNotExist < VagrantError + error_key(:triggers_guest_not_exist) + end + class TriggersGuestNotRunning < VagrantError error_key(:triggers_guest_not_running) end diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index c4fc253d1..993de2b50 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -110,7 +110,7 @@ module Vagrant @ui = Vagrant::UI::Prefixed.new(@env.ui, @name) @ui_mutex = Mutex.new @state_mutex = Mutex.new - @triggers = Vagrant::Plugin::V2::Trigger.new(@env, @config.trigger, self) + @triggers = Vagrant::Plugin::V2::Trigger.new(@env, @config.trigger, self, @ui) # Read the ID, which is usually in local storage @id = nil @@ -160,10 +160,7 @@ module Vagrant # as extra data set on the environment hash for the middleware # runner. def action(name, opts=nil) - plugins = Vagrant::Plugin::Manager.instance.installed_plugins - if !plugins.keys.include?("vagrant-triggers") - @triggers.fire_triggers(name, :before, @name.to_s) - end + @triggers.fire_triggers(name, :before, @name.to_s, :action) @logger.info("Calling action: #{name} on provider #{@provider}") @@ -175,6 +172,10 @@ module Vagrant # Extra env keys are the remaining opts extra_env = opts.dup + # An environment is required for triggers to function properly. This is + # passed in specifically for the `#Action::Warden` class triggers. We call it + # `:trigger_env` instead of `env` in case it collides with an existing environment + extra_env[:trigger_env] = @env check_cwd # Warns the UI if the machine was last used on a different dir @@ -210,9 +211,7 @@ module Vagrant action_result end - if !plugins.keys.include?("vagrant-triggers") - @triggers.fire_triggers(name, :after, @name.to_s) - end + @triggers.fire_triggers(name, :after, @name.to_s, :action) # preserve returning environment after machine action runs return return_env rescue Errors::EnvironmentLockedError diff --git a/lib/vagrant/plugin/v2/trigger.rb b/lib/vagrant/plugin/v2/trigger.rb index 055c472b0..93f371d83 100644 --- a/lib/vagrant/plugin/v2/trigger.rb +++ b/lib/vagrant/plugin/v2/trigger.rb @@ -20,20 +20,35 @@ module Vagrant # @param [Vagrant::Environment] env Vagrant environment # @param [Kernel_V2::TriggerConfig] config Trigger configuration # @param [Vagrant::Machine] machine Active Machine - def initialize(env, config, machine) + # @param [Vagrant::UI] ui Class for printing messages to user + def initialize(env, config, machine, ui) @env = env @config = config @machine = machine + @ui = ui @logger = Log4r::Logger.new("vagrant::trigger::#{self.class.to_s.downcase}") end - # Fires all triggers, if any are defined for the action and guest + # Fires all triggers, if any are defined for the action and guest. Returns early + # and logs a warning if the community plugin `vagrant-triggers` is installed # # @param [Symbol] action Vagrant command to fire trigger on # @param [Symbol] stage :before or :after # @param [String] guest_name The guest that invoked firing the triggers - def fire_triggers(action, stage, guest_name) + def fire_triggers(action, stage, guest_name, type) + if community_plugin_detected? + @logger.warn("Community plugin `vagrant-triggers detected, so core triggers will not fire") + return + end + + if !action + @logger.warn("Action given is nil, no triggers will fire") + return + else + action = action.to_sym + end + # get all triggers matching action triggers = [] if stage == :before @@ -51,27 +66,41 @@ module Vagrant guest_name: guest_name end - triggers = filter_triggers(triggers, guest_name) + triggers = filter_triggers(triggers, guest_name, type) if !triggers.empty? @logger.info("Firing trigger for action #{action} on guest #{guest_name}") - @machine.ui.info(I18n.t("vagrant.trigger.start", stage: stage, action: action)) + @ui.info(I18n.t("vagrant.trigger.start", type: type, stage: stage, action: action)) fire(triggers, guest_name) end end protected + #------------------------------------------------------------------- # Internal methods, don't call these. #------------------------------------------------------------------- + # Looks up if the community plugin `vagrant-triggers` is installed + # and also caches the result + # + # @return [Boolean] + def community_plugin_detected? + if !defined?(@_triggers_enabled) + plugins = Vagrant::Plugin::Manager.instance.installed_plugins + @_triggers_enabled = plugins.keys.include?("vagrant-triggers") + end + @_triggers_enabled + end + # Filters triggers to be fired based on configured restraints # # @param [Array] triggers An array of triggers to be filtered # @param [String] guest_name The name of the current guest + # @param [Symbol] type The type of trigger (:command or :type) # @return [Array] The filtered array of triggers - def filter_triggers(triggers, guest_name) + def filter_triggers(triggers, guest_name, type) # look for only_on trigger constraint and if it doesn't match guest # name, throw it away also be sure to preserve order filter = triggers.dup @@ -91,6 +120,10 @@ module Vagrant index = triggers.index(trigger) unless match == true end + if trigger.type != type + index = triggers.index(trigger) + end + if index @logger.debug("Trigger #{trigger.id} will be ignored for #{guest_name}") triggers.delete_at(index) @@ -110,10 +143,10 @@ module Vagrant @logger.debug("Running trigger #{trigger.id}...") if trigger.name - @machine.ui.info(I18n.t("vagrant.trigger.fire_with_name", + @ui.info(I18n.t("vagrant.trigger.fire_with_name", name: trigger.name)) else - @machine.ui.info(I18n.t("vagrant.trigger.fire")) + @ui.info(I18n.t("vagrant.trigger.fire")) end if trigger.info @@ -146,14 +179,14 @@ module Vagrant # # @param [String] message The string to be printed def info(message) - @machine.ui.info(message) + @ui.info(message) end # Prints the given message at warn level for a trigger # # @param [String] message The string to be printed def warn(message) - @machine.ui.warn(message) + @ui.warn(message) end # Runs a script on a guest @@ -167,14 +200,14 @@ module Vagrant cmd = Shellwords.split(config.inline) end - @machine.ui.detail(I18n.t("vagrant.trigger.run.inline", command: config.inline)) + @ui.detail(I18n.t("vagrant.trigger.run.inline", command: config.inline)) else cmd = File.expand_path(config.path, @env.root_path).shellescape args = Array(config.args) cmd << " #{args.join(' ')}" if !args.empty? cmd = Shellwords.split(cmd) - @machine.ui.detail(I18n.t("vagrant.trigger.run.script", path: config.path)) + @ui.detail(I18n.t("vagrant.trigger.run.script", path: config.path)) end # Pick an execution method to run the script or inline string with @@ -199,22 +232,22 @@ module Vagrant options[:color] = :red if !config.keep_color end - @machine.ui.detail(data, options) + @ui.detail(data, options) end if !exit_codes.include?(result.exit_code) raise Errors::TriggersBadExitCodes, code: result.exit_code end rescue => e - @machine.ui.error(I18n.t("vagrant.errors.triggers_run_fail")) - @machine.ui.error(e.message) + @ui.error(I18n.t("vagrant.errors.triggers_run_fail")) + @ui.error(e.message) if on_error == :halt @logger.debug("Trigger run encountered an error. Halting on error...") raise e else @logger.debug("Trigger run encountered an error. Continuing on anyway...") - @machine.ui.warn(I18n.t("vagrant.trigger.on_error_continue")) + @ui.warn(I18n.t("vagrant.trigger.on_error_continue")) end end end @@ -223,7 +256,16 @@ module Vagrant # # @param [ShellProvisioner/Config] config A Shell provisioner config def run_remote(config, on_error, exit_codes) - unless @machine.state.id == :running + if !@machine + # machine doesn't even exist. + if on_error == :halt + raise Errors::TriggersGuestNotExist + else + @ui.warn(I18n.t("vagrant.errors.triggers_guest_not_exist")) + @ui.warn(I18n.t("vagrant.trigger.on_error_continue")) + return + end + elsif @machine.state.id != :running if on_error == :halt raise Errors::TriggersGuestNotRunning, machine_name: @machine.name, @@ -258,7 +300,7 @@ module Vagrant # # @param [Integer] code Code to exit Vagrant on def trigger_abort(exit_code) - @machine.ui.warn(I18n.t("vagrant.trigger.abort")) + @ui.warn(I18n.t("vagrant.trigger.abort")) exit(exit_code) end diff --git a/plugins/kernel_v2/config/trigger.rb b/plugins/kernel_v2/config/trigger.rb index 546901aee..d3f378b72 100644 --- a/plugins/kernel_v2/config/trigger.rb +++ b/plugins/kernel_v2/config/trigger.rb @@ -44,18 +44,22 @@ module VagrantPlugins command.flatten! blk = block - if !block_given? && command.last.is_a?(Hash) - # We were given a hash rather than a block, - # so the last element should be the "config block" - # and the rest are commands for the trigger - blk = command.pop + if command.last.is_a?(Hash) + if block_given? + extra_cfg = command.pop + else + # We were given a hash rather than a block, + # so the last element should be the "config block" + # and the rest are commands for the trigger + blk = command.pop + end elsif !block_given? raise Vagrant::Errors::TriggersNoBlockGiven, command: command end command.each do |cmd| - trigger = create_trigger(cmd, blk) + trigger = create_trigger(cmd, blk, extra_cfg) @_before_triggers << trigger end end @@ -69,18 +73,22 @@ module VagrantPlugins command.flatten! blk = block - if !block_given? && command.last.is_a?(Hash) - # We were given a hash rather than a block, - # so the last element should be the "config block" - # and the rest are commands for the trigger - blk = command.pop + if command.last.is_a?(Hash) + if block_given? + extra_cfg = command.pop + else + # We were given a hash rather than a block, + # so the last element should be the "config block" + # and the rest are commands for the trigger + blk = command.pop + end elsif !block_given? raise Vagrant::Errors::TriggersNoBlockGiven, command: command end command.each do |cmd| - trigger = create_trigger(cmd, blk) + trigger = create_trigger(cmd, blk, extra_cfg) @_after_triggers << trigger end end @@ -95,13 +103,15 @@ module VagrantPlugins # # @param [Symbol] command Vagrant command to create trigger on # @param [Block] block The defined config block + # @param [Hash] extra_cfg Extra configurations for a block defined trigger (Optional) # @return [VagrantConfigTrigger] - def create_trigger(command, block) + def create_trigger(command, block, extra_cfg=nil) trigger = VagrantConfigTrigger.new(command) if block.is_a?(Hash) trigger.set_options(block) else block.call(trigger, VagrantConfigTrigger) + trigger.set_options(extra_cfg) if extra_cfg end return trigger end diff --git a/plugins/kernel_v2/config/vm_trigger.rb b/plugins/kernel_v2/config/vm_trigger.rb index aa27da955..644eabe8a 100644 --- a/plugins/kernel_v2/config/vm_trigger.rb +++ b/plugins/kernel_v2/config/vm_trigger.rb @@ -9,6 +9,7 @@ module VagrantPlugins # Defaults DEFAULT_ON_ERROR = :halt DEFAULT_EXIT_CODE = 0 + VALID_TRIGGER_TYPES = [:command, :action, :hook].freeze #------------------------------------------------------------------- # Config class for a given Trigger @@ -89,6 +90,12 @@ module VagrantPlugins # @return [Proc] attr_accessor :ruby + # The type of trigger, which defines where it will fire. If not defined, + # the option will default to `:action` + # + # @return [Symbol] + attr_accessor :type + def initialize(command) @logger = Log4r::Logger.new("vagrant::config::vm::trigger::config") @@ -103,6 +110,7 @@ module VagrantPlugins @exit_codes = UNSET_VALUE @abort = UNSET_VALUE @ruby = UNSET_VALUE + @type = UNSET_VALUE # Internal options @id = SecureRandom.uuid @@ -135,6 +143,7 @@ module VagrantPlugins @only_on = nil if @only_on == UNSET_VALUE @exit_codes = DEFAULT_EXIT_CODE if @exit_codes == UNSET_VALUE @abort = nil if @abort == UNSET_VALUE + @type = :action if @type == UNSET_VALUE @ruby_block = nil if @ruby_block == UNSET_VALUE @ruby = nil if @ruby == UNSET_VALUE @@ -187,20 +196,33 @@ module VagrantPlugins if @abort == true @abort = 1 end + + if @type + @type = @type.to_sym + end end # @return [Array] array of strings of error messages from config option validation def validate(machine) errors = _detected_errors - commands = [] - Vagrant.plugin("2").manager.commands.each do |key,data| - commands.push(key) + if @type && !VALID_TRIGGER_TYPES.include?(@type) + errors << I18n.t("vagrant.config.triggers.bad_trigger_type", + type: @type, + trigger: @command, + types: VALID_TRIGGER_TYPES.join(', ')) end - if !commands.include?(@command) && @command != :all - machine.ui.warn(I18n.t("vagrant.config.triggers.bad_command_warning", - cmd: @command)) + if @type == :command || !@type + commands = [] + Vagrant.plugin("2").manager.commands.each do |key,data| + commands.push(key) + end + + if !commands.include?(@command) && @command != :all + machine.ui.warn(I18n.t("vagrant.config.triggers.bad_command_warning", + cmd: @command)) + end end if @run diff --git a/templates/locales/en.yml b/templates/locales/en.yml index fe2a80115..6fcddc342 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -296,7 +296,7 @@ en: abort: |- Vagrant has been configured to abort. Terminating now... start: |- - Running triggers %{stage} %{action} ... + Running %{type} triggers %{stage} %{action} ... fire_with_name: |- Running trigger: %{name}... fire: |- @@ -1502,6 +1502,8 @@ en: Trigger run failed triggers_guest_not_running: |- Could not run remote script on %{machine_name} because its state is %{state} + triggers_guest_not_exist: |- + Could not run remote script on guest because it does not exist. triggers_bad_exit_codes: |- A script exited with an unacceptable exit code %{code}. triggers_no_block_given: |- @@ -1826,6 +1828,8 @@ en: values are exactly the same, only the name of the option has changed. ssh_config_missing: "`config` file must exist: %{path}" triggers: + bad_trigger_type: |- + The type '%{type}' defined for trigger '%{trigger}' is not valid. Must be one of the following types: '%{types}' bad_command_warning: |- The command '%{cmd}' was not found for this trigger. name_bad_type: |- diff --git a/test/unit/plugins/kernel_v2/config/vm_trigger_test.rb b/test/unit/plugins/kernel_v2/config/vm_trigger_test.rb index e6d69384a..6735e219f 100644 --- a/test/unit/plugins/kernel_v2/config/vm_trigger_test.rb +++ b/test/unit/plugins/kernel_v2/config/vm_trigger_test.rb @@ -71,6 +71,7 @@ describe VagrantPlugins::Kernel_V2::VagrantConfigTrigger do cfg.only_on = :guest cfg.ignore = "up" cfg.abort = true + cfg.type = "action" cfg.ruby do var = 1+1 end @@ -112,6 +113,11 @@ describe VagrantPlugins::Kernel_V2::VagrantConfigTrigger do expect(cfg.abort).to eq(1) end + + it "converts types to symbols" do + cfg.finalize! + expect(cfg.type).to eq(:action) + end end describe "defining a basic trigger config" do diff --git a/test/unit/vagrant/action/builder_test.rb b/test/unit/vagrant/action/builder_test.rb index c4f67a118..94f9d846a 100644 --- a/test/unit/vagrant/action/builder_test.rb +++ b/test/unit/vagrant/action/builder_test.rb @@ -22,6 +22,10 @@ describe Vagrant::Action::Builder do @app = app end + def self.name + "TestAction" + end + define_method(:call) do |env| env[:data] << "#{data}_in" @app.call(env) @@ -147,6 +151,10 @@ describe Vagrant::Action::Builder do @app = app end + def self.name + "TestAction" + end + define_method(:call) do |env| env[:data] << "#{letter}1" @app.call(env) @@ -266,6 +274,10 @@ describe Vagrant::Action::Builder do @app = app end + def self.name + "TestAction" + end + define_method(:call) do |env| inner = described_klass.new inner.use wrapper_proc[2] diff --git a/test/unit/vagrant/action/builtin/call_test.rb b/test/unit/vagrant/action/builtin/call_test.rb index 43e910c25..b4779032c 100644 --- a/test/unit/vagrant/action/builtin/call_test.rb +++ b/test/unit/vagrant/action/builtin/call_test.rb @@ -10,6 +10,10 @@ describe Vagrant::Action::Builtin::Call do @app = app end + def self.name + "TestAction" + end + define_method(:call) do |env| env[:data] << "#{data}_in" @app.call(env) @@ -103,6 +107,10 @@ describe Vagrant::Action::Builtin::Call do env[:arg] = arg end + def self.name + "TestAction" + end + def call(env); end end @@ -126,6 +134,10 @@ describe Vagrant::Action::Builtin::Call do @env = env end + def self.name + "TestAction" + end + def call(env) @app.call(env) end @@ -137,6 +149,10 @@ describe Vagrant::Action::Builtin::Call do super end + def self.name + "TestAction" + end + def recover(env) env[:steps] << :recover_A end @@ -148,6 +164,10 @@ describe Vagrant::Action::Builtin::Call do super end + def self.name + "TestAction" + end + def recover(env) env[:steps] << :recover_B end diff --git a/test/unit/vagrant/action/runner_test.rb b/test/unit/vagrant/action/runner_test.rb index bddd4a6ff..7ffc1d14e 100644 --- a/test/unit/vagrant/action/runner_test.rb +++ b/test/unit/vagrant/action/runner_test.rb @@ -1,7 +1,7 @@ require File.expand_path("../../../base", __FILE__) describe Vagrant::Action::Runner do - let(:instance) { described_class.new } + let(:instance) { described_class.new(action_name: "test") } it "should raise an error if an invalid callable is given" do expect { instance.run(7) }.to raise_error(ArgumentError, /must be a callable/) @@ -18,11 +18,29 @@ describe Vagrant::Action::Runner do raise Exception, "BANG" end end + callable = klass.new.method(:action) expect { instance.run(callable) }.to raise_error(Exception, "BANG") end it "should be able to use a Class as a callable" do + callable = Class.new do + def initialize(app, env) + end + + def self.name + "TestAction" + end + + def call(env) + raise Exception, "BOOM" + end + end + + expect { instance.run(callable) }.to raise_error(Exception, "BOOM") + end + + it "should be able to use a Class as a callable with no name attribute" do callable = Class.new do def initialize(app, env) end @@ -63,7 +81,7 @@ describe Vagrant::Action::Runner do result = env["data"] end - instance = described_class.new("data" => "bar") + instance = described_class.new("data" => "bar", action_name: "test") instance.run(callable) expect(result).to eq("bar") end @@ -74,7 +92,7 @@ describe Vagrant::Action::Runner do result = env["data"] end - instance = described_class.new { { "data" => "bar" } } + instance = described_class.new { { "data" => "bar", action_name: "test" } } instance.run(callable) expect(result).to eq("bar") end diff --git a/test/unit/vagrant/cli_test.rb b/test/unit/vagrant/cli_test.rb index 5d079278f..db7e7e9bd 100644 --- a/test/unit/vagrant/cli_test.rb +++ b/test/unit/vagrant/cli_test.rb @@ -28,6 +28,8 @@ describe Vagrant::CLI do end describe "#execute" do + let(:triggers) { double("triggers") } + it "invokes help and exits with 1 if invalid command" do subject = described_class.new(["i-dont-exist"], env) expect(subject).to receive(:help).once @@ -54,6 +56,37 @@ describe Vagrant::CLI do expect(checkpoint).to receive(:display) described_class.new(["destroy"], env).execute end + + it "fires triggers, if enabled" do + allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). + with("typed_triggers").and_return("true") + allow(triggers).to receive(:fire_triggers) + + commands[:destroy] = [command_lambda("destroy", 42), {}] + + allow(Vagrant::Plugin::V2::Trigger).to receive(:new).and_return(triggers) + + subject = described_class.new(["destroy"], env) + + expect(triggers).to receive(:fire_triggers).twice + + expect(subject).not_to receive(:help) + expect(subject.execute).to eql(42) + end + + it "does not fire triggers if disabled" do + allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). + with("typed_triggers").and_return("false") + + commands[:destroy] = [command_lambda("destroy", 42), {}] + + subject = described_class.new(["destroy"], env) + + expect(triggers).not_to receive(:fire_triggers) + + expect(subject).not_to receive(:help) + expect(subject.execute).to eql(42) + end end describe "#help" do diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index e956636b6..c542402c1 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -411,38 +411,6 @@ describe Vagrant::Machine do expect(subject.ui).to_not have_received(:warn) end end - - context "with the vagrant-triggers community plugin" do - it "should not call the internal trigger functions if installed" do - action_name = :destroy - callable = lambda { |_env| } - - allow(provider).to receive(:action).with(action_name).and_return(callable) - - # 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) - instance.action(action_name) - end - - it "should call the internal trigger functions if not installed" do - action_name = :destroy - callable = lambda { |_env| } - - allow(provider).to receive(:action).with(action_name).and_return(callable) - allow(Vagrant::Plugin::Manager.instance).to receive(:installed_plugins) - .and_return({}) - - expect(instance.instance_variable_get(:@triggers)).to receive(:fire_triggers).twice - instance.action(action_name) - end - end end describe "#action_raw" do diff --git a/test/unit/vagrant/plugin/v2/trigger_test.rb b/test/unit/vagrant/plugin/v2/trigger_test.rb index dd4ce6b90..512bf360b 100644 --- a/test/unit/vagrant/plugin/v2/trigger_test.rb +++ b/test/unit/vagrant/plugin/v2/trigger_test.rb @@ -17,9 +17,10 @@ describe Vagrant::Plugin::V2::Trigger do allow(m).to receive(:state).and_return(state) end end + let(:ui) { Vagrant::UI::Silent.new } let(:env) { { machine: machine, - ui: Vagrant::UI::Silent.new, + ui: ui, } } let(:triggers) { VagrantPlugins::Kernel_V2::TriggerConfig.new } @@ -36,19 +37,33 @@ describe Vagrant::Plugin::V2::Trigger do end - let(:subject) { described_class.new(env, triggers, machine) } + let(:subject) { described_class.new(env, triggers, machine, ui) } context "#fire_triggers" do it "raises an error if an inproper stage is given" do - expect{ subject.fire_triggers(:up, :not_real, "guest") }. + expect{ subject.fire_triggers(:up, :not_real, "guest", :action) }. to raise_error(Vagrant::Errors::TriggersNoStageGiven) end + + it "does not fire triggers if community plugin is detected" do + allow(subject).to receive(:community_plugin_detected?).and_return(true) + + expect(subject).not_to receive(:fire) + subject.fire_triggers(:up, :before, "guest", :action) + end + + it "does fire triggers if community plugin is not detected" do + allow(subject).to receive(:community_plugin_detected?).and_return(false) + + expect(subject).to receive(:fire) + subject.fire_triggers(:up, :before, "guest", :action) + end end context "#filter_triggers" do it "returns all triggers if no constraints" do before_triggers = triggers.before_triggers - filtered_triggers = subject.send(:filter_triggers, before_triggers, "guest") + filtered_triggers = subject.send(:filter_triggers, before_triggers, "guest", :action) expect(filtered_triggers).to eq(before_triggers) end @@ -59,7 +74,7 @@ describe Vagrant::Plugin::V2::Trigger do after_triggers = triggers.after_triggers expect(after_triggers.size).to eq(3) - subject.send(:filter_triggers, after_triggers, "ubuntu") + subject.send(:filter_triggers, after_triggers, "ubuntu", :action) expect(after_triggers.size).to eq(2) end @@ -70,7 +85,7 @@ describe Vagrant::Plugin::V2::Trigger do after_triggers = triggers.after_triggers expect(after_triggers.size).to eq(3) - subject.send(:filter_triggers, after_triggers, "ubuntu-guest") + subject.send(:filter_triggers, after_triggers, "ubuntu-guest", :action) expect(after_triggers.size).to eq(3) end @@ -81,7 +96,7 @@ describe Vagrant::Plugin::V2::Trigger do after_triggers = triggers.after_triggers expect(after_triggers.size).to eq(3) - subject.send(:filter_triggers, after_triggers, "ubuntu-guest") + subject.send(:filter_triggers, after_triggers, "ubuntu-guest", :action) expect(after_triggers.size).to eq(3) end end @@ -101,7 +116,7 @@ describe Vagrant::Plugin::V2::Trigger do it "prints messages at INFO" do output = "" - allow(machine.ui).to receive(:info) do |data| + allow(ui).to receive(:info) do |data| output << data end @@ -115,7 +130,7 @@ describe Vagrant::Plugin::V2::Trigger do it "prints messages at WARN" do output = "" - allow(machine.ui).to receive(:warn) do |data| + allow(ui).to receive(:warn) do |data| output << data end @@ -304,6 +319,29 @@ describe Vagrant::Plugin::V2::Trigger do trigger_run.finalize! end + context "with no machine existing" do + let(:machine) { nil } + + it "raises an error and halts if guest does not exist" do + trigger = trigger_run.after_triggers.first + shell_config = trigger.run_remote + on_error = trigger.on_error + exit_codes = trigger.exit_codes + + expect { subject.send(:run_remote, shell_config, on_error, exit_codes) }. + to raise_error(Vagrant::Errors::TriggersGuestNotExist) + end + + it "continues on if guest does not exist but is configured to continue on error" do + trigger = trigger_run.before_triggers.first + shell_config = trigger.run_remote + on_error = trigger.on_error + exit_codes = trigger.exit_codes + + subject.send(:run_remote, shell_config, on_error, exit_codes) + end + end + it "raises an error and halts if guest is not running" do allow(machine.state).to receive(:id).and_return(:not_running) diff --git a/website/source/docs/experimental/index.html.md b/website/source/docs/experimental/index.html.md new file mode 100644 index 000000000..cb3d7818c --- /dev/null +++ b/website/source/docs/experimental/index.html.md @@ -0,0 +1,49 @@ +--- +layout: "docs" +page_title: "Vagrant Experimental Feature Flag" +sidebar_current: "experimental" +description: |- + Introduction to Vagrants Experimental Feature Flag +--- + +# Experimental Feature Flag + +Some features that aren't ready for release can be enabled through this feature +flag. There are a couple of different ways of going about enabling these features. +It is also worth noting that Vagrant will not validate the existance of a feature +flag. + +For example if you are on Linux or Mac, and you wish to enable every single experimental feature, you can set the flag +to "on" by setting it to `1`: + +```shell +export VAGRANT_EXPERIMENTAL="1" +``` + +You can also enable some or many features if there are specific ones you would like, +but don't want every single feature enabled: + +```shell +# Only enables feature_one +export VAGRANT_EXPERIMENTAL="feature_one" +``` + +```shell +# Enables both feature_one and feature_two +export VAGRANT_EXPERIMENTAL="feature_one,feature_two" +``` + +## Valid experimental features + +
+ Advanced topic! This is an advanced topic for use only if + you want to use new Vagrant features. If you are just getting + started with Vagrant, you may safely skip this section. +
+ +This is a list of all the valid experimental features that Vagrant recognizes: + +### `typed_triggers` + +Enabling this feature allows triggers to recognize and execute `:type` triggers. +More information about how these should be used can be found on the [trigger documentation page](/docs/triggers/configuration.html#trigger-types) diff --git a/website/source/docs/triggers/configuration.html.md b/website/source/docs/triggers/configuration.html.md index a3569ef08..62e62ae1d 100644 --- a/website/source/docs/triggers/configuration.html.md +++ b/website/source/docs/triggers/configuration.html.md @@ -10,7 +10,28 @@ description: |- Vagrant Triggers has a few options to define trigger behavior. -## Options +## Execution Order + +The trigger config block takes two different operations that determine when a trigger +should fire: + +* `before` +* `after` + +These define _how_ the trigger behaves and when it should fire off during +the Vagrant life cycle. A simple example of a _before_ operation could look like: + +```ruby +config.trigger.before :up do |t| + t.info = "Bringing up your Vagrant guest machine!" +end +``` + +Triggers can also be used with [_commands_](#commands), [_actions_](#actions), or [_hooks_](#hooks). +By default triggers will be defined to run before or after a Vagrant guest. For more +detailed examples of how to use triggers, check out the [usage section](/docs/triggers/usage.html). + +## Trigger Options The trigger class takes various options. @@ -58,3 +79,117 @@ The trigger class takes various options. * `exit_codes` (integer, array) - A set of acceptable exit codes to continue on. Defaults to `0` if option is absent. For now only valid with the `run` option. * `abort` (integer,boolean) - An option that will exit the running Vagrant process once the trigger fires. If set to `true`, Vagrant will use exit code 1. Otherwise, an integer can be provided and Vagrant will it as its exit code when aborting. + +## Trigger Types + +Optionally, it is possible to define a trigger that executes around Vagrant commands, +hooks, and actions. + +
+ Warning! This feature is still experimental and may break or + change in between releases. Use at your own risk. + + This feature currently reqiures the experimental flag to be used. To explicitly enable this feature, you can set the experimental flag to: + + ``` + VAGRANT_EXPERIMENTAL="typed_triggers" + ``` + + Please note that `VAGRANT_EXPERIMENTAL` is an environment variable. For more + information about this flag visit the [Experimental docs page](/docs/experimental/) + for more info. Without this flag enabled, triggers with the `:type` option + will be ignored. +
+ + +A trigger can be one of three types: + +* `type` (symbol) - Optional + - `:action` - Action triggers run before or after a Vagrant action + - `:command` - Command triggers run before or after a Vagrant command + - `:hook` - Action hook triggers run before or after a Vagrant hook + +These types determine when and where a defined trigger will execute. + +```ruby +config.trigger.after :destroy, type: :command do |t| + t.warn = "Destroy command completed" +end +``` + +#### Quick Note + +Triggers _without_ the type option will run before or after a Vagrant guest. + +Older Vagrant versions will unfortunetly not be able to properly parse the new +`:type` option. If you are worried about older clients failing to parse your Vagrantfile, +you can guard the new trigger based on the version of Vagrant: + +```ruby +if Vagrant.version?(">= 2.3.0") + config.trigger.before :status, type: :command do |t| + t.info = "before action!!!!!!!" + end +end +``` + +### Commands + +Command typed triggers can be defined for any valid Vagrant command. They will always +run before or after the command. + +The difference between this and the default behavior is that these triggers are +not attached to any specific guest, and will always run before or after the given +command. A simple example might be running a trigger before the up command to give +a simple message to the user: + +```ruby +config.trigger.before :up, type: :command do |t| + t.info = "Before command!" +end +``` + +For a more detailed example, please check out the [examples](/docs/triggers/usage.html#commands) +page for more. + +### Hooks + +
+ Advanced topic! This is an advanced topic for use only if + you want to execute triggers around Vagrant hooks. If you are just getting + started with Vagrant and triggers, you may safely skip this section. +
+ +Hook typed triggers can be defined for any valid Vagrant action hook that is defined. + +A simple example would be running a trigger on a given hook called `action_hook_name`. + +```ruby +config.trigger.after :action_hook_name, type: :hook do |t| + t.info = "After action hook!" +end +``` + +For a more detailed example, please check out the [examples](/docs/triggers/usage.html#hooks) +page for more. + +### Actions + +
+ Advanced topic! This is an advanced topic for use only if + you want to execute triggers around Vagrant actions. If you are just getting + started with Vagrant and triggers, you may safely skip this section. +
+ +Action typed triggers can be defined for any valid Vagrant action class. Actions +in this case refer to the Vagrant class `#Action`, which is used internally to +Vagrant and in every Vagrant plugin. + +```ruby +config.trigger.before :"Action::Class::Name", type: :action do |t| + t.info = "Before action class! +end +``` + +For a more detailed example, please check out the [examples](/docs/triggers/usage.html#actions) +page for more. diff --git a/website/source/docs/triggers/usage.html.md b/website/source/docs/triggers/usage.html.md index a5ec536b8..85934b326 100644 --- a/website/source/docs/triggers/usage.html.md +++ b/website/source/docs/triggers/usage.html.md @@ -131,3 +131,70 @@ Vagrant.configure("2") do |config| end end ``` + +### Typed Triggers + +Below are some basic examples of using `:type` triggers. They cover commands, hooks, +and actions. + +It is important to note that while `command` triggers will be a fairly common use case, +both `action` and `hook` triggers are more complicated and are a more advanced use case. + +#### Commands + +The most common use case for typed triggers are with `command`. These kinds of +triggers allow you to run something before or after a subcommand in Vagrant. + +```ruby +config.trigger.after :status, type: :command do |t| + t.info = "Showing status of all VMs!" +end +``` + +Because they are specifically for subcommands, they do not work with any guest +operations like `run_remote` or if you define the trigger as a guest trigger. + +#### Hooks + +Below is an example of a Vagrant trigger that runs before and after each defined +provisioner: + +```ruby +config.trigger.before :provisioner_run, type: :hook do |t| + t.info = "Before the provision!" +end + +config.vm.provision "file", source: "scripts/script.sh", destination: "/test/script.sh" + +config.vm.provision "shell", inline: <<-SHELL +echo "Provision the guest!" +SHELL + +``` + +Notice how this trigger runs before _each_ provisioner defined for the guest: + +```shell +==> guest: Running provisioner: Sandbox (file)... +==> vargant: Running hook triggers before provisioner_run ... +==> vargant: Running trigger... +==> vargant: Before the provision! + guest: /home/hashicorp/vagrant-sandbox/scripts/script.sh => /home/vagrant/test/script.sh +==> guest: Running provisioner: shell... +==> vargant: Running hook triggers before provisioner_run ... +==> vargant: Running trigger... +==> vargant: Before the provision! + guest: Running: inline script + guest: Provision the guest! +``` +#### Actions + +With action typed triggers, you can fire off triggers before or after certain +Action classes. A simple example of this might be warning the user when Vagrant +invokes the `GracefulHalt` action. + +```ruby +config.trigger.before :"Vagrant::Action::Builtin::GracefulHalt", type: :action do |t| + t.warn = "Vagrant is halting your guest..." +end +``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index e4ea9aa30..462b5c509 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -216,6 +216,10 @@ + > + Experimental + + > Other