Merge pull request #9713 from briancain/vagrant-triggers-config

Integrate vagrant-triggers plugin functionality into core Vagrant
This commit is contained in:
Brian Cain 2018-04-24 11:35:04 -07:00 committed by GitHub
commit 5643ba0c7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1634 additions and 2 deletions

View File

@ -776,6 +776,18 @@ module Vagrant
error_key(:synced_folder_unusable)
end
class TriggersGuestNotRunning < VagrantError
error_key(:triggers_guest_not_running)
end
class TriggersNoBlockGiven < VagrantError
error_key(:triggers_no_block_given)
end
class TriggersNoStageGiven < VagrantError
error_key(:triggers_no_stage_given)
end
class UIExpectsTTY < VagrantError
error_key(:ui_expects_tty)
end

View File

@ -149,6 +149,8 @@ module Vagrant
# Output a bunch of information about this machine in
# machine-readable format in case someone is listening.
@ui.machine("metadata", "provider", provider_name)
@triggers = Vagrant::Plugin::V2::Trigger.new(@env, @config.trigger, self)
end
# This calls an action on the provider. The provider may or may not
@ -159,6 +161,7 @@ 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)
@logger.info("Calling action: #{name} on provider #{@provider}")
opts ||= {}
@ -185,7 +188,7 @@ module Vagrant
locker = @env.method(:lock) if lock && !name.to_s.start_with?("ssh")
# Lock this machine for the duration of this action
locker.call("machine-action-#{id}") do
return_env = locker.call("machine-action-#{id}") do
# Get the callable from the provider.
callable = @provider.action(name)
@ -203,6 +206,10 @@ module Vagrant
ui.machine("action", name.to_s, "end")
action_result
end
@triggers.fire_triggers(name, :after, @name.to_s)
# preserve returning environment after machine action runs
return return_env
rescue Errors::EnvironmentLockedError
raise Errors::MachineActionLockedError,
action: name,

View File

@ -19,6 +19,7 @@ module Vagrant
autoload :Push, "vagrant/plugin/v2/push"
autoload :Provisioner, "vagrant/plugin/v2/provisioner"
autoload :SyncedFolder, "vagrant/plugin/v2/synced_folder"
autoload :Trigger, "vagrant/plugin/v2/trigger"
end
end
end

View File

@ -45,7 +45,7 @@ module Vagrant
def parse_options(opts=nil)
# make sure optparse doesn't use POSIXLY_CORRECT parsing
ENV["POSIXLY_CORRECT"] = nil
# Creating a shallow copy of the arguments so the OptionParser
# doesn't destroy the originals.
argv = @argv.dup

View File

@ -0,0 +1,242 @@
require 'fileutils'
require 'log4r'
require 'shellwords'
require Vagrant.source_root.join("plugins/provisioners/shell/provisioner")
require "vagrant/util/subprocess"
require "vagrant/util/platform"
require "vagrant/util/powershell"
module Vagrant
module Plugin
module V2
class Trigger
# @return [Kernel_V2::Config::Trigger]
attr_reader :config
# This class is responsible for setting up basic triggers that were
# defined inside a Vagrantfile.
#
# @param [Vagrant::Environment] env Vagrant environment
# @param [Kernel_V2::TriggerConfig] config Trigger configuration
# @param [Vagrant::Machine] machine Active Machine
def initialize(env, config, machine)
@env = env
@config = config
@machine = machine
@logger = Log4r::Logger.new("vagrant::trigger::#{self.class.to_s.downcase}")
end
# Fires all triggers, if any are defined for the action and guest
#
# @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)
# get all triggers matching action
triggers = []
if stage == :before
triggers = config.before_triggers.select do |t|
t.command == action || (t.command == :all && !t.ignore.include?(action))
end
elsif stage == :after
triggers = config.after_triggers.select do |t|
t.command == action || (t.command == :all && !t.ignore.include?(action))
end
else
raise Errors::TriggersNoStageGiven,
action: action,
stage: stage,
guest_name: guest_name
end
triggers = filter_triggers(triggers, guest_name)
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))
fire(triggers, guest_name)
end
end
protected
#-------------------------------------------------------------------
# Internal methods, don't call these.
#-------------------------------------------------------------------
# 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
# @return [Array] The filtered array of triggers
def filter_triggers(triggers, guest_name)
# 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
filter.each do |trigger|
index = nil
match = false
if trigger.only_on
trigger.only_on.each do |o|
if o.match(guest_name)
# trigger matches on current guest, so we're fine to use it
match = true
break
end
end
# no matches found, so don't use trigger for guest
index = triggers.index(trigger) unless match == true
end
if index
@logger.debug("Trigger #{trigger.id} will be ignored for #{guest_name}")
triggers.delete_at(index)
end
end
return triggers
end
# Fires off all triggers in the given array
#
# @param [Array] triggers An array of triggers to be fired
def fire(triggers, guest_name)
# ensure on_error is respected by exiting or continuing
triggers.each do |trigger|
@logger.debug("Running trigger #{trigger.id}...")
if trigger.name
@machine.ui.info(I18n.t("vagrant.trigger.fire_with_name",
name: trigger.name))
else
@machine.ui.info(I18n.t("vagrant.trigger.fire"))
end
if trigger.info
info(trigger.info)
end
if trigger.warn
warn(trigger.warn)
end
if trigger.run
run(trigger.run, trigger.on_error)
end
if trigger.run_remote
run_remote(trigger.run_remote, trigger.on_error)
end
end
end
# Prints the given message at info level for a trigger
#
# @param [String] message The string to be printed
def info(message)
@machine.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)
end
# Runs a script on a guest
#
# @param [Provisioners::Shell::Config] config A Shell provisioner config
def run(config, on_error)
if config.inline
cmd = Shellwords.split(config.inline)
@machine.ui.detail(I18n.t("vagrant.trigger.run.inline", command: config.inline))
else
cmd = File.expand_path(config.path, @env.root_path)
cmd << " #{config.args.join(' ' )}" if config.args
cmd = Shellwords.split(cmd)
@machine.ui.detail(I18n.t("vagrant.trigger.run.script", path: config.path))
end
# Pick an execution method to run the script or inline string with
# Default to Subprocess::Execute
exec_method = Vagrant::Util::Subprocess.method(:execute)
if Vagrant::Util::Platform.windows?
if config.inline
exec_method = Vagrant::Util::PowerShell.method(:execute_inline)
else
exec_method = Vagrant::Util::PowerShell.method(:execute)
end
end
begin
result = exec_method.call(*cmd, :notify => [:stdout, :stderr]) do |type,data|
options = {}
case type
when :stdout
options[:color] = :green if !config.keep_color
when :stderr
options[:color] = :red if !config.keep_color
end
@machine.ui.detail(data, options)
end
rescue => e
@machine.ui.error(I18n.t("vagrant.errors.triggers_run_fail"))
@machine.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"))
end
end
end
# Runs a script on the guest
#
# @param [ShellProvisioner/Config] config A Shell provisioner config
def run_remote(config, on_error)
unless @machine.state.id == :running
if on_error == :halt
raise Errors::TriggersGuestNotRunning,
machine_name: @machine.name,
state: @machine.state.id
else
@machine.ui.error(I18n.t("vagrant.errors.triggers_guest_not_running",
machine_name: @machine.name,
state: @machine.state.id))
@machine.ui.warn(I18n.t("vagrant.trigger.on_error_continue"))
return
end
end
prov = VagrantPlugins::Shell::Provisioner.new(@machine, config)
begin
prov.provision
rescue => e
@machine.ui.error(I18n.t("vagrant.errors.triggers_run_fail"))
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.error(e.message)
end
end
end
end
end
end
end

