368 lines
13 KiB
Ruby
368 lines
13 KiB
Ruby
# Copyright (c) HashiCorp, Inc.
|
|
# SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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
|
|
# @param [Vagrant::UI] ui Class for printing messages to user
|
|
def initialize(env, config, machine, ui)
|
|
@env = env
|
|
@config = config
|
|
@machine = machine
|
|
@ui = ui
|
|
|
|
@logger = Log4r::Logger.new("vagrant::trigger::#{self.class.to_s.downcase}")
|
|
end
|
|
|
|
# Fires all triggers, if any are defined for the named type and guest. Returns early
|
|
# and logs a warning if the community plugin `vagrant-triggers` is installed
|
|
#
|
|
# @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 (:action, :hook, :command)
|
|
def fire(name, stage, guest, type, all: false)
|
|
if community_plugin_detected?
|
|
@logger.warn("Community plugin `vagrant-triggers detected, so core triggers will not fire")
|
|
return
|
|
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 = find(name, stage, guest, type, all: all)
|
|
|
|
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, all: false)
|
|
triggers = nil
|
|
name = nameify(name)
|
|
|
|
if stage == :before
|
|
triggers = config.before_triggers.select do |t|
|
|
(all && t.command.respond_to?(:to_sym) && t.command.to_sym == :all && !t.ignore.include?(name.to_sym)) ||
|
|
(type == :hook && matched_hook?(t.command, name)) ||
|
|
nameify(t.command) == name
|
|
end
|
|
elsif stage == :after
|
|
triggers = config.after_triggers.select do |t|
|
|
(all && t.command.respond_to?(:to_sym) && t.command.to_sym == :all && !t.ignore.include?(name.to_sym)) ||
|
|
(type == :hook && matched_hook?(t.command, name)) ||
|
|
nameify(t.command) == name
|
|
end
|
|
else
|
|
raise Errors::TriggersNoStageGiven,
|
|
name: name,
|
|
stage: stage,
|
|
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
|
|
#
|
|
# @return [Boolean]
|
|
def community_plugin_detected?
|
|
if !defined?(@_triggers_enabled)
|
|
plugins = Vagrant::Plugin::Manager.instance.installed_plugins
|
|
@_triggers_enabled = plugins.keys.include?("vagrant-triggers")
|
|
end
|
|
@_triggers_enabled
|
|
end
|
|
|
|
# Filters triggers to be fired based on configured restraints
|
|
#
|
|
# @param [Array] triggers An array of triggers to be filtered
|
|
# @param [String] guest_name The name of the current guest
|
|
# @param [Symbol] type The type of trigger (:command or :type)
|
|
# @return [Array] The filtered array of triggers
|
|
def filter_triggers(triggers, guest_name, type)
|
|
# look for only_on trigger constraint and if it doesn't match guest
|
|
# name, throw it away also be sure to preserve order
|
|
filter = triggers.dup
|
|
|
|
filter.each do |trigger|
|
|
index = nil
|
|
match = false
|
|
if trigger.only_on
|
|
trigger.only_on.each do |o|
|
|
if o.match(guest_name.to_s)
|
|
# 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 trigger.type != type
|
|
index = triggers.index(trigger)
|
|
end
|
|
|
|
if index
|
|
@logger.debug("Trigger #{trigger.id} will be ignored for #{guest_name}")
|
|
triggers.delete_at(index)
|
|
end
|
|
end
|
|
|
|
return triggers
|
|
end
|
|
|
|
# Execute all triggers in the given array
|
|
#
|
|
# @param [Array] triggers An array of triggers to be fired
|
|
def execute(triggers)
|
|
# ensure on_error is respected by exiting or continuing
|
|
triggers.each do |trigger|
|
|
@logger.debug("Running trigger #{trigger.id}...")
|
|
|
|
if trigger.name
|
|
@ui.info(I18n.t("vagrant.trigger.fire_with_name",
|
|
name: trigger.name))
|
|
else
|
|
@ui.info(I18n.t("vagrant.trigger.fire"))
|
|
end
|
|
|
|
if trigger.info
|
|
info(trigger.info)
|
|
end
|
|
|
|
if trigger.warn
|
|
warn(trigger.warn)
|
|
end
|
|
|
|
if trigger.abort
|
|
trigger_abort(trigger.abort)
|
|
end
|
|
|
|
if trigger.run
|
|
run(trigger.run, trigger.on_error, trigger.exit_codes)
|
|
end
|
|
|
|
if trigger.run_remote
|
|
run_remote(trigger.run_remote, trigger.on_error, trigger.exit_codes)
|
|
end
|
|
|
|
if trigger.ruby_block
|
|
execute_ruby(trigger.ruby_block)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Prints the given message at info level for a trigger
|
|
#
|
|
# @param [String] message The string to be printed
|
|
def info(message)
|
|
@ui.info(message)
|
|
end
|
|
|
|
# Prints the given message at warn level for a trigger
|
|
#
|
|
# @param [String] message The string to be printed
|
|
def warn(message)
|
|
@ui.warn(message)
|
|
end
|
|
|
|
# Runs a script on a guest
|
|
#
|
|
# @param [Provisioners::Shell::Config] config A Shell provisioner config
|
|
def run(config, on_error, exit_codes)
|
|
if config.inline
|
|
if Vagrant::Util::Platform.windows?
|
|
cmd = config.inline
|
|
else
|
|
cmd = Shellwords.split(config.inline)
|
|
end
|
|
|
|
@ui.detail(I18n.t("vagrant.trigger.run.inline", command: config.inline))
|
|
else
|
|
cmd = File.expand_path(config.path, @env.root_path).shellescape
|
|
args = Array(config.args)
|
|
cmd << " #{args.join(' ')}" if !args.empty?
|
|
cmd = Shellwords.split(cmd)
|
|
|
|
@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
|
|
|
|
@ui.detail(data, **options)
|
|
end
|
|
if !exit_codes.include?(result.exit_code)
|
|
raise Errors::TriggersBadExitCodes,
|
|
code: result.exit_code
|
|
end
|
|
rescue => e
|
|
@ui.error(I18n.t("vagrant.errors.triggers_run_fail"))
|
|
@ui.error(e.message)
|
|
|
|
if on_error == :halt
|
|
@logger.debug("Trigger run encountered an error. Halting on error...")
|
|
raise e
|
|
else
|
|
@logger.debug("Trigger run encountered an error. Continuing on anyway...")
|
|
@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, exit_codes)
|
|
if !@machine
|
|
# machine doesn't even exist.
|
|
if on_error == :halt
|
|
raise Errors::TriggersGuestNotExist
|
|
else
|
|
@ui.warn(I18n.t("vagrant.errors.triggers_guest_not_exist"))
|
|
@ui.warn(I18n.t("vagrant.trigger.on_error_continue"))
|
|
return
|
|
end
|
|
elsif @machine.state.id != :running
|
|
if on_error == :halt
|
|
raise Errors::TriggersGuestNotRunning,
|
|
machine_name: @machine.name,
|
|
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
|
|
|
|
# Exits Vagrant immediately
|
|
#
|
|
# @param [Integer] code Code to exit Vagrant on
|
|
def trigger_abort(exit_code)
|
|
if Thread.current[:batch_parallel_action]
|
|
@ui.warn(I18n.t("vagrant.trigger.abort_threaded"))
|
|
@logger.debug("Trigger abort within parallel batch action. " \
|
|
"Setting exit code and terminating.")
|
|
Thread.current[:exit_code] = exit_code
|
|
Thread.current.terminate
|
|
else
|
|
@ui.warn(I18n.t("vagrant.trigger.abort"))
|
|
@logger.debug("Trigger abort within non-parallel action, exiting directly")
|
|
Process.exit!(exit_code)
|
|
end
|
|
end
|
|
|
|
# Calls the given ruby block for execution
|
|
#
|
|
# @param [Proc] ruby_block
|
|
def execute_ruby(ruby_block)
|
|
ruby_block.call(@env, @machine)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|