Merge pull request #11455 from chrisroberts/enhancement/triggers-insertions
Adjust how trigger actions are inserted into the stack
This commit is contained in:
commit
c5e2f36ddc
@ -16,6 +16,7 @@ module Vagrant
|
||||
autoload :CleanupDisks, "vagrant/action/builtin/cleanup_disks"
|
||||
autoload :Confirm, "vagrant/action/builtin/confirm"
|
||||
autoload :ConfigValidate, "vagrant/action/builtin/config_validate"
|
||||
autoload :Delayed, "vagrant/action/builtin/delayed"
|
||||
autoload :DestroyConfirm, "vagrant/action/builtin/destroy_confirm"
|
||||
autoload :Disk, "vagrant/action/builtin/disk"
|
||||
autoload :EnvSet, "vagrant/action/builtin/env_set"
|
||||
@ -35,6 +36,7 @@ module Vagrant
|
||||
autoload :SSHRun, "vagrant/action/builtin/ssh_run"
|
||||
autoload :SyncedFolders, "vagrant/action/builtin/synced_folders"
|
||||
autoload :SyncedFolderCleanup, "vagrant/action/builtin/synced_folder_cleanup"
|
||||
autoload :Trigger, "vagrant/action/builtin/trigger"
|
||||
autoload :WaitForCommunicator, "vagrant/action/builtin/wait_for_communicator"
|
||||
end
|
||||
|
||||
|
||||
@ -122,6 +122,7 @@ module Vagrant
|
||||
# @return [Integer]
|
||||
def index(object)
|
||||
stack.each_with_index do |item, i|
|
||||
return i if item == object
|
||||
return i if item[0] == object
|
||||
return i if item[0].respond_to?(:name) && item[0].name == object
|
||||
end
|
||||
@ -132,42 +133,162 @@ module Vagrant
|
||||
# Converts the builder stack to a runnable action sequence.
|
||||
#
|
||||
# @param [Hash] env The action environment hash
|
||||
# @return [Object] A callable object
|
||||
# @return [Warden] A callable object
|
||||
def to_app(env)
|
||||
app_stack = nil
|
||||
# Start with a duplicate of ourself which can
|
||||
# be modified
|
||||
builder = self.dup
|
||||
|
||||
# If we have action hooks, then we apply them
|
||||
if env[:action_hooks]
|
||||
builder = self.dup
|
||||
# Apply all dynamic modifications of the stack. This
|
||||
# will generate dynamic hooks for all actions within
|
||||
# the stack, load any triggers for action classes, and
|
||||
# apply them to the builder's stack
|
||||
builder.apply_dynamic_updates(env)
|
||||
|
||||
# These are the options to pass into hook application.
|
||||
options = {}
|
||||
|
||||
# If we already ran through once and did append/prepends,
|
||||
# then don't do it again.
|
||||
if env[:action_hooks_already_ran]
|
||||
options[:no_prepend_or_append] = true
|
||||
end
|
||||
|
||||
# Specify that we already ran, so in the future we don't repeat
|
||||
# the prepend/append hooks.
|
||||
env[:action_hooks_already_ran] = true
|
||||
|
||||
# Apply all the hooks to the new builder instance
|
||||
env[:action_hooks].each do |hook|
|
||||
hook.apply(builder, options)
|
||||
end
|
||||
|
||||
# The stack is now the result of the new builder
|
||||
app_stack = builder.stack.dup
|
||||
end
|
||||
|
||||
# If we don't have a stack then default to using our own
|
||||
app_stack ||= stack.dup
|
||||
# Now that the stack is fully expanded, apply any
|
||||
# action hooks that may be defined so they are on
|
||||
# the outermost locations of the stack
|
||||
builder.apply_action_name(env)
|
||||
|
||||
# Wrap the middleware stack with the Warden to provide a consistent
|
||||
# and predictable behavior upon exceptions.
|
||||
Warden.new(app_stack, env)
|
||||
Warden.new(builder.stack.dup, env)
|
||||
end
|
||||
|
||||
# Find any action hooks or triggers which have been defined
|
||||
# for items within the stack. Update the stack with any
|
||||
# hooks or triggers found.
|
||||
#
|
||||
# @param [Hash] env Call environment
|
||||
# @return [Builder] self
|
||||
def apply_dynamic_updates(env)
|
||||
if Vagrant::Util::Experimental.feature_enabled?("typed_triggers")
|
||||
triggers = env[:triggers]
|
||||
end
|
||||
|
||||
# Use a Hook as a convenient interface for injecting
|
||||
# any applicable trigger actions within the stack
|
||||
machine_name = env[:machine].name if env[:machine]
|
||||
|
||||
# Iterate over all items in the stack and apply new items
|
||||
# into the hook as they are found. Must be sure to dup the
|
||||
# stack here since we are modifying the stack in the loop.
|
||||
stack.dup.each do |item|
|
||||
hook = Hook.new
|
||||
|
||||
action = item.first
|
||||
next if action.is_a?(Proc)
|
||||
|
||||
# Start with adding any action triggers that may be defined
|
||||
if triggers && !triggers.find(action, :before, machine_name, :action).empty?
|
||||
hook.prepend(Vagrant::Action::Builtin::Trigger,
|
||||
action.name, triggers, :before, :action)
|
||||
end
|
||||
|
||||
if triggers && !triggers.find(action, :after, machine_name, :action).empty?
|
||||
hook.append(Vagrant::Action::Builtin::Trigger,
|
||||
action.name, triggers, :after, :action)
|
||||
end
|
||||
|
||||
# Next look for any hook triggers that may be defined against
|
||||
# the dynamically generated action class hooks
|
||||
if triggers && !triggers.find(action, :before, machine_name, :hook).empty?
|
||||
hook.prepend(Vagrant::Action::Builtin::Trigger,
|
||||
action.name, triggers, :before, :hook)
|
||||
end
|
||||
|
||||
if triggers && !triggers.find(action, :after, machine_name, :hook).empty?
|
||||
hook.append(Vagrant::Action::Builtin::Trigger,
|
||||
action.name, triggers, :after, :hook)
|
||||
end
|
||||
|
||||
# Finally load any registered hooks for dynamically generated
|
||||
# action class based hooks
|
||||
Vagrant.plugin("2").manager.find_action_hooks(action).each do |hook_proc|
|
||||
hook_proc.call(hook)
|
||||
end
|
||||
|
||||
hook.apply(self, root: item)
|
||||
end
|
||||
|
||||
# Apply the hook to ourself to update the stack
|
||||
self
|
||||
end
|
||||
|
||||
# If action hooks have not already been set, this method
|
||||
# will perform three tasks:
|
||||
# 1. Load any hook triggers defined for the action_name
|
||||
# 2. Load any action_hooks defined from plugins
|
||||
# 3. Load any action triggers based on machine action called (not action classes)
|
||||
#
|
||||
# @param [Hash] env Call environment
|
||||
# @return [Builder]
|
||||
def apply_action_name(env)
|
||||
return self if !env[:action_name]
|
||||
hook = Hook.new
|
||||
machine_name = env[:machine].name if env[:machine]
|
||||
|
||||
# Start with loading any hook triggers if applicable
|
||||
if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") && env[:triggers]
|
||||
if !env[:triggers].find(env[:action_name], :before, machine_name, :hook).empty?
|
||||
hook.prepend(Vagrant::Action::Builtin::Trigger,
|
||||
env[:action_name], env[:triggers], :before, :hook)
|
||||
end
|
||||
if !env[:triggers].find(env[:action_name], :after, machine_name, :hook).empty?
|
||||
hook.append(Vagrant::Action::Builtin::Trigger,
|
||||
env[:action_name], env[:triggers], :after, :hook)
|
||||
end
|
||||
end
|
||||
|
||||
# Next we load up all the action hooks that plugins may
|
||||
# have defined
|
||||
action_hooks = Vagrant.plugin("2").manager.action_hooks(env[:action_name])
|
||||
action_hooks.each do |hook_proc|
|
||||
hook_proc.call(hook)
|
||||
end
|
||||
|
||||
# Finally load any action triggers defined. The action triggers
|
||||
# are the originally implemented trigger style. They run before
|
||||
# and after specific provider actions (like :up, :halt, etc) and
|
||||
# are different from true action triggers
|
||||
if env[:triggers]
|
||||
if !env[:triggers].find(env[:raw_action_name], :before, machine_name, :action).empty?
|
||||
hook.prepend(Vagrant::Action::Builtin::Trigger,
|
||||
env[:raw_action_name], env[:triggers], :before, :action)
|
||||
end
|
||||
if !env[:triggers].find(env[:raw_action_name], :after, machine_name, :action).empty?
|
||||
# NOTE: These after triggers need to be delayed before running to
|
||||
# allow the rest of the call stack to complete before being
|
||||
# run. The delayed action is prepended to the stack (not appended)
|
||||
# to ensure it is called first, which results in it properly
|
||||
# waiting for everything to finish before itself completing.
|
||||
builder = self.class.build(Vagrant::Action::Builtin::Trigger,
|
||||
env[:raw_action_name], env[:triggers], :after, :action)
|
||||
hook.prepend(Vagrant::Action::Builtin::Delayed, builder)
|
||||
end
|
||||
end
|
||||
|
||||
# If the hooks are empty, then there was nothing to apply and
|
||||
# we can just send ourself back
|
||||
return self if hook.empty?
|
||||
|
||||
# These are the options to pass into hook application.
|
||||
options = {}
|
||||
|
||||
# If we already ran through once and did append/prepends,
|
||||
# then don't do it again.
|
||||
if env[:action_hooks_already_ran]
|
||||
options[:no_prepend_or_append] = true
|
||||
end
|
||||
|
||||
# Specify that we already ran, so in the future we don't repeat
|
||||
# the prepend/append hooks.
|
||||
env[:action_hooks_already_ran] = true
|
||||
|
||||
# Apply all the hooks to the new builder instance
|
||||
hook.apply(self, options)
|
||||
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
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
|
||||
@ -1,28 +0,0 @@
|
||||
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
|
||||
26
lib/vagrant/action/builtin/delayed.rb
Normal file
26
lib/vagrant/action/builtin/delayed.rb
Normal file
@ -0,0 +1,26 @@
|
||||
module Vagrant
|
||||
module Action
|
||||
module Builtin
|
||||
# This class is used to delay execution until the end of
|
||||
# a configured stack
|
||||
class Delayed
|
||||
# @param [Object] callable The object to call (must respond to #call)
|
||||
def initialize(app, env, callable)
|
||||
if !callable.respond_to?(:call)
|
||||
raise TypeError, "Callable argument is expected to respond to `#call`"
|
||||
end
|
||||
@app = app
|
||||
@env = env
|
||||
@callable = callable
|
||||
end
|
||||
|
||||
def call(env)
|
||||
# Allow the rest of the call stack to execute
|
||||
@app.call(env)
|
||||
# Now call our delayed stack
|
||||
@callable.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
36
lib/vagrant/action/builtin/trigger.rb
Normal file
36
lib/vagrant/action/builtin/trigger.rb
Normal file
@ -0,0 +1,36 @@
|
||||
module Vagrant
|
||||
module Action
|
||||
module Builtin
|
||||
# This class is used within the Builder class for injecting triggers into
|
||||
# different parts of the call stack.
|
||||
class Trigger
|
||||
# @param [Class, String, Symbol] name Name of trigger to fire
|
||||
# @param [Vagrant::Plugin::V2::Triger] triggers Trigger object
|
||||
# @param [Symbol] timing When trigger should fire (:before/:after)
|
||||
# @param [Symbol] type Type of trigger
|
||||
def initialize(app, env, name, triggers, timing, type=:action)
|
||||
@app = app
|
||||
@env = env
|
||||
@triggers = triggers
|
||||
@name = name
|
||||
@timing = timing
|
||||
@type = type
|
||||
|
||||
if ![:before, :after].include?(timing)
|
||||
raise ArgumentError,
|
||||
"Invalid value provided for `timing` (allowed: :before or :after)"
|
||||
end
|
||||
end
|
||||
|
||||
def call(env)
|
||||
machine = env[:machine]
|
||||
machine_name = machine.name if machine
|
||||
|
||||
@triggers.fire(@name, @timing, machine_name, @type)
|
||||
# Carry on
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -65,6 +65,14 @@ module Vagrant
|
||||
@prepend_hooks << [new, args, block]
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
def empty?
|
||||
before_hooks.empty? &&
|
||||
after_hooks.empty? &&
|
||||
prepend_hooks.empty? &&
|
||||
append_hooks.empty?
|
||||
end
|
||||
|
||||
# This applies the given hook to a builder. This should not be
|
||||
# called directly.
|
||||
#
|
||||
@ -75,12 +83,22 @@ module Vagrant
|
||||
if !options[:no_prepend_or_append]
|
||||
# Prepends first
|
||||
@prepend_hooks.each do |klass, args, block|
|
||||
builder.insert(0, klass, *args, &block)
|
||||
if options[:root]
|
||||
idx = builder.index(options[:root])
|
||||
else
|
||||
idx = 0
|
||||
end
|
||||
builder.insert(idx, klass, *args, &block)
|
||||
end
|
||||
|
||||
# Appends
|
||||
@append_hooks.each do |klass, args, block|
|
||||
builder.use(klass, *args, &block)
|
||||
if options[:root]
|
||||
idx = builder.index(options[:root])
|
||||
builder.insert(idx + 1, klass, *args, &block)
|
||||
else
|
||||
builder.use(klass, *args, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -34,30 +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
|
||||
# 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]
|
||||
|
||||
environment[:triggers] = machine.triggers if machine
|
||||
|
||||
if env
|
||||
ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant")
|
||||
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?
|
||||
@logger.info("Preparing hooks for middleware sequence...")
|
||||
environment[:action_hooks] = hooks.map do |hook_proc|
|
||||
Hook.new.tap do |h|
|
||||
hook_proc.call(h)
|
||||
end
|
||||
end
|
||||
|
||||
@logger.info("#{environment[:action_hooks].length} hooks defined.")
|
||||
environment[:triggers] ||= Vagrant::Plugin::V2::Trigger.
|
||||
new(env, env.vagrantfile.config.trigger, machine, ui)
|
||||
end
|
||||
|
||||
# Run the action chain in a busy block, marking the environment as
|
||||
@ -95,14 +84,10 @@ module Vagrant
|
||||
|
||||
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
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
require "log4r"
|
||||
require 'vagrant/util/experimental'
|
||||
require 'vagrant/action/builtin/before_trigger'
|
||||
require 'vagrant/action/builtin/after_trigger'
|
||||
|
||||
module Vagrant
|
||||
module Action
|
||||
@ -47,40 +45,9 @@ module Vagrant
|
||||
raise Errors::VagrantInterrupt if env[:interrupted]
|
||||
action = @actions.shift
|
||||
@logger.info("Calling IN action: #{action}")
|
||||
|
||||
actions = [action]
|
||||
|
||||
# If this is an action class, set name to check for any defined hooks
|
||||
if !action.is_a?(Proc)
|
||||
hook_name = action.class.name
|
||||
@logger.debug("Performing hooks lookup on #{hook_name} (Action: #{action} - class: #{action.class})")
|
||||
hooks = Vagrant.plugin("2").manager.find_action_hooks(hook_name)
|
||||
|
||||
if !hooks.empty?
|
||||
@logger.debug("Found #{hooks.count} hooks defined for #{hook_name} (Action: #{action})")
|
||||
# Create a new builder for the current action so we can
|
||||
# properly apply the hooks with the correct ordering
|
||||
b = Builder.new
|
||||
b.stack << action
|
||||
|
||||
hooks.each do |hook_proc|
|
||||
hook = Hook.new.tap { |h| hook_proc.call(h) }
|
||||
hook.apply(b)
|
||||
end
|
||||
|
||||
actions = b.stack.map { |a| finalize_action(a, @env) }
|
||||
end
|
||||
end
|
||||
|
||||
@logger.info("Calling BEGIN action: #{action}")
|
||||
actions.each do |local_action|
|
||||
@logger.info("Calling IN action: #{local_action} (root: #{action})")
|
||||
@stack.unshift(local_action).first.call(env)
|
||||
raise Errors::VagrantInterrupt if env[:interrupted]
|
||||
@logger.info("Calling OUT action: #{local_action} (root: #{action})")
|
||||
end
|
||||
|
||||
@logger.info("Calling COMPLETE action: #{action}")
|
||||
@stack.unshift(action).first.call(env)
|
||||
raise Errors::VagrantInterrupt if env[:interrupted]
|
||||
@logger.info("Calling OUT action: #{action}")
|
||||
rescue SystemExit, NoMemoryError
|
||||
# This means that an "exit" or "abort" was called, or we have run out
|
||||
# of memory. In these cases, we just exit immediately.
|
||||
@ -132,19 +99,7 @@ module Vagrant
|
||||
args ||= []
|
||||
|
||||
if klass.is_a?(Class)
|
||||
# A action klass which is to be instantiated with the
|
||||
# app, env, and any arguments given
|
||||
|
||||
# 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)]
|
||||
klass.new(self, env, *args, &block)
|
||||
elsif klass.respond_to?(:call)
|
||||
# Make it a lambda which calls the item then forwards
|
||||
# up the chain
|
||||
|
||||
@ -62,9 +62,11 @@ 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")
|
||||
@triggers.fire(@sub_command, :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")
|
||||
@triggers.fire(@sub_command, :after, nil, :command) if
|
||||
Vagrant::Util::Experimental.feature_enabled?("typed_triggers")
|
||||
rescue Interrupt
|
||||
@env.ui.info(I18n.t("vagrant.cli_interrupt"))
|
||||
result = 1
|
||||
|
||||
@ -62,6 +62,11 @@ module Vagrant
|
||||
# @return [Hash]
|
||||
attr_reader :provider_options
|
||||
|
||||
# The triggers with machine specific configuration applied
|
||||
#
|
||||
# @return [Vagrant::Plugin::V2::Trigger]
|
||||
attr_reader :triggers
|
||||
|
||||
# The UI for outputting in the scope of this machine.
|
||||
#
|
||||
# @return [UI]
|
||||
@ -160,8 +165,6 @@ module Vagrant
|
||||
# as extra data set on the environment hash for the middleware
|
||||
# runner.
|
||||
def action(name, opts=nil)
|
||||
@triggers.fire_triggers(name, :before, @name.to_s, :action)
|
||||
|
||||
@logger.info("Calling action: #{name} on provider #{@provider}")
|
||||
|
||||
opts ||= {}
|
||||
@ -210,8 +213,6 @@ module Vagrant
|
||||
ui.machine("action", name.to_s, "end")
|
||||
action_result
|
||||
end
|
||||
|
||||
@triggers.fire_triggers(name, :after, @name.to_s, :action)
|
||||
# preserve returning environment after machine action runs
|
||||
return return_env
|
||||
rescue Errors::EnvironmentLockedError
|
||||
@ -230,6 +231,7 @@ module Vagrant
|
||||
def action_raw(name, callable, extra_env=nil)
|
||||
# Run the action with the action runner on the environment
|
||||
env = {
|
||||
raw_action_name: name,
|
||||
action_name: "machine_action_#{name}".to_sym,
|
||||
machine: self,
|
||||
machine_action: name,
|
||||
|
||||
@ -64,7 +64,11 @@ module Vagrant
|
||||
# @param [Class, String] key Base key for generation
|
||||
# @return [Array<String>] all valid keys
|
||||
def generate_hook_keys(key)
|
||||
key = key.is_a?(Class) ? key.name : key.to_s
|
||||
if key.is_a?(Class)
|
||||
key = key.name.to_s
|
||||
else
|
||||
key = key.to_s
|
||||
end
|
||||
parts = key.split("::")
|
||||
[].tap do |keys|
|
||||
until parts.empty?
|
||||
|
||||
@ -71,6 +71,7 @@ module Vagrant
|
||||
# @return [Array] List of the hooks for the given action.
|
||||
def self.action_hook(name, hook_name=nil, &block)
|
||||
# The name is currently not used but we want it for the future.
|
||||
hook_name = hook_name.to_s if hook_name
|
||||
|
||||
hook_name ||= ALL_ACTIONS
|
||||
components.action_hooks[hook_name.to_sym] << block
|
||||
|
||||
@ -30,58 +30,97 @@ module Vagrant
|
||||
@logger = Log4r::Logger.new("vagrant::trigger::#{self.class.to_s.downcase}")
|
||||
end
|
||||
|
||||
# Fires all triggers, if any are defined for the action and guest. Returns early
|
||||
# Fires all triggers, if any are defined for the named type 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] name Name of `type` thing 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, type)
|
||||
# @param [String] guest The guest that invoked firing the triggers
|
||||
# @param [Symbol] type Type of trigger to fire (:action, :hook, :command)
|
||||
def fire(name, stage, guest, 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
|
||||
return @logger.warn("Name given is nil, no triggers will fire") if !name
|
||||
return @logger.warn("Name given cannot be symbolized, no triggers will fire") if
|
||||
!name.respond_to?(:to_sym)
|
||||
|
||||
name = name.to_sym
|
||||
|
||||
# get all triggers matching action
|
||||
triggers = []
|
||||
triggers = find(name, stage, guest, type)
|
||||
|
||||
if !triggers.empty?
|
||||
@logger.info("Firing trigger for #{type} #{name} on guest #{guest}")
|
||||
@ui.info(I18n.t("vagrant.trigger.start", type: type, stage: stage, name: name))
|
||||
execute(triggers)
|
||||
end
|
||||
end
|
||||
|
||||
# Find all triggers defined for the named type and guest.
|
||||
#
|
||||
# @param [Symbol] name Name of `type` thing to fire trigger on
|
||||
# @param [Symbol] stage :before or :after
|
||||
# @param [String] guest The guest that invoked firing the triggers
|
||||
# @param [Symbol] type Type of trigger to fire
|
||||
# @return [Array]
|
||||
def find(name, stage, guest, type)
|
||||
triggers = nil
|
||||
name = nameify(name)
|
||||
|
||||
if stage == :before
|
||||
triggers = config.before_triggers.select do |t|
|
||||
t.command == action || (t.command == :all && !t.ignore.include?(action))
|
||||
(t.command == :all && !t.ignore.include?(name)) ||
|
||||
(type == :hook && matched_hook?(t.command, name)) ||
|
||||
nameify(t.command) == name
|
||||
end
|
||||
elsif stage == :after
|
||||
triggers = config.after_triggers.select do |t|
|
||||
t.command == action || (t.command == :all && !t.ignore.include?(action))
|
||||
(t.command == :all && !t.ignore.include?(name)) ||
|
||||
(type == :hook && matched_hook?(t.command, name)) ||
|
||||
nameify(t.command) == name
|
||||
end
|
||||
else
|
||||
raise Errors::TriggersNoStageGiven,
|
||||
action: action,
|
||||
name: name,
|
||||
stage: stage,
|
||||
guest_name: guest_name
|
||||
end
|
||||
|
||||
triggers = filter_triggers(triggers, guest_name, type)
|
||||
|
||||
if !triggers.empty?
|
||||
@logger.info("Firing trigger for action #{action} on guest #{guest_name}")
|
||||
@ui.info(I18n.t("vagrant.trigger.start", type: type, stage: stage, action: action))
|
||||
fire(triggers, guest_name)
|
||||
type: type,
|
||||
guest_name: guest
|
||||
end
|
||||
filter_triggers(triggers, guest, type)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Convert object into name
|
||||
#
|
||||
# @param [Object, Class] object Object to name
|
||||
# @return [String]
|
||||
def nameify(object)
|
||||
if object.is_a?(Class)
|
||||
object.name.to_s
|
||||
else
|
||||
object.to_s
|
||||
end
|
||||
end
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
# Internal methods, don't call these.
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
# Generate all valid lookup keys for given action key
|
||||
#
|
||||
# @param [Class, String] key Base key for generation
|
||||
# @return [Array<String>] all valid keys
|
||||
def matched_hook?(key, subject)
|
||||
subject = nameify(subject)
|
||||
Vagrant.plugin("2").manager.generate_hook_keys(key).any? do |k|
|
||||
k == subject
|
||||
end
|
||||
end
|
||||
|
||||
# Looks up if the community plugin `vagrant-triggers` is installed
|
||||
# and also caches the result
|
||||
#
|
||||
@ -133,12 +172,11 @@ module Vagrant
|
||||
return triggers
|
||||
end
|
||||
|
||||
# Fires off all triggers in the given array
|
||||
# Execute all triggers in the given array
|
||||
#
|
||||
# @param [Array] triggers An array of triggers to be fired
|
||||
def fire(triggers, guest_name)
|
||||
def execute(triggers)
|
||||
# ensure on_error is respected by exiting or continuing
|
||||
|
||||
triggers.each do |trigger|
|
||||
@logger.debug("Running trigger #{trigger.id}...")
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ module Vagrant
|
||||
autoload :CredentialScrubber, 'vagrant/util/credential_scrubber'
|
||||
autoload :DeepMerge, 'vagrant/util/deep_merge'
|
||||
autoload :Env, 'vagrant/util/env'
|
||||
autoload :Experimental, 'vagrant/util/experimental'
|
||||
autoload :HashWithIndifferentAccess, 'vagrant/util/hash_with_indifferent_access'
|
||||
autoload :GuestInspection, 'vagrant/util/guest_inspection'
|
||||
autoload :LoggingFormatter, 'vagrant/util/logging_formatter'
|
||||
|
||||
@ -114,7 +114,7 @@ module VagrantPlugins
|
||||
|
||||
# Internal options
|
||||
@id = SecureRandom.uuid
|
||||
@command = command.to_sym
|
||||
@command = command.to_s
|
||||
@ruby_block = UNSET_VALUE
|
||||
|
||||
@logger.debug("Trigger defined for: #{command}")
|
||||
@ -214,10 +214,7 @@ module VagrantPlugins
|
||||
end
|
||||
|
||||
if @type == :command || !@type
|
||||
commands = []
|
||||
Vagrant.plugin("2").manager.commands.each do |key,data|
|
||||
commands.push(key)
|
||||
end
|
||||
commands = Vagrant.plugin("2").manager.commands.keys.map(&:to_s)
|
||||
|
||||
if !commands.include?(@command) && @command != :all
|
||||
machine.ui.warn(I18n.t("vagrant.config.triggers.bad_command_warning",
|
||||
|
||||
@ -301,7 +301,7 @@ en:
|
||||
Vagrant has been configured to abort. Vagrant will terminate
|
||||
after remaining running actions have completed...
|
||||
start: |-
|
||||
Running %{type} triggers %{stage} %{action} ...
|
||||
Running %{type} triggers %{stage} %{name} ...
|
||||
fire_with_name: |-
|
||||
Running trigger: %{name}...
|
||||
fire: |-
|
||||
@ -1529,7 +1529,8 @@ en:
|
||||
triggers_no_stage_given: |-
|
||||
The incorrect stage was given to the trigger plugin:
|
||||
Guest: %{guest_name}
|
||||
Action: %{action}
|
||||
Name: %{name}
|
||||
Type: %{type}
|
||||
Stage: %{stage}
|
||||
|
||||
This is an internal error that should be reported as a bug.
|
||||
|
||||
@ -53,7 +53,7 @@ describe VagrantPlugins::Kernel_V2::VagrantConfigTrigger do
|
||||
|
||||
it "sets a command" do
|
||||
subject.finalize!
|
||||
expect(subject.command).to eq(command)
|
||||
expect(subject.command).to eq(command.to_s)
|
||||
end
|
||||
|
||||
it "uses default error behavior" do
|
||||
|
||||
@ -237,13 +237,19 @@ describe Vagrant::Action::Builder do
|
||||
end
|
||||
|
||||
describe "action hooks" do
|
||||
it "applies them properly" do
|
||||
hook = double("hook")
|
||||
allow(hook).to receive(:apply) do |builder|
|
||||
builder.use appender_proc(2)
|
||||
end
|
||||
let(:hook) { double("hook") }
|
||||
let(:manager) { Vagrant.plugin("2").manager }
|
||||
|
||||
data[:action_hooks] = [hook]
|
||||
before do
|
||||
allow(manager).to receive(:action_hooks).and_return([])
|
||||
end
|
||||
|
||||
it "applies them properly" do
|
||||
hook_proc = proc{ |h| h.append(appender_proc(2)) }
|
||||
expect(manager).to receive(:action_hooks).with(:test_action).
|
||||
and_return([hook_proc])
|
||||
|
||||
data[:action_name] = :test_action
|
||||
|
||||
subject.use appender_proc(1)
|
||||
subject.call(data)
|
||||
@ -253,11 +259,16 @@ describe Vagrant::Action::Builder do
|
||||
end
|
||||
|
||||
it "applies without prepend/append if it has already" do
|
||||
hook = double("hook")
|
||||
expect(hook).to receive(:apply).with(anything, { no_prepend_or_append: true }).once
|
||||
hook_proc = proc{ |h| h.append(appender_proc(2)) }
|
||||
expect(manager).to receive(:action_hooks).with(:test_action).
|
||||
and_return([hook_proc])
|
||||
|
||||
data[:action_hooks] = [hook]
|
||||
data[:action_hooks_already_ran] = true
|
||||
data[:action_name] = :test_action
|
||||
|
||||
subject.use appender_proc(1)
|
||||
subject.call(data.merge(action_hooks_already_ran: true))
|
||||
|
||||
expect(data[:data]).to eq([1])
|
||||
subject.call(data)
|
||||
end
|
||||
end
|
||||
@ -295,4 +306,456 @@ describe Vagrant::Action::Builder do
|
||||
"1_in", "2_in", "3_in", "3_out", "2_out", "1_out"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "dynamic action hooks" do
|
||||
class ActionOne
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env[:data] << 1 if env[:data]
|
||||
@app.call(env)
|
||||
end
|
||||
|
||||
def recover(env)
|
||||
env[:recover] << 1
|
||||
end
|
||||
end
|
||||
|
||||
class ActionTwo
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env[:data] << 2 if env[:data]
|
||||
@app.call(env)
|
||||
end
|
||||
|
||||
def recover(env)
|
||||
env[:recover] << 2
|
||||
end
|
||||
end
|
||||
|
||||
let(:data) { {data: []} }
|
||||
let(:hook_action_name) { :action_two }
|
||||
|
||||
let(:plugin) do
|
||||
h_name = hook_action_name
|
||||
@plugin ||= Class.new(Vagrant.plugin("2")) do
|
||||
name "Test Plugin"
|
||||
action_hook(:before_test, h_name) do |hook|
|
||||
hook.prepend(proc{ |env| env[:data] << :first })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before { plugin }
|
||||
|
||||
after do
|
||||
Vagrant.plugin("2").manager.unregister(@plugin) if @plugin
|
||||
@plugin = nil
|
||||
end
|
||||
|
||||
it "should call hook before running action" do
|
||||
instance = described_class.build(ActionTwo)
|
||||
instance.call(data)
|
||||
expect(data[:data].first).to eq(:first)
|
||||
expect(data[:data].last).to eq(2)
|
||||
end
|
||||
|
||||
context "when hook is appending to action" do
|
||||
let(:plugin) do
|
||||
@plugin ||= Class.new(Vagrant.plugin("2")) do
|
||||
name "Test Plugin"
|
||||
action_hook(:before_test, :action_two) do |hook|
|
||||
hook.append(proc{ |env| env[:data] << :first })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "should call hook after action when action is nested" do
|
||||
instance = described_class.build(ActionTwo).use(described_class.build(ActionOne))
|
||||
instance.call(data)
|
||||
expect(data[:data][0]).to eq(2)
|
||||
expect(data[:data][1]).to eq(:first)
|
||||
expect(data[:data][2]).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses class name" do
|
||||
let(:hook_action_name) { "ActionTwo" }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance = described_class.build(ActionTwo)
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when action includes a namespace" do
|
||||
module Vagrant
|
||||
module Test
|
||||
class ActionTest
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env[:data] << :test if env[:data]
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:instance) { described_class.build(Vagrant::Test::ActionTest) }
|
||||
|
||||
context "when hook uses short snake case name" do
|
||||
let(:hook_action_name) { :action_test }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses partial snake case name" do
|
||||
let(:hook_action_name) { :test_action_test }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses full snake case name" do
|
||||
let(:hook_action_name) { :vagrant_test_action_test }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses short class name" do
|
||||
let(:hook_action_name) { "ActionTest" }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses partial namespace class name" do
|
||||
let(:hook_action_name) { "Test::ActionTest" }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses full namespace class name" do
|
||||
let(:hook_action_name) { "Vagrant::Test::ActionTest" }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#apply_dynamic_updates" do
|
||||
let(:env) { {triggers: triggers, machine: machine} }
|
||||
let(:machine) { nil }
|
||||
let(:triggers) { nil }
|
||||
|
||||
let(:subject) do
|
||||
@subject ||= described_class.new.tap do |b|
|
||||
b.use Vagrant::Action::Builtin::EnvSet
|
||||
b.use Vagrant::Action::Builtin::Confirm
|
||||
end
|
||||
end
|
||||
|
||||
after { @subject = nil }
|
||||
|
||||
it "should not modify the builder stack by default" do
|
||||
s1 = subject.stack.dup
|
||||
subject.apply_dynamic_updates(env)
|
||||
s2 = subject.stack.dup
|
||||
expect(s1).to eq(s2)
|
||||
end
|
||||
|
||||
context "when an action hooks is defined" do
|
||||
let(:plugin) do
|
||||
@plugin ||= Class.new(Vagrant.plugin("2")) do
|
||||
name "Test Plugin"
|
||||
action_hook(:before_action, Vagrant::Action::Builtin::Confirm) do |hook|
|
||||
hook.prepend(Vagrant::Action::Builtin::Call)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before { plugin }
|
||||
|
||||
after do
|
||||
Vagrant.plugin("2").manager.unregister(@plugin) if @plugin
|
||||
@plugin = nil
|
||||
end
|
||||
|
||||
it "should modify the builder stack" do
|
||||
s1 = subject.stack.dup
|
||||
subject.apply_dynamic_updates(env)
|
||||
s2 = subject.stack.dup
|
||||
expect(s1).not_to eq(s2)
|
||||
end
|
||||
|
||||
it "should add new action to the middle of the call stack" do
|
||||
subject.apply_dynamic_updates(env)
|
||||
expect(subject.stack[1].first).to eq(Vagrant::Action::Builtin::Call)
|
||||
end
|
||||
end
|
||||
|
||||
context "when triggers are enabled" do
|
||||
let(:triggers) { double("triggers") }
|
||||
|
||||
before do
|
||||
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
|
||||
with("typed_triggers").and_return(true)
|
||||
allow(triggers).to receive(:find).and_return([])
|
||||
end
|
||||
|
||||
it "should not modify the builder stack by default" do
|
||||
s1 = subject.stack.dup
|
||||
subject.apply_dynamic_updates(env)
|
||||
s2 = subject.stack.dup
|
||||
expect(s1).to eq(s2)
|
||||
end
|
||||
|
||||
context "when triggers are found" do
|
||||
let(:action) { Vagrant::Action::Builtin::EnvSet }
|
||||
|
||||
before { expect(triggers).to receive(:find).
|
||||
with(action, timing, nil, type).and_return([true]) }
|
||||
|
||||
context "for action type" do
|
||||
let(:type) { :action }
|
||||
|
||||
context "for before timing" do
|
||||
let(:timing) { :before }
|
||||
|
||||
it "should add trigger action to start of stack" do
|
||||
subject.apply_dynamic_updates(env)
|
||||
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Trigger)
|
||||
end
|
||||
|
||||
it "should have timing and type arguments" do
|
||||
subject.apply_dynamic_updates(env)
|
||||
args = subject.stack[0][1]
|
||||
expect(args).to include(type)
|
||||
expect(args).to include(timing)
|
||||
expect(args).to include(action.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context "for after timing" do
|
||||
let(:timing) { :after }
|
||||
|
||||
it "should add trigger action to middle of stack" do
|
||||
subject.apply_dynamic_updates(env)
|
||||
expect(subject.stack[1].first).to eq(Vagrant::Action::Builtin::Trigger)
|
||||
end
|
||||
|
||||
it "should have timing and type arguments" do
|
||||
subject.apply_dynamic_updates(env)
|
||||
args = subject.stack[1][1]
|
||||
expect(args).to include(type)
|
||||
expect(args).to include(timing)
|
||||
expect(args).to include(action.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for hook type" do
|
||||
let(:type) { :hook }
|
||||
|
||||
context "for before timing" do
|
||||
let(:timing) { :before }
|
||||
|
||||
it "should add trigger action to start of stack" do
|
||||
subject.apply_dynamic_updates(env)
|
||||
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Trigger)
|
||||
end
|
||||
|
||||
it "should have timing and type arguments" do
|
||||
subject.apply_dynamic_updates(env)
|
||||
args = subject.stack[0][1]
|
||||
expect(args).to include(type)
|
||||
expect(args).to include(timing)
|
||||
expect(args).to include(action.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context "for after timing" do
|
||||
let(:timing) { :after }
|
||||
|
||||
it "should add trigger action to middle of stack" do
|
||||
subject.apply_dynamic_updates(env)
|
||||
expect(subject.stack[1].first).to eq(Vagrant::Action::Builtin::Trigger)
|
||||
end
|
||||
|
||||
it "should have timing and type arguments" do
|
||||
subject.apply_dynamic_updates(env)
|
||||
args = subject.stack[1][1]
|
||||
expect(args).to include(type)
|
||||
expect(args).to include(timing)
|
||||
expect(args).to include(action.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#apply_action_name" do
|
||||
let(:env) { {triggers: triggers, machine: machine, action_name: action_name, raw_action_name: raw_action_name} }
|
||||
let(:raw_action_name) { :up }
|
||||
let(:action_name) { "machine_#{raw_action_name}".to_sym }
|
||||
let(:machine) { nil }
|
||||
let(:triggers) { double("triggers") }
|
||||
|
||||
let(:subject) do
|
||||
@subject ||= described_class.new.tap do |b|
|
||||
b.use Vagrant::Action::Builtin::EnvSet
|
||||
b.use Vagrant::Action::Builtin::Confirm
|
||||
end
|
||||
end
|
||||
|
||||
before { allow(triggers).to receive(:find).and_return([]) }
|
||||
after { @subject = nil }
|
||||
|
||||
it "should mark action hooks applied within env" do
|
||||
subject.apply_action_name(env)
|
||||
expect(env[:action_hooks_already_ran]).to be_truthy
|
||||
end
|
||||
|
||||
context "when a plugin has added an action hook" do
|
||||
let(:plugin) do
|
||||
@plugin ||= Class.new(Vagrant.plugin("2")) do
|
||||
name "Test Plugin"
|
||||
action_hook(:before_action, :machine_up) do |hook|
|
||||
hook.prepend(Vagrant::Action::Builtin::Call)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before { plugin }
|
||||
|
||||
after do
|
||||
Vagrant.plugin("2").manager.unregister(@plugin) if @plugin
|
||||
@plugin = nil
|
||||
end
|
||||
|
||||
it "should add new action to the call stack" do
|
||||
subject.apply_action_name(env)
|
||||
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Call)
|
||||
end
|
||||
|
||||
it "should only add new action to the call stack once" do
|
||||
subject.apply_action_name(env)
|
||||
subject.apply_action_name(env)
|
||||
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Call)
|
||||
expect(subject.stack[1].first).not_to eq(Vagrant::Action::Builtin::Call)
|
||||
end
|
||||
end
|
||||
|
||||
context "when trigger has been defined for raw action" do
|
||||
before do
|
||||
expect(triggers).to receive(:find).with(raw_action_name, timing, nil, :action).
|
||||
and_return([true])
|
||||
end
|
||||
|
||||
context "when timing is before" do
|
||||
let(:timing) { :before }
|
||||
|
||||
it "should add a trigger action to the start of the stack" do
|
||||
subject.apply_action_name(env)
|
||||
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Trigger)
|
||||
end
|
||||
|
||||
it "should include arguments to the trigger action" do
|
||||
subject.apply_action_name(env)
|
||||
args = subject.stack[0][1]
|
||||
expect(args).to include(raw_action_name)
|
||||
expect(args).to include(timing)
|
||||
expect(args).to include(:action)
|
||||
end
|
||||
end
|
||||
|
||||
context "when timing is after" do
|
||||
let(:timing) { :after }
|
||||
|
||||
it "should add a trigger action to the end of the stack" do
|
||||
subject.apply_action_name(env)
|
||||
expect(subject.stack.first.first).to eq(Vagrant::Action::Builtin::Delayed)
|
||||
end
|
||||
|
||||
it "should include arguments to the trigger action" do
|
||||
subject.apply_action_name(env)
|
||||
builder = subject.stack.first[1]&.first
|
||||
expect(builder).not_to be_nil
|
||||
args = builder.stack.first[1]
|
||||
expect(args).to include(raw_action_name)
|
||||
expect(args).to include(timing)
|
||||
expect(args).to include(:action)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when trigger has been defined for hook" do
|
||||
before do
|
||||
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
|
||||
with("typed_triggers").and_return(true)
|
||||
expect(triggers).to receive(:find).with(action_name, timing, nil, :hook).
|
||||
and_return([true])
|
||||
end
|
||||
|
||||
context "when timing is before" do
|
||||
let(:timing) { :before }
|
||||
|
||||
it "should add a trigger action to the start of the stack" do
|
||||
subject.apply_action_name(env)
|
||||
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Trigger)
|
||||
end
|
||||
|
||||
it "should include arguments to the trigger action" do
|
||||
subject.apply_action_name(env)
|
||||
args = subject.stack[0][1]
|
||||
expect(args).to include(action_name)
|
||||
expect(args).to include(timing)
|
||||
expect(args).to include(:hook)
|
||||
end
|
||||
end
|
||||
|
||||
context "when timing is after" do
|
||||
let(:timing) { :after }
|
||||
|
||||
it "should add a trigger action to the end of the stack" do
|
||||
subject.apply_action_name(env)
|
||||
expect(subject.stack.last.first).to eq(Vagrant::Action::Builtin::Trigger)
|
||||
end
|
||||
|
||||
it "should include arguments to the trigger action" do
|
||||
subject.apply_action_name(env)
|
||||
args = subject.stack.last[1]
|
||||
expect(args).to include(action_name)
|
||||
expect(args).to include(timing)
|
||||
expect(args).to include(:hook)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
22
test/unit/vagrant/action/builtin/delayed_test.rb
Normal file
22
test/unit/vagrant/action/builtin/delayed_test.rb
Normal file
@ -0,0 +1,22 @@
|
||||
require File.expand_path("../../../../base", __FILE__)
|
||||
|
||||
describe Vagrant::Action::Builtin::Delayed do
|
||||
let(:app) { lambda {|*_|} }
|
||||
let(:env) { {} }
|
||||
|
||||
it "should raise error when callable does not provide #call" do
|
||||
expect { described_class.new(app, env, true) }.
|
||||
to raise_error(TypeError)
|
||||
end
|
||||
|
||||
it "should delay executing action to end of stack" do
|
||||
result = []
|
||||
one = proc{ |*_| result << :one }
|
||||
two = proc{ |*_| result << :two }
|
||||
builder = Vagrant::Action::Builder.build(described_class, two)
|
||||
builder.use(one)
|
||||
builder.call(env)
|
||||
expect(result.first).to eq(:one)
|
||||
expect(result.last).to eq(:two)
|
||||
end
|
||||
end
|
||||
63
test/unit/vagrant/action/builtin/trigger_test.rb
Normal file
63
test/unit/vagrant/action/builtin/trigger_test.rb
Normal file
@ -0,0 +1,63 @@
|
||||
require File.expand_path("../../../../base", __FILE__)
|
||||
|
||||
describe Vagrant::Action::Builtin::Trigger do
|
||||
let(:app) { lambda { |env| } }
|
||||
let(:env) { {machine: machine} }
|
||||
let(:machine) { nil }
|
||||
let(:triggers) { double("triggers") }
|
||||
let(:name) { "trigger-name" }
|
||||
let(:timing) { :before }
|
||||
let(:type) { :action }
|
||||
|
||||
let(:subject) { described_class.
|
||||
new(app, env, name, triggers, timing, type) }
|
||||
|
||||
before do
|
||||
allow(triggers).to receive(:fire)
|
||||
allow(app).to receive(:call)
|
||||
end
|
||||
|
||||
|
||||
it "should properly create a new instance" do
|
||||
expect(subject).to be_a(described_class)
|
||||
end
|
||||
|
||||
it "should fire triggers" do
|
||||
expect(triggers).to receive(:fire)
|
||||
subject.call(env)
|
||||
end
|
||||
|
||||
it "should fire triggers without machine name" do
|
||||
expect(triggers).to receive(:fire).with(name, timing, nil, type)
|
||||
subject.call(env)
|
||||
end
|
||||
|
||||
context "when machine is provided" do
|
||||
let(:machine) { double("machine", name: "machine-name") }
|
||||
|
||||
it "should include machine name when firing triggers" do
|
||||
expect(triggers).to receive(:fire).with(name, timing, "machine-name", type)
|
||||
subject.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
context "when timing is :before" do
|
||||
it "should not error" do
|
||||
expect { subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context "when timing is :after" do
|
||||
it "should not error" do
|
||||
expect { subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context "when timing is not :before or :after" do
|
||||
let(:timing) { :unknown }
|
||||
|
||||
it "should raise error" do
|
||||
expect { subject }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -96,4 +96,49 @@ describe Vagrant::Action::Runner do
|
||||
instance.run(callable)
|
||||
expect(result).to eq("bar")
|
||||
end
|
||||
|
||||
describe "triggers" do
|
||||
let(:environment) { double("environment", ui: nil) }
|
||||
let(:machine) { double("machine", triggers: machine_triggers, name: "") }
|
||||
let(:env_triggers) { double("env_triggers", find: []) }
|
||||
let(:machine_triggers) { double("machine_triggers", find: []) }
|
||||
|
||||
before do
|
||||
allow(environment).to receive_message_chain(:vagrantfile, :config, :trigger)
|
||||
allow(Vagrant::Plugin::V2::Trigger).to receive(:new).
|
||||
and_return(env_triggers)
|
||||
end
|
||||
|
||||
context "when only environment is provided" do
|
||||
let(:instance) { described_class.new(action_name: "test", env: environment) }
|
||||
|
||||
it "should use environment to build new trigger" do
|
||||
expect(environment).to receive_message_chain(:vagrantfile, :config, :trigger)
|
||||
instance.run(lambda{|_|})
|
||||
end
|
||||
|
||||
it "should pass environment based triggers to callable" do
|
||||
callable = lambda { |env| expect(env[:triggers]).to eq(env_triggers) }
|
||||
instance.run(callable)
|
||||
end
|
||||
end
|
||||
|
||||
context "when only machine is provided" do
|
||||
let(:instance) { described_class.new(action_name: "test", machine: machine) }
|
||||
|
||||
it "should pass machine based triggers to callable" do
|
||||
callable = lambda { |env| expect(env[:triggers]).to eq(machine_triggers) }
|
||||
instance.run(callable)
|
||||
end
|
||||
end
|
||||
|
||||
context "when both environment and machine is provided" do
|
||||
let(:instance) { described_class.new(action_name: "test", machine: machine, env: environment) }
|
||||
|
||||
it "should pass machine based triggers to callable" do
|
||||
callable = lambda { |env| expect(env[:triggers]).to eq(machine_triggers) }
|
||||
instance.run(callable)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -103,135 +103,4 @@ describe Vagrant::Action::Warden do
|
||||
# The recover should not have been called
|
||||
expect(data.key?(:recover)).not_to be
|
||||
end
|
||||
|
||||
describe "dynamic action hooks" do
|
||||
let(:data) { {data: []} }
|
||||
let(:hook_action_name) { :action_two }
|
||||
|
||||
let(:plugin) do
|
||||
h_name = hook_action_name
|
||||
@plugin ||= Class.new(Vagrant.plugin("2")) do
|
||||
name "Test Plugin"
|
||||
action_hook(:before_test, h_name) do |hook|
|
||||
hook.prepend(proc{ |env| env[:data] << :first })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before { plugin }
|
||||
|
||||
after do
|
||||
Vagrant.plugin("2").manager.unregister(@plugin) if @plugin
|
||||
@plugin = nil
|
||||
end
|
||||
|
||||
it "should call hook before running action" do
|
||||
instance = described_class.new([ActionTwo], data)
|
||||
instance.call(data)
|
||||
expect(data[:data].first).to eq(:first)
|
||||
expect(data[:data].last).to eq(2)
|
||||
end
|
||||
|
||||
context "when hook is appending to action" do
|
||||
let(:plugin) do
|
||||
@plugin ||= Class.new(Vagrant.plugin("2")) do
|
||||
name "Test Plugin"
|
||||
action_hook(:before_test, :action_two) do |hook|
|
||||
hook.append(proc{ |env| env[:data] << :first })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "should call hook after action when action is nested" do
|
||||
instance = described_class.new([described_class.new([ActionTwo], data), ActionOne], data)
|
||||
instance.call(data)
|
||||
expect(data[:data][0]).to eq(2)
|
||||
expect(data[:data][1]).to eq(:first)
|
||||
expect(data[:data][2]).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses class name" do
|
||||
let(:hook_action_name) { "ActionTwo" }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance = described_class.new([ActionTwo], data)
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when action includes a namespace" do
|
||||
module Vagrant
|
||||
module Test
|
||||
class ActionTest
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env[:data] << :test if env[:data]
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:instance) { described_class.new([Vagrant::Test::ActionTest], data) }
|
||||
|
||||
context "when hook uses short snake case name" do
|
||||
let(:hook_action_name) { :action_test }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses partial snake case name" do
|
||||
let(:hook_action_name) { :test_action_test }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses full snake case name" do
|
||||
let(:hook_action_name) { :vagrant_test_action_test }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses short class name" do
|
||||
let(:hook_action_name) { "ActionTest" }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses partial namespace class name" do
|
||||
let(:hook_action_name) { "Test::ActionTest" }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hook uses full namespace class name" do
|
||||
let(:hook_action_name) { "Vagrant::Test::ActionTest" }
|
||||
|
||||
it "should execute the hook" do
|
||||
instance.call(data)
|
||||
expect(data[:data]).to include(:first)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -59,8 +59,9 @@ describe Vagrant::CLI do
|
||||
|
||||
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)
|
||||
with("typed_triggers").and_return(true)
|
||||
allow(triggers).to receive(:fire)
|
||||
allow(triggers).to receive(:find).and_return([double("trigger-result")])
|
||||
|
||||
commands[:destroy] = [command_lambda("destroy", 42), {}]
|
||||
|
||||
@ -68,7 +69,7 @@ describe Vagrant::CLI do
|
||||
|
||||
subject = described_class.new(["destroy"], env)
|
||||
|
||||
expect(triggers).to receive(:fire_triggers).twice
|
||||
expect(triggers).to receive(:fire).twice
|
||||
|
||||
expect(subject).not_to receive(:help)
|
||||
expect(subject.execute).to eql(42)
|
||||
@ -76,13 +77,13 @@ describe Vagrant::CLI do
|
||||
|
||||
it "does not fire triggers if disabled" do
|
||||
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
|
||||
with("typed_triggers").and_return("false")
|
||||
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(triggers).not_to receive(:fire)
|
||||
|
||||
expect(subject).not_to receive(:help)
|
||||
expect(subject.execute).to eql(42)
|
||||
|
||||
@ -987,7 +987,6 @@ VF
|
||||
Dir.chdir(temp_dir) do
|
||||
instance = described_class.new(local_data_path: "foo")
|
||||
expect(instance.local_data_path).to eq(instance.cwd.join("foo"))
|
||||
expect(File.exist?(instance.local_data_path)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -1108,7 +1107,6 @@ VF
|
||||
end
|
||||
|
||||
expect { instance }.to_not raise_error
|
||||
expect(Pathname.new(local_data_path)).to_not be_exist
|
||||
end
|
||||
|
||||
it "should upgrade all active VMs" do
|
||||
|
||||
@ -36,31 +36,99 @@ describe Vagrant::Plugin::V2::Trigger do
|
||||
triggers.finalize!
|
||||
end
|
||||
|
||||
|
||||
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", :action) }.
|
||||
describe "#fire" do
|
||||
it "raises an error if an improper stage is given" do
|
||||
expect{ subject.fire(: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)
|
||||
expect(subject).not_to receive(:execute)
|
||||
subject.fire(: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)
|
||||
expect(subject).to receive(:execute)
|
||||
subject.fire(:up, :before, "guest", :action)
|
||||
end
|
||||
end
|
||||
|
||||
context "#filter_triggers" do
|
||||
describe "#find" do
|
||||
it "raises an error if an improper stage is given" do
|
||||
expect { subject.find(:up, :not_real, "guest", :action) }.
|
||||
to raise_error(Vagrant::Errors::TriggersNoStageGiven)
|
||||
end
|
||||
|
||||
it "returns empty array when no triggers are found" do
|
||||
expect(subject.find(:halt, :after, "guest", :action)).to be_empty
|
||||
end
|
||||
|
||||
it "returns items in array when triggers are found" do
|
||||
expect(subject.find(:halt, :before, "guest", :action)).not_to be_empty
|
||||
end
|
||||
|
||||
it "returns the execpted number of items in the array when triggers are found" do
|
||||
expect(subject.find(:halt, :before, "guest", :action).count).to eq(1)
|
||||
end
|
||||
|
||||
it "filters all found triggers" do
|
||||
expect(subject).to receive(:filter_triggers)
|
||||
subject.find(:halt, :before, "guest", :action)
|
||||
end
|
||||
|
||||
it "should not attempt to match hook name with non-hook type" do
|
||||
expect(subject).not_to receive(:matched_hook?)
|
||||
subject.find(:halt, :before, "guest", :action)
|
||||
end
|
||||
|
||||
context "with hook type" do
|
||||
before do
|
||||
triggers.before(:environment_load, hash_block.merge(type: :hook))
|
||||
triggers.before(Vagrant::Action::Builtin::SyncedFolders, hash_block.merge(type: :hook))
|
||||
triggers.finalize!
|
||||
end
|
||||
|
||||
it "returns empty array when no triggers are found" do
|
||||
expect(subject.find(:environment_unload, :before, "guest", :hook)).to be_empty
|
||||
end
|
||||
|
||||
it "returns items in array when triggers are found" do
|
||||
expect(subject.find(:environment_load, :before, "guest", :hook).size).to eq(1)
|
||||
end
|
||||
|
||||
it "should locate hook trigger using class constant" do
|
||||
expect(subject.find(Vagrant::Action::Builtin::SyncedFolders, :before, "guest", :hook)).
|
||||
not_to be_empty
|
||||
end
|
||||
|
||||
it "should locate hook trigger using string" do
|
||||
expect(subject.find("environment_load", :before, "guest", :hook)).not_to be_empty
|
||||
end
|
||||
|
||||
it "should locate hook trigger using full converted name" do
|
||||
expect(subject.find(:vagrant_action_builtin_synced_folders, :before, "guest", :hook)).
|
||||
not_to be_empty
|
||||
end
|
||||
|
||||
it "should locate hook trigger using partial suffix converted name" do
|
||||
expect(subject.find(:builtin_synced_folders, :before, "guest", :hook)).
|
||||
not_to be_empty
|
||||
end
|
||||
|
||||
it "should not locate hook trigger using partial prefix converted name" do
|
||||
expect(subject.find(:vagrant_action, :before, "guest", :hook)).
|
||||
to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#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", :action)
|
||||
@ -101,17 +169,17 @@ describe Vagrant::Plugin::V2::Trigger do
|
||||
end
|
||||
end
|
||||
|
||||
context "#fire" do
|
||||
describe "#execute" do
|
||||
it "calls the corresponding trigger methods if options set" do
|
||||
expect(subject).to receive(:info).twice
|
||||
expect(subject).to receive(:warn).once
|
||||
expect(subject).to receive(:run).twice
|
||||
expect(subject).to receive(:run_remote).once
|
||||
subject.send(:fire, triggers.before_triggers, "guest")
|
||||
subject.send(:execute, triggers.before_triggers)
|
||||
end
|
||||
end
|
||||
|
||||
context "#info" do
|
||||
describe "#info" do
|
||||
let(:message) { "Printing some info" }
|
||||
|
||||
it "prints messages at INFO" do
|
||||
@ -125,7 +193,7 @@ describe Vagrant::Plugin::V2::Trigger do
|
||||
end
|
||||
end
|
||||
|
||||
context "#warn" do
|
||||
describe "#warn" do
|
||||
let(:message) { "Printing some warnings" }
|
||||
|
||||
it "prints messages at WARN" do
|
||||
@ -139,7 +207,7 @@ describe Vagrant::Plugin::V2::Trigger do
|
||||
end
|
||||
end
|
||||
|
||||
context "#run" do
|
||||
describe "#run" do
|
||||
let(:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new }
|
||||
let(:shell_block) { {info: "hi", run: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}} }
|
||||
let(:shell_block_exit_codes) {
|
||||
@ -305,7 +373,7 @@ describe Vagrant::Plugin::V2::Trigger do
|
||||
end
|
||||
end
|
||||
|
||||
context "#run_remote" do
|
||||
describe "#run_remote" do
|
||||
let (:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new }
|
||||
let (:shell_block) { {info: "hi", run_remote: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}} }
|
||||
let (:path_block) { {warn: "bye",
|
||||
@ -412,7 +480,7 @@ describe Vagrant::Plugin::V2::Trigger do
|
||||
end
|
||||
end
|
||||
|
||||
context "#trigger_abort" do
|
||||
describe "#trigger_abort" do
|
||||
it "system exits when called" do
|
||||
allow(Process).to receive(:exit!).and_return(true)
|
||||
output = ""
|
||||
@ -456,7 +524,7 @@ describe Vagrant::Plugin::V2::Trigger do
|
||||
end
|
||||
end
|
||||
|
||||
context "#ruby" do
|
||||
describe "#ruby" do
|
||||
let(:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new }
|
||||
let(:block) { proc{var = 1+1} }
|
||||
let(:ruby_trigger) { {info: "hi", ruby: block} }
|
||||
@ -471,4 +539,18 @@ describe Vagrant::Plugin::V2::Trigger do
|
||||
subject.send(:execute_ruby, block)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#nameify" do
|
||||
it "should return empty string when object" do
|
||||
expect(subject.send(:nameify, "")).to eq("")
|
||||
end
|
||||
|
||||
it "should return name of class" do
|
||||
expect(subject.send(:nameify, String)).to eq("String")
|
||||
end
|
||||
|
||||
it "should return empty string when class has no name" do
|
||||
expect(subject.send(:nameify, Class.new)).to eq("")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user