View File

@ -87,6 +87,27 @@ module Vagrant
return r.stdout.chomp
end
# Execute a powershell command and return a result
#
# @param [String] command PowerShell command to execute.
# @param [Hash] opts A collection of options for subprocess::execute
# @param [Block] block Ruby block
def self.execute_inline(*command, **opts, &block)
validate_install!
c = [
executable,
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy", "Bypass",
"-Command",
command
].flatten.compact
c << opts
Subprocess.execute(*c, &block)
end
# Returns the version of PowerShell that is installed.
#
# @return [String]

View File

@ -0,0 +1,201 @@
require "vagrant"
require File.expand_path("../vm_trigger", __FILE__)
module VagrantPlugins
module Kernel_V2
class TriggerConfig < Vagrant.plugin("2", :config)
# The TriggerConfig class is what gets called when a user
# defines a new trigger in their Vagrantfile. The two entry points are
# either `config.trigger.before` or `config.trigger.after`.
def initialize
@logger = Log4r::Logger.new("vagrant::config::trigger")
# Internal State
@_before_triggers = [] # An array of VagrantConfigTrigger objects
@_after_triggers = [] # An array of VagrantConfigTrigger objects
end
#-------------------------------------------------------------------
# Trigger before/after functions
#-------------------------------------------------------------------
#
# Commands are expected to be ether:
# - splat
# + config.trigger.before :up, :destroy, :halt do |trigger|....
# - array
# + config.trigger.before [:up, :destroy, :halt] do |trigger|....
#
# Config is expected to be given as a block, or the last parameter as a hash
#
# - block
# + config.trigger.before :up, :destroy, :halt do |trigger|
# trigger.option = "option"
# end
# - hash
# + config.trigger.before :up, :destroy, :halt, options: "option"
# Reads in and parses Vagrant command whitelist and settings for a defined
# trigger
#
# @param [Symbol] command Vagrant command to create trigger on
# @param [Block] block The defined before block
def before(*command, &block)
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
elsif !block_given?
raise Vagrant::Errors::TriggersNoBlockGiven,
command: command
end
command.each do |cmd|
trigger = create_trigger(cmd, blk)
@_before_triggers << trigger
end
end
# Reads in and parses Vagrant command whitelist and settings for a defined
# trigger
#
# @param [Symbol] command Vagrant command to create trigger on
# @param [Block] block The defined after block
def after(*command, &block)
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
elsif !block_given?
raise Vagrant::Errors::TriggersNoBlockGiven,
command: command
end
command.each do |cmd|
trigger = create_trigger(cmd, blk)
@_after_triggers << trigger
end
end
#-------------------------------------------------------------------
# Internal methods, don't call these.
#-------------------------------------------------------------------
# Creates a new trigger config. If a block is given, parse that block
# by calling it with the created trigger. Otherwise set the options if it's
# a hash.
#
# @param [Symbol] command Vagrant command to create trigger on
# @param [Block] block The defined config block
# @return [VagrantConfigTrigger]
def create_trigger(command, block)
trigger = VagrantConfigTrigger.new(command)
if block.is_a?(Hash)
trigger.set_options(block)
else
block.call(trigger, VagrantConfigTrigger)
end
return trigger
end
def merge(other)
super.tap do |result|
new_before_triggers = []
new_after_triggers = []
other_defined_before_triggers = other.instance_variable_get(:@_before_triggers)
other_defined_after_triggers = other.instance_variable_get(:@_after_triggers)
@_before_triggers.each do |bt|
other_bft = other_defined_before_triggers.find { |o| bt.id == o.id }
if other_bft
# Override, take it
other_bft = bt.merge(other_bft)
# Preserve order, always
bt = other_bft
other_defined_before_triggers.delete(other_bft)
end
new_before_triggers << bt.dup
end
other_defined_before_triggers.each do |obt|
new_before_triggers << obt.dup
end
result.instance_variable_set(:@_before_triggers, new_before_triggers)
@_after_triggers.each do |at|
other_aft = other_defined_after_triggers.find { |o| at.id == o.id }
if other_aft
# Override, take it
other_aft = at.merge(other_aft)
# Preserve order, always
at = other_aft
other_defined_after_triggers.delete(other_aft)
end
new_after_triggers << at.dup
end
other_defined_after_triggers.each do |oat|
new_after_triggers << oat.dup
end
result.instance_variable_set(:@_after_triggers, new_after_triggers)
end
end
# Iterates over all defined triggers and finalizes their config objects
def finalize!
if !@_before_triggers.empty?
@_before_triggers.map { |t| t.finalize! }
end
if !@_after_triggers.empty?
@_after_triggers.map { |t| t.finalize! }
end
end
# Validate Trigger Arrays
def validate(machine)
errors = _detected_errors
@_before_triggers.each do |bt|
error = bt.validate(machine)
errors.concat error if !error.empty?
end
@_after_triggers.each do |at|
error = at.validate(machine)
errors.concat error if !error.empty?
end
{"trigger" => errors}
end
# return [Array]
def before_triggers
@_before_triggers
end
# return [Array]
def after_triggers
@_after_triggers
end
# The String representation of this Trigger.
#
# @return [String]
def to_s
"trigger"
end
end
end
end

