Merge pull request #10615 from briancain/introduce-typed-triggers

Introduce :type option for Vagrant triggers
This commit is contained in:
Brian Cain 2019-02-12 09:16:01 -08:00 committed by GitHub
commit 0bc0bdd616
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 640 additions and 94 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: |-

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
<div class="alert alert-warning">
<strong>Advanced topic!</strong> 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.
</div>
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)

View File

@ -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.
<div class="alert alert-warning">
<strong>Warning!</strong> 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.
</div>
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
<div class="alert alert-warning">
<strong>Advanced topic!</strong> 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.
</div>
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
<div class="alert alert-warning">
<strong>Advanced topic!</strong> 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.
</div>
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.

View File

@ -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
```

View File

@ -216,6 +216,10 @@
</ul>
</li>
<li<%= sidebar_current("experimental") %>>
<a href="/docs/experimental/">Experimental</a>
</li>
<li<%= sidebar_current("other") %>>
<a href="/docs/other/">Other</a>
<ul class="nav">