591 lines
20 KiB
Ruby
591 lines
20 KiB
Ruby
# Copyright (c) HashiCorp, Inc.
|
|
# SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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(:ui) { Vagrant::UI::Silent.new }
|
|
let(:env) { {
|
|
machine: machine,
|
|
ui: ui,
|
|
} }
|
|
|
|
let(:triggers) {
|
|
@triggers ||= VagrantPlugins::Kernel_V2::TriggerConfig.new.tap do |triggers|
|
|
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(:hash_block) { {info: "hi", run: {inline: "echo 'hi'"}} }
|
|
let(:hash_block_two) { {warn: "WARNING!!", run_remote: {inline: "echo 'hi'"}} }
|
|
|
|
let(:subject) { described_class.new(env, triggers, machine, ui) }
|
|
|
|
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(: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(:execute)
|
|
subject.fire(:up, :before, "guest", :action)
|
|
end
|
|
end
|
|
|
|
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 :all special value" do
|
|
let(:triggers) { VagrantPlugins::Kernel_V2::TriggerConfig.new }
|
|
let(:ignores) { [] }
|
|
|
|
before do
|
|
triggers.before(:all, hash_block.merge(ignore: ignores))
|
|
triggers.after(:all, hash_block.merge(ignore: ignores))
|
|
triggers.finalize!
|
|
end
|
|
|
|
[:destroy, :halt, :provision, :reload, :resume, :suspend, :up].each do |supported_action|
|
|
it "should locate trigger when before #{supported_action} action is requested" do
|
|
expect(subject.find(supported_action, :before, "guest", :action, all: true)).not_to be_empty
|
|
end
|
|
|
|
it "should locate trigger when after #{supported_action} action is requested" do
|
|
expect(subject.find(supported_action, :after, "guest", :action, all: true)).not_to be_empty
|
|
end
|
|
end
|
|
|
|
context "with ignores" do
|
|
let(:ignores) { [:halt, :up] }
|
|
|
|
it "should not locate trigger when before command is ignored" do
|
|
expect(subject.find(:up, :before, "guest", :action, all: true)).to be_empty
|
|
end
|
|
|
|
it "should not locate trigger when after command is ignored" do
|
|
expect(subject.find(:halt, :after, "guest", :action, all: true)).to be_empty
|
|
end
|
|
|
|
it "should locate trigger when before command is not ignored" do
|
|
expect(subject.find(:provision, :before, "guest", :action, all: true)).not_to be_empty
|
|
end
|
|
|
|
it "should locate trigger when after command is not ignored" do
|
|
expect(subject.find(:provision, :after, "guest", :action, all: true)).not_to be_empty
|
|
end
|
|
end
|
|
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)
|
|
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, :action)
|
|
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", :action)
|
|
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", :action)
|
|
expect(after_triggers.size).to eq(3)
|
|
end
|
|
end
|
|
|
|
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(:execute, triggers.before_triggers)
|
|
end
|
|
end
|
|
|
|
describe "#info" do
|
|
let(:message) { "Printing some info" }
|
|
|
|
it "prints messages at INFO" do
|
|
expect(ui).to receive(:info).with(message).and_call_original
|
|
|
|
subject.send(:info, message)
|
|
end
|
|
end
|
|
|
|
describe "#warn" do
|
|
let(:message) { "Printing some warnings" }
|
|
|
|
it "prints messages at WARN" do
|
|
expect(ui).to receive(:warn).with(message).and_call_original
|
|
|
|
subject.send(:warn, message)
|
|
end
|
|
end
|
|
|
|
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) {
|
|
{info: "hi", run: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}},
|
|
exit_codes: [0,50]} }
|
|
let(:path_block) { {warn: "bye",
|
|
run: {path: "path/to the/script.sh", args: "HELLO", env: {"KEY"=>"VALUE"}},
|
|
on_error: :continue} }
|
|
|
|
let(:path_block_ps1) { {warn: "bye",
|
|
run: {path: "script.ps1", args: ["HELLO", "THERE"], 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
|
|
|
|
let(:subprocess_result_failure) do
|
|
double("subprocess_result_failure").tap do |result|
|
|
allow(result).to receive(:exit_code).and_return(1)
|
|
allow(result).to receive(:stderr).and_return("")
|
|
end
|
|
end
|
|
|
|
let(:subprocess_result_custom) do
|
|
double("subprocess_result_custom").tap do |result|
|
|
allow(result).to receive(:exit_code).and_return(50)
|
|
allow(result).to receive(:stderr).and_return("")
|
|
end
|
|
end
|
|
|
|
before do
|
|
trigger_run.after(:up, shell_block)
|
|
trigger_run.after(:up, shell_block_exit_codes)
|
|
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_yield(:stderr, "some output").
|
|
and_return(subprocess_result)
|
|
|
|
trigger = trigger_run.after_triggers.first
|
|
shell_config = trigger.run
|
|
on_error = trigger.on_error
|
|
exit_codes = trigger.exit_codes
|
|
|
|
expect(Vagrant::Util::PowerShell).to receive(:execute_inline).
|
|
with("echo 'hi'", options)
|
|
subject.send(:run, shell_config, on_error, exit_codes)
|
|
end
|
|
|
|
it "executes a 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_yield(:stdout, "more output").
|
|
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
|
|
exit_codes = trigger.exit_codes
|
|
|
|
expect(Vagrant::Util::PowerShell).to receive(:execute).
|
|
with("/vagrant/home/script.ps1", "HELLO", "THERE", options)
|
|
subject.send(:run, shell_config, on_error, exit_codes)
|
|
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
|
|
exit_codes = trigger.exit_codes
|
|
|
|
expect(Vagrant::Util::Subprocess).to receive(:execute).
|
|
with("echo", "hi", options)
|
|
subject.send(:run, shell_config, on_error, exit_codes)
|
|
end
|
|
|
|
it "executes a 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
|
|
exit_codes = trigger.exit_codes
|
|
|
|
expect(Vagrant::Util::Subprocess).to receive(:execute).
|
|
with("/vagrant/home/path/to the/script.sh", "HELLO", options)
|
|
subject.send(:run, shell_config, on_error, exit_codes)
|
|
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
|
|
exit_codes = trigger.exit_codes
|
|
|
|
expect(Vagrant::Util::Subprocess).to receive(:execute).
|
|
with("/vagrant/home/path/to the/script.sh", "HELLO", options)
|
|
subject.send(:run, shell_config, on_error, exit_codes)
|
|
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
|
|
exit_codes = trigger.exit_codes
|
|
|
|
expect(Vagrant::Util::Subprocess).to receive(:execute).
|
|
with("echo", "hi", options)
|
|
expect { subject.send(:run, shell_config, on_error, exit_codes) }.to raise_error("Fail!")
|
|
end
|
|
|
|
it "allows for acceptable exit codes" do
|
|
allow(Vagrant::Util::Subprocess).to receive(:execute).
|
|
and_return(subprocess_result_custom)
|
|
|
|
trigger = trigger_run.after_triggers[1]
|
|
shell_config = trigger.run
|
|
on_error = trigger.on_error
|
|
exit_codes = trigger.exit_codes
|
|
|
|
expect(Vagrant::Util::Subprocess).to receive(:execute).
|
|
with("echo", "hi", options)
|
|
subject.send(:run, shell_config, on_error, exit_codes)
|
|
end
|
|
|
|
it "exits if given a bad exit code" do
|
|
allow(Vagrant::Util::Subprocess).to receive(:execute).
|
|
and_return(subprocess_result_custom)
|
|
|
|
trigger = trigger_run.after_triggers.first
|
|
shell_config = trigger.run
|
|
on_error = trigger.on_error
|
|
exit_codes = trigger.exit_codes
|
|
|
|
expect(Vagrant::Util::Subprocess).to receive(:execute).
|
|
with("echo", "hi", options)
|
|
expect { subject.send(:run, shell_config, on_error, exit_codes) }.to raise_error(Vagrant::Errors::TriggersBadExitCodes)
|
|
end
|
|
end
|
|
|
|
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",
|
|
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
|
|
|
|
context "with no machine existing" do
|
|
let(:machine) { nil }
|
|
|
|
it "raises an error and halts if guest does not exist" do
|
|
trigger = trigger_run.after_triggers.first
|
|
shell_config = trigger.run_remote
|
|
on_error = trigger.on_error
|
|
exit_codes = trigger.exit_codes
|
|
|
|
expect { subject.send(:run_remote, shell_config, on_error, exit_codes) }.
|
|
to raise_error(Vagrant::Errors::TriggersGuestNotExist)
|
|
end
|
|
|
|
it "continues on if guest does not exist but is configured to continue on error" do
|
|
trigger = trigger_run.before_triggers.first
|
|
shell_config = trigger.run_remote
|
|
on_error = trigger.on_error
|
|
exit_codes = trigger.exit_codes
|
|
|
|
subject.send(:run_remote, shell_config, on_error, exit_codes)
|
|
end
|
|
end
|
|
|
|
it "raises an error and halts if guest is not running" do
|
|
allow(machine.state).to receive(:id).and_return(:not_running)
|
|
|
|
trigger = trigger_run.after_triggers.first
|
|
shell_config = trigger.run_remote
|
|
on_error = trigger.on_error
|
|
exit_codes = trigger.exit_codes
|
|
|
|
expect { subject.send(:run_remote, shell_config, on_error, exit_codes) }.
|
|
to raise_error(Vagrant::Errors::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
|
|
exit_codes = trigger.exit_codes
|
|
|
|
subject.send(:run_remote, shell_config, on_error, exit_codes)
|
|
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
|
|
exit_codes = trigger.exit_codes
|
|
|
|
subject.send(:run_remote, shell_config, on_error, exit_codes)
|
|
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
|
|
exit_codes = trigger.exit_codes
|
|
|
|
subject.send(:run_remote, shell_config, on_error, exit_codes)
|
|
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
|
|
exit_codes = trigger.exit_codes
|
|
|
|
expect { subject.send(:run_remote, shell_config, on_error, exit_codes) }.
|
|
to raise_error("Nope!")
|
|
end
|
|
end
|
|
|
|
describe "#trigger_abort" do
|
|
it "system exits when called" do
|
|
allow(Process).to receive(:exit!).and_return(true)
|
|
|
|
expect(Process).to receive(:exit!).with(3)
|
|
subject.send(:trigger_abort, 3)
|
|
end
|
|
|
|
context "when running in parallel" do
|
|
let(:thread) {
|
|
@t ||= Thread.new do
|
|
Thread.current[:batch_parallel_action] = true
|
|
Thread.stop
|
|
subject.send(:trigger_abort, exit_code)
|
|
end
|
|
}
|
|
let(:exit_code) { 22 }
|
|
|
|
before do
|
|
expect(Process).not_to receive(:exit!)
|
|
sleep(0.1) until thread.stop?
|
|
end
|
|
|
|
after { @t = nil }
|
|
|
|
it "should terminate the thread" do
|
|
expect(thread).to receive(:terminate).and_call_original
|
|
thread.wakeup
|
|
thread.join(1) while thread.alive?
|
|
end
|
|
|
|
it "should set the exit code into the thread data" do
|
|
expect(thread).to receive(:terminate).and_call_original
|
|
thread.wakeup
|
|
thread.join(1) while thread.alive?
|
|
expect(thread[:exit_code]).to eq(exit_code)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#ruby" do
|
|
let(:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new }
|
|
let(:block) { proc{var = 1+1} }
|
|
let(:ruby_trigger) { {info: "hi", ruby: block} }
|
|
|
|
before do
|
|
trigger_run.after(:up, ruby_trigger)
|
|
trigger_run.finalize!
|
|
end
|
|
|
|
it "executes a ruby block" do
|
|
expect(block).to receive(:call)
|
|
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
|