View File

@ -0,0 +1,204 @@
require 'log4r'
require Vagrant.source_root.join('plugins/provisioners/shell/config')
module VagrantPlugins
module Kernel_V2
# Represents a single configured provisioner for a VM.
class VagrantConfigTrigger < Vagrant.plugin("2", :config)
# Defaults
DEFAULT_ON_ERROR = :halt
#-------------------------------------------------------------------
# Config class for a given Trigger
#-------------------------------------------------------------------
# Internal unique name for this trigger
#
# Note: This is for internal use only.
#
# @return [String]
attr_reader :id
# Name for the given Trigger. Defaults to nil.
#
# @return [String]
attr_accessor :name
# Command to fire the trigger on
#
# @return [Symbol]
attr_reader :command
# A string to print at the WARN level
#
# @return [String]
attr_accessor :info
# A string to print at the WARN level
#
# @return [String]
attr_accessor :warn
# Determines what how a Trigger should behave if it runs into an error.
# Defaults to :halt, otherwise can only be set to :continue.
#
# @return [Symbol]
attr_accessor :on_error
# If set, will not run trigger for the configured Vagrant commands.
#
# @return [Symbol, Array]
attr_accessor :ignore
# If set, will only run trigger for guests that match keys for this parameter.
#
# @return [String, Regex, Array]
attr_accessor :only_on
# A local inline or file script to execute for the trigger
#
# @return [Hash]
attr_accessor :run
# A remote inline or file script to execute for the trigger
#
# @return [Hash]
attr_accessor :run_remote
def initialize(command)
@logger = Log4r::Logger.new("vagrant::config::vm::trigger::config")
@name = UNSET_VALUE
@info = UNSET_VALUE
@warn = UNSET_VALUE
@on_error = UNSET_VALUE
@ignore = UNSET_VALUE
@only_on = UNSET_VALUE
@run = UNSET_VALUE
@run_remote = UNSET_VALUE
# Internal options
@id = SecureRandom.uuid
@command = command.to_sym
@logger.debug("Trigger defined for command: #{command}")
end
def finalize!
# Ensure all config options are set to nil or default value if untouched
# by user
@name = nil if @name == UNSET_VALUE
@info = nil if @info == UNSET_VALUE
@warn = nil if @warn == UNSET_VALUE
@on_error = DEFAULT_ON_ERROR if @on_error == UNSET_VALUE
@ignore = [] if @ignore == UNSET_VALUE
@run = nil if @run == UNSET_VALUE
@run_remote = nil if @run_remote == UNSET_VALUE
@only_on = nil if @only_on == UNSET_VALUE
# these values are expected to always be an Array internally,
# but can be set as a single String or Symbol
#
# Guests are stored internally as strings
if @only_on
@only_on = Array(@only_on)
end
# Commands must be stored internally as symbols
if @ignore
@ignore = Array(@ignore)
@ignore.map! { |i| i.to_sym }
end
# Convert @run and @run_remote to be a "Shell provisioner" config
if @run && @run.is_a?(Hash)
# Powershell args and privileged for run commands is currently not supported
# so by default use empty string or false if unset. This helps the validate
# function determine if the setting was purposefully set, to print a warning
if !@run.key?(:powershell_args)
@run[:powershell_args] = ""
end
if !@run.key?(:privileged)
@run[:privileged] = false
end
new_run = VagrantPlugins::Shell::Config.new
new_run.set_options(@run)
new_run.finalize!
@run = new_run
end
if @run_remote && @run_remote.is_a?(Hash)
new_run = VagrantPlugins::Shell::Config.new
new_run.set_options(@run_remote)
new_run.finalize!
@run_remote = new_run
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)
end
if !commands.include?(@command) && @command != :all
machine.ui.warn(I18n.t("vagrant.config.triggers.bad_command_warning",
cmd: @command))
end
if @run
errorz = @run.validate(machine)
errors.concat errorz["shell provisioner"] if !errorz.empty?
if @run.privileged == true
machine.ui.warn(I18n.t("vagrant.config.triggers.privileged_ignored",
command: @command))
end
if @run.powershell_args != ""
machine.ui.warn(I18n.t("vagrant.config.triggers.powershell_args_ignored"))
end
end
if @run_remote
errorz = @run_remote.validate(machine)
errors.concat errorz["shell provisioner"] if !errorz.empty?
end
if @name && !@name.is_a?(String)
errors << I18n.t("vagrant.config.triggers.name_bad_type", cmd: @command)
end
if @info && !@info.is_a?(String)
errors << I18n.t("vagrant.config.triggers.info_bad_type", cmd: @command)
end
if @warn && !@warn.is_a?(String)
errors << I18n.t("vagrant.config.triggers.warn_bad_type", cmd: @command)
end
if @on_error != :halt
if @on_error != :continue
errors << I18n.t("vagrant.config.triggers.on_error_bad_type", cmd: @command)
end
end
errors
end
# The String representation of this Trigger.
#
# @return [String]
def to_s
"trigger config"
end
end
end
end

View File

@ -39,6 +39,11 @@ module VagrantPlugins
require File.expand_path("../config/vm", __FILE__)
VMConfig
end
config("trigger") do
require File.expand_path("../config/trigger", __FILE__)
TriggerConfig
end
end
end
end

View File

@ -279,6 +279,24 @@ en:
up some disk space.
Press the Enter or Return key to continue.
trigger:
on_error_continue: |-
Trigger configured to continue on error...
start: |-
Running triggers %{stage} %{action} ...
fire_with_name: |-
Running trigger: %{name}...
fire: |-
Running trigger...
run:
inline: |-
Running local: Inline script
%{command}
script: |-
Running local script: %{path}
version_current: |-
Installed Version: %{version}
version_latest: |-
@ -1392,6 +1410,22 @@ en:
The synced folder type '%{type}' is reporting as unusable for
your current setup. Please verify you have all the proper
prerequisites for using this shared folder type and try again.
triggers_run_fail: |-
Trigger run failed
triggers_guest_not_running: |-
Could not run remote script on %{machine_name} because its state is %{state}
triggers_no_block_given: |-
There was an error parsing the Vagrantfile:
No config was given for the trigger(s) %{command}.
triggers_no_stage_given: |-
The incorrect stage was given to the trigger plugin:
Guest: %{guest_name}
Action: %{action}
Stage: %{stage}
This is an internal error that should be reported as a bug.
ui_expects_tty: |-
Vagrant is attempting to interface with the UI in a way that requires
a TTY. Most actions in Vagrant that require a TTY have configuration
@ -1677,6 +1711,33 @@ en:
paranoid_deprecated: |-
The key `paranoid` is deprecated. Please use `verify_host_key`. Supported
values are exactly the same, only the name of the option has changed.
triggers:
bad_command_warning: |-
The command '%{cmd}' was not found for this trigger.
name_bad_type: |-
Invalid type set for `name` on trigger for command '%{cmd}'. `name` should be a String.
info_bad_type: |-
Invalid type set for `info` on trigger for command '%{cmd}'. `info` should be a String.
warn_bad_type: |-
Invalid type set for `warn` on trigger for command '%{cmd}'. `warn` should be a String.
on_error_bad_type: |-
Invalid type set for `on_error` on trigger for command '%{cmd}'. `on_error` can
only be `:halt` (default) or `:continue`.
only_on_bad_type: |-
Invalid type found for `only_on`. All values must be a `String` or `Regexp`.
privileged_ignored: |-
The `privileged` setting for option `run` for trigger command '%{command}' will be ignored and set to false.
powershell_args_ignored: |-
The setting `powershell_args` is not supported for the trigger option `run` and will be ignored.
run:
bad_type: |-
Invalid type set for `run` on trigger for command '%{cmd}'. `run`
must be a Hash.
run_remote:
bad_type: |-
Invalid type set for `run` on trigger for command '%{cmd}'. `run`
must be a Hash.
vm:
bad_version: |-
Invalid box version constraints: %{version}

View File

@ -0,0 +1,194 @@
require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/kernel_v2/config/trigger")
describe VagrantPlugins::Kernel_V2::TriggerConfig do
include_context "unit"
subject { described_class.new }
let(:machine) { double("machine") }
def assert_invalid
errors = subject.validate(machine)
if !errors.values.any? { |v| !v.empty? }
raise "No errors: #{errors.inspect}"
end
end
def assert_valid
errors = subject.validate(machine)
if !errors.values.all? { |v| v.empty? }
raise "Errors: #{errors.inspect}"
end
end
before do
env = double("env")
allow(env).to receive(:root_path).and_return(nil)
allow(machine).to receive(:env).and_return(env)
allow(machine).to receive(:provider_config).and_return(nil)
allow(machine).to receive(:provider_options).and_return({})
end
it "is valid with test defaults" do
subject.finalize!
assert_valid
end
let (:hash_block) { {info: "hi", run: {inline: "echo 'hi'"}} }
let (:splat) { [:up, :destroy, :halt] }
let (:arr) { [[:up, :destroy, :halt]] }
describe "creating a before trigger" do
it "creates a trigger with the splat syntax" do
subject.before(:up, hash_block)
bf_trigger = subject.instance_variable_get(:@_before_triggers)
expect(bf_trigger.size).to eq(1)
expect(bf_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger)
end
it "creates a trigger with the array syntax" do
subject.before([:up], hash_block)
bf_trigger = subject.instance_variable_get(:@_before_triggers)
expect(bf_trigger.size).to eq(1)
expect(bf_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger)
end
it "creates a trigger with the block syntax" do
subject.before :up do |trigger|
trigger.name = "rspec"
end
bf_trigger = subject.instance_variable_get(:@_before_triggers)
expect(bf_trigger.size).to eq(1)
expect(bf_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger)
end
it "creates multiple triggers with the splat syntax" do
subject.before(splat, hash_block)
bf_trigger = subject.instance_variable_get(:@_before_triggers)
expect(bf_trigger.size).to eq(3)
bf_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) }
end
it "creates multiple triggers with the block syntax" do
subject.before splat do |trigger|
trigger.name = "rspec"
end
bf_trigger = subject.instance_variable_get(:@_before_triggers)
expect(bf_trigger.size).to eq(3)
bf_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) }
end
it "creates multiple triggers with the array syntax" do
subject.before(arr, hash_block)
bf_trigger = subject.instance_variable_get(:@_before_triggers)
expect(bf_trigger.size).to eq(3)
bf_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) }
end
end
describe "creating an after trigger" do
it "creates a trigger with the splat syntax" do
subject.after(:up, hash_block)
af_trigger = subject.instance_variable_get(:@_after_triggers)
expect(af_trigger.size).to eq(1)
expect(af_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger)
end
it "creates a trigger with the array syntax" do
subject.after([:up], hash_block)
af_trigger = subject.instance_variable_get(:@_after_triggers)
expect(af_trigger.size).to eq(1)
expect(af_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger)
end
it "creates a trigger with the block syntax" do
subject.after :up do |trigger|
trigger.name = "rspec"
end
af_trigger = subject.instance_variable_get(:@_after_triggers)
expect(af_trigger.size).to eq(1)
expect(af_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger)
end
it "creates multiple triggers with the splat syntax" do
subject.after(splat, hash_block)
af_trigger = subject.instance_variable_get(:@_after_triggers)
expect(af_trigger.size).to eq(3)
af_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) }
end
it "creates multiple triggers with the block syntax" do
subject.after splat do |trigger|
trigger.name = "rspec"
end
af_trigger = subject.instance_variable_get(:@_after_triggers)
expect(af_trigger.size).to eq(3)
af_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) }
end
it "creates multiple triggers with the array syntax" do
subject.after(arr, hash_block)
af_trigger = subject.instance_variable_get(:@_after_triggers)
expect(af_trigger.size).to eq(3)
af_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) }
end
end
describe "#create_trigger" do
let(:command) { :up }
let(:hash_block) { {info: "hi", run: {inline: "echo 'hi'"}} }
it "returns a new VagrantConfigTrigger object if given a hash" do
trigger = subject.create_trigger(command, hash_block)
expect(trigger).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger)
end
it "returns a new VagrantConfigTrigger object if given a block" do
block = Proc.new { |b| b.info = "test"}
trigger = subject.create_trigger(command, block)
expect(trigger).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger)
end
end
describe "#merge" do
it "merges defined triggers" do
a = described_class.new()
b = described_class.new()
a.before(splat, hash_block)
a.after(arr, hash_block)
b.before(splat, hash_block)
b.after(arr, hash_block)
result = a.merge(b)
bf_trigger = result.instance_variable_get(:@_before_triggers)
af_trigger = result.instance_variable_get(:@_after_triggers)
expect(bf_trigger).to be_a(Array)
expect(af_trigger).to be_a(Array)
expect(bf_trigger.size).to eq(6)
expect(af_trigger.size).to eq(6)
end
it "merges the other triggers if a class is empty" do
a = described_class.new()
b = described_class.new()
a.before(splat, hash_block)
a.after(arr, hash_block)
b_bf_trigger = b.instance_variable_get(:@_before_triggers)
b_af_trigger = b.instance_variable_get(:@_after_triggers)
result = a.merge(b)
bf_trigger = result.instance_variable_get(:@_before_triggers)
af_trigger = result.instance_variable_get(:@_after_triggers)
expect(bf_trigger.size).to eq(3)
expect(af_trigger.size).to eq(3)
end
end
end

View File

@ -0,0 +1,124 @@
require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/kernel_v2/config/vm_trigger")
describe VagrantPlugins::Kernel_V2::VagrantConfigTrigger do
include_context "unit"
let(:command) { :up }
subject { described_class.new(command) }
let(:machine) { double("machine") }
def assert_invalid
errors = subject.validate(machine)
if !errors.empty? { |v| !v.empty? }
raise "No errors: #{errors.inspect}"
end
end
def assert_valid
errors = subject.validate(machine)
if !errors.empty? { |v| v.empty? }
raise "Errors: #{errors.inspect}"
end
end
before do
env = double("env")
allow(env).to receive(:root_path).and_return(nil)
allow(machine).to receive(:env).and_return(env)
allow(machine).to receive(:provider_config).and_return(nil)
allow(machine).to receive(:provider_options).and_return({})
subject.name = "foo"
subject.info = "Hello there"
subject.warn = "Warning!!"
subject.ignore = :up
subject.only_on = "guest"
subject.run = {inline: "apt-get update"}
subject.run_remote = {inline: "apt-get update", env: {"VAR"=>"VAL"}}
end
describe "with defaults" do
it "is valid with test defaults" do
subject.finalize!
assert_valid
end
it "sets a command" do
subject.finalize!
expect(subject.command).to eq(command)
end
it "uses default error behavior" do
subject.finalize!
expect(subject.on_error).to eq(:halt)
end
end
describe "defining a new config that needs to match internal restraints" do
let(:cmd) { :destroy }
let(:cfg) { described_class.new(cmd) }
let(:arr_cfg) { described_class.new(cmd) }
before do
cfg.only_on = :guest
cfg.ignore = "up"
arr_cfg.only_on = ["guest", /other/]
arr_cfg.ignore = ["up", "destroy"]
end
it "ensures only_on is an array" do
cfg.finalize!
arr_cfg.finalize!
expect(cfg.only_on).to be_a(Array)
expect(arr_cfg.only_on).to be_a(Array)
end
it "ensures ignore is an array of symbols" do
cfg.finalize!
arr_cfg.finalize!
expect(cfg.ignore).to be_a(Array)
expect(arr_cfg.ignore).to be_a(Array)
cfg.ignore.each do |a|
expect(a).to be_a(Symbol)
end
arr_cfg.ignore.each do |a|
expect(a).to be_a(Symbol)
end
end
end
describe "defining a basic trigger config" do
let(:cmd) { :up }
let(:cfg) { described_class.new(cmd) }
before do
cfg.info = "Hello there"
cfg.warn = "Warning!!"
cfg.on_error = :continue
cfg.ignore = :up
cfg.only_on = "guest"
cfg.run = {inline: "apt-get update"}
cfg.run_remote = {inline: "apt-get update", env: {"VAR"=>"VAL"}}
end
it "sets the options" do
cfg.finalize!
expect(cfg.info).to eq("Hello there")
expect(cfg.warn).to eq("Warning!!")
expect(cfg.on_error).to eq(:continue)
expect(cfg.ignore).to eq([:up])
expect(cfg.only_on).to eq(["guest"])
expect(cfg.run).to be_a(VagrantPlugins::Shell::Config)
expect(cfg.run_remote).to be_a(VagrantPlugins::Shell::Config)
end
end
end

View File

@ -0,0 +1,319 @@
require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/kernel_v2/config/trigger")
describe Vagrant::Plugin::V2::Trigger do
include_context "unit"
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
isolated_environment.tap do |env|
env.vagrantfile("")
end
end
let(:iso_vagrant_env) { iso_env.create_vagrant_env }
let(:state) { double("state", id: :running) }
let(:machine) do
iso_vagrant_env.machine(iso_vagrant_env.machine_names[0], :dummy).tap do |m|
allow(m).to receive(:state).and_return(state)
end
end
let(:env) { {
machine: machine,
ui: Vagrant::UI::Silent.new,
} }
let(:triggers) { VagrantPlugins::Kernel_V2::TriggerConfig.new }
let(:hash_block) { {info: "hi", run: {inline: "echo 'hi'"}} }
let(:hash_block_two) { {warn: "WARNING!!", run_remote: {inline: "echo 'hi'"}} }
before do
triggers.before(:up, hash_block)
triggers.before(:destroy, hash_block)
triggers.before(:halt, hash_block_two)
triggers.after(:up, hash_block)
triggers.after(:destroy, hash_block)
triggers.finalize!
end
let(:subject) { described_class.new(env, triggers, machine) }
context "#fire_triggers" do
it "raises an error if an inproper stage is given" do
expect{ subject.fire_triggers(:up, :not_real, "guest") }.
to raise_error(Vagrant::Errors::TriggersNoStageGiven)
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")
expect(filtered_triggers).to eq(before_triggers)
end
it "filters a trigger if it doesn't match guest_name" do
trigger_config = {info: "no", only_on: "notrealguest"}
triggers.after(:up, trigger_config)
triggers.finalize!
after_triggers = triggers.after_triggers
expect(after_triggers.size).to eq(3)
subject.send(:filter_triggers, after_triggers, "ubuntu")
expect(after_triggers.size).to eq(2)
end
it "keeps a trigger that has a restraint that matches guest name" do
trigger_config = {info: "no", only_on: /guest/}
triggers.after(:up, trigger_config)
triggers.finalize!
after_triggers = triggers.after_triggers
expect(after_triggers.size).to eq(3)
subject.send(:filter_triggers, after_triggers, "ubuntu-guest")
expect(after_triggers.size).to eq(3)
end
it "keeps a trigger that has multiple restraints that matches guest name" do
trigger_config = {info: "no", only_on: ["debian", /guest/]}
triggers.after(:up, trigger_config)
triggers.finalize!
after_triggers = triggers.after_triggers
expect(after_triggers.size).to eq(3)
subject.send(:filter_triggers, after_triggers, "ubuntu-guest")
expect(after_triggers.size).to eq(3)
end
end
context "#fire" 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")
end
end
context "#info" do
let(:message) { "Printing some info" }
it "prints messages at INFO" do
output = ""
allow(machine.ui).to receive(:info) do |data|
output << data
end
subject.send(:info, message)
expect(output).to include(message)
end
end
context "#warn" do
let(:message) { "Printing some warnings" }
it "prints messages at WARN" do
output = ""
allow(machine.ui).to receive(:warn) do |data|
output << data
end
subject.send(:warn, message)
expect(output).to include(message)
end
end
context "#run" do
let(:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new }
let(:shell_block) { {info: "hi", run: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}} }
let(:path_block) { {warn: "bye",
run: {path: "script.sh", env: {"KEY"=>"VALUE"}},
on_error: :continue} }
let(:path_block_ps1) { {warn: "bye",
run: {path: "script.ps1", env: {"KEY"=>"VALUE"}},
on_error: :continue} }
let(:exit_code) { 0 }
let(:options) { {:notify=>[:stdout, :stderr]} }
let(:subprocess_result) do
double("subprocess_result").tap do |result|
allow(result).to receive(:exit_code).and_return(exit_code)
allow(result).to receive(:stderr).and_return("")
end
end
before do
trigger_run.after(:up, shell_block)
trigger_run.before(:destroy, path_block)
trigger_run.before(:destroy, path_block_ps1)
trigger_run.finalize!
end
it "executes an inline script with powershell if windows" do
allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true)
allow(Vagrant::Util::PowerShell).to receive(:execute_inline).
and_return(subprocess_result)
trigger = trigger_run.after_triggers.first
shell_config = trigger.run
on_error = trigger.on_error
expect(Vagrant::Util::PowerShell).to receive(:execute_inline).
with("echo", "hi", options)
subject.send(:run, shell_config, on_error)
end
it "executes an path script with powershell if windows" do
allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true)
allow(Vagrant::Util::PowerShell).to receive(:execute).
and_return(subprocess_result)
allow(env).to receive(:root_path).and_return("/vagrant/home")
trigger = trigger_run.before_triggers[1]
shell_config = trigger.run
on_error = trigger.on_error
expect(Vagrant::Util::PowerShell).to receive(:execute).
with("/vagrant/home/script.ps1", options)
subject.send(:run, shell_config, on_error)
end
it "executes an inline script" do
allow(Vagrant::Util::Subprocess).to receive(:execute).
and_return(subprocess_result)
trigger = trigger_run.after_triggers.first
shell_config = trigger.run
on_error = trigger.on_error
expect(Vagrant::Util::Subprocess).to receive(:execute).
with("echo", "hi", options)
subject.send(:run, shell_config, on_error)
end
it "executes an path script" do
allow(Vagrant::Util::Subprocess).to receive(:execute).
and_return(subprocess_result)
allow(env).to receive(:root_path).and_return("/vagrant/home")
allow(FileUtils).to receive(:chmod).and_return(true)
trigger = trigger_run.before_triggers.first
shell_config = trigger.run
on_error = trigger.on_error
expect(Vagrant::Util::Subprocess).to receive(:execute).
with("/vagrant/home/script.sh", options)
subject.send(:run, shell_config, on_error)
end
it "continues on error" do
allow(Vagrant::Util::Subprocess).to receive(:execute).
and_raise("Fail!")
allow(env).to receive(:root_path).and_return("/vagrant/home")
allow(FileUtils).to receive(:chmod).and_return(true)
trigger = trigger_run.before_triggers.first
shell_config = trigger.run
on_error = trigger.on_error
expect(Vagrant::Util::Subprocess).to receive(:execute).
with("/vagrant/home/script.sh", options)
subject.send(:run, shell_config, on_error)
end
it "halts on error" do
allow(Vagrant::Util::Subprocess).to receive(:execute).
and_raise("Fail!")
trigger = trigger_run.after_triggers.first
shell_config = trigger.run
on_error = trigger.on_error
expect(Vagrant::Util::Subprocess).to receive(:execute).
with("echo", "hi", options)
expect { subject.send(:run, shell_config, on_error) }.to raise_error("Fail!")
end
end
context "#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",
run_remote: {path: "script.sh", env: {"KEY"=>"VALUE"}},
on_error: :continue} }
let(:provision) { double("provision") }
before do
trigger_run.after(:up, shell_block)
trigger_run.before(:destroy, path_block)
trigger_run.finalize!
end
it "raises an error and halts if guest is not running" do
allow(machine.state).to receive(:id).and_return(:not_running)
trigger = trigger_run.after_triggers.first
shell_config = trigger.run_remote
on_error = trigger.on_error
expect { subject.send(:run_remote, shell_config, on_error) }.
to raise_error(Vagrant::Errors::TriggersGuestNotRunning)
end
it "continues on if guest is not running but is configured to continue on error" do
allow(machine.state).to receive(:id).and_return(:not_running)
allow(env).to receive(:root_path).and_return("/vagrant/home")
allow(FileUtils).to receive(:chmod).and_return(true)
trigger = trigger_run.before_triggers.first
shell_config = trigger.run_remote
on_error = trigger.on_error
subject.send(:run_remote, shell_config, on_error)
end
it "calls the provision function on the shell provisioner" do
allow(machine.state).to receive(:id).and_return(:running)
allow(provision).to receive(:provision).and_return("Provision!")
allow(VagrantPlugins::Shell::Provisioner).to receive(:new).
and_return(provision)
trigger = trigger_run.after_triggers.first
shell_config = trigger.run_remote
on_error = trigger.on_error
subject.send(:run_remote, shell_config, on_error)
end
it "continues on if provision fails" do
allow(machine.state).to receive(:id).and_return(:running)
allow(provision).to receive(:provision).and_raise("Nope!")
allow(VagrantPlugins::Shell::Provisioner).to receive(:new).
and_return(provision)
trigger = trigger_run.before_triggers.first
shell_config = trigger.run_remote
on_error = trigger.on_error
subject.send(:run_remote, shell_config, on_error)
end
it "fails if it encounters an error" do
allow(machine.state).to receive(:id).and_return(:running)
allow(provision).to receive(:provision).and_raise("Nope!")
allow(VagrantPlugins::Shell::Provisioner).to receive(:new).
and_return(provision)
trigger = trigger_run.after_triggers.first
shell_config = trigger.run_remote
on_error = trigger.on_error
expect { subject.send(:run_remote, shell_config, on_error) }.
to raise_error("Nope!")
end
end
end

View File

@ -0,0 +1,43 @@
---
layout: "docs"
page_title: "Vagrant Triggers Configuration"
sidebar_current: "triggers-configuration"
description: |-
Documentation of various configuration options for Vagrant Triggers
---
# Configuration
Vagrant Triggers has a few options to define trigger behavior.
## Options
The trigger class takes various options.
* `action` (symbol, array) - Expected to be a single symbol value, an array of symbols, or a _splat_ of symbols. The first argument that comes after either __before__ or __after__ when defining a new trigger. Can be any valid Vagrant command. It also accepts a special value `:all` which will make the trigger fire for every action. An action can be ignored with the `ignore` setting if desired. These are the valid action commands for triggers:
- `destroy`
- `halt`
- `provision`
- `reload`
- `resume`
- `up`
* `ignore` (symbol, array) - Symbol or array of symbols corresponding to the action that a trigger should not fire on.
* `info` (string) - A message that will be printed at the beginning of a trigger.
* `name` (string) - The name of the trigger. If set, the name will be displayed when firing the trigger.
* `on_error` (symbol) - Defines how the trigger should behave if it encounters an error. By default this will be `:halt`, but can be configured to ignore failures and continue on with `:continue`.
* `only_on` (string, regex, array) - Guest or guests to be ignored on the defined trigger. Values can be a string or regex that matches a guest name.
* `run_remote` (hash) - A collection of settings to run a inline or remote script with on the guest. These settings correspond to the [shell provosioner](/docs/provisioning/shell.html).
* `run` (hash) - A collection of settings to run a inline or remote script with on the host. These settings correspond to the [shell provosioner](/docs/provisioning/shell.html). However, at the moment the only settings `run` takes advantage of are:
+ `args`
+ `inline`
+ `path`
* `warn` (string) - A warning message that will be printed at the beginning of a trigger.

View File

@ -0,0 +1,95 @@
---
layout: "docs"
page_title: "Vagrant Triggers"
sidebar_current: "triggers"
description: |-
Introduction to Vagrant Triggers
---
# Vagrant Triggers
As of version 2.1.0, Vagrant is capable of executing machine triggers _before_ or
_after_ Vagrant commands.
Each trigger is expected to be given a command key for when it should be fired
during the Vagrant command lifecycle. These could be defined as a single key or
an array which acts like a _whitelist_ for the defined trigger.
```ruby
# single command trigger
config.trigger.after :up do |trigger|
...
end
# multiple commands for this trigger
config.trigger.before [:up, :destroy, :halt, :package] do |trigger|
...
end
# or defined as a splat list
config.trigger.before :up, :destroy, :halt, :package do |trigger|
...
end
```
Alternatively, the key `:all` could be given which would run the trigger before
or after every Vagrant command. If there is a command you don't want the trigger
to run on, you can ignore that command with the `ignore` option.
```ruby
# single command trigger
config.trigger.before :all do |trigger|
trigger.info = "Running a before trigger!"
trigger.ignore = [:destroy, :halt]
end
```
__Note:__ _If a trigger is defined on a command that does not exist, a warning
will be displayed._
Triggers can be defined as a block or hash in a Vagrantfile. The example below
will result in the same trigger:
```ruby
config.trigger.after :up do |trigger|
trigger.name = "Finished Message"
trigger.info = "Machine is up!"
end
config.trigger.after :up,
name: "Finished Message",
info: "Machine is up!"
```
Triggers can also be defined within the scope of guests in a Vagrantfile. These
triggers will only run on the configured guest. An example of a guest only trigger:
```ruby
config.vm.define "ubuntu" do |ubuntu|
ubuntu.vm.box = "ubuntu"
ubuntu.trigger.before :destroy do |trigger|
trigger.warn = "Dumping database to /vagrant/outfile"
trigger.run_remote {inline: "pg_dump dbname > /vagrant/outfile"}
end
end
```
Global and machine-scoped triggers will execute in the order that they are
defined within a Vagrantfile. Take for example an abstracted Vagrantfile:
```
Vagrantfile
global trigger 1
global trigger 2
machine defined
machine trigger 3
global trigger 4
end
```
In this generic case, the triggers would fire in the order: 1 -> 2 -> 3 -> 4
For more information about what options are available for triggers, see the
[configuration section](/docs/triggers/configuration.html).

View File

@ -0,0 +1,95 @@
---
layout: "docs"
page_title: "Vagrant Triggers Usage"
sidebar_current: "triggers-usage"
description: |-
Various Vagrant Triggers examples
---
# Basic Usage
Below are some very simple examples of how to use Vagrant Triggers.
## Examples
The following is a basic example of two global triggers. One that runs _before_
the `:up` command and one that runs _after_ the `:up` command:
```ruby
Vagrant.configure("2") do |config|
config.trigger.before :up do |trigger|
trigger.name = "Hello world"
trigger.info = "I am running before vagrant up!!"
end
config.trigger.before :up do |trigger|
trigger.name = "Hello world"
trigger.info = "I am running after vagrant up!!"
end
config.vm.define "ubuntu" do |ubuntu|
ubuntu.vm.box = "ubuntu"
end
end
```
These will run before and after each defined guest in the Vagrantfile.
Running a remote script to save a database on your host before __destroy__ing a
guest:
```ruby
Vagrant.configure("2") do |config|
config.vm.define "ubuntu" do |ubuntu|
ubuntu.vm.box = "ubuntu"
ubuntu.trigger.before :destroy do |trigger|
trigger.warn = "Dumping database to /vagrant/outfile"
trigger.run_remote = {inline: "pg_dump dbname > /vagrant/outfile"}
end
end
end
```
Now that the trigger is defined, running the __destroy__ command will fire off
the defined trigger before Vagrant destroys the machine.
```shell
$ vagrant destroy ubuntu
```
An example of defining three triggers that start and stop tinyproxy on your host
machine using homebrew:
```shell
#/bin/bash
# start-tinyproxy.sh
brew services start tinyproxy
```
```shell
#/bin/bash
# stop-tinyproxy.sh
brew services stop tinyproxy
```
```ruby
Vagrant.configure("2") do |config|
config.vm.define "ubuntu" do |ubuntu|
ubuntu.vm.box = "ubuntu"
ubuntu.trigger.before :up do |trigger|
trigger.info = "Starting tinyproxy..."
trigger.run = {path: "start-tinyproxy.sh"}
end
ubuntu.trigger.after :destroy, :halt do |trigger|
trigger.info = "Stopping tinyproxy..."
trigger.run = {path: "stop-tinyproxy.sh"}
end
end
end
```
Running `vagrant up` would fire the before trigger to start tinyproxy, where as
running either `vagrant destroy` or `vagrant halt` would stop tinyproxy.

View File

@ -207,6 +207,14 @@
</ul>
</li>
<li<%= sidebar_current("triggers") %>>
<a href="/docs/triggers/">Triggers</a>
<ul class="nav">
<li<%= sidebar_current("triggers-configuration") %>><a href="/docs/triggers/configuration.html">Configuration</a></li>
<li<%= sidebar_current("triggers-usage") %>><a href="/docs/triggers/usage.html">Usage</a></li>
</ul>
</li>
<li<%= sidebar_current("other") %>>
<a href="/docs/other/">Other</a>
<ul class="nav">