Merge pull request #11043 from briancain/provisioner-enhancements

Introduce new Provisioner options: before and after
This commit is contained in:
Brian Cain 2019-09-06 08:52:40 -07:00 committed by GitHub
commit f74239bed9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 586 additions and 4 deletions

View File

@ -29,15 +29,92 @@ module Vagrant
options = {
name: provisioner.name,
run: provisioner.run,
before: provisioner.before,
after: provisioner.after,
}
# Return the result
[result, options]
end
@_provisioner_instances = sort_provisioner_instances(@_provisioner_instances)
return @_provisioner_instances.compact
end
private
# Sorts provisioners based on order specified with before/after options
#
# @return [Array<Provisioner, Hash>]
def sort_provisioner_instances(pvs)
final_provs = []
root_provs = []
# extract root provisioners
root_provs = pvs.find_all { |_, o| o[:before].nil? && o[:after].nil? }
if root_provs.size == pvs.size
# no dependencies found
return pvs
end
# ensure placeholder variables are Arrays
dep_provs = []
each_provs = []
all_provs = []
# extract dependency provisioners
dep_provs = pvs.find_all { |_, o| o[:before].is_a?(String) || o[:after].is_a?(String) }
# extract each provisioners
each_provs = pvs.find_all { |_,o| o[:before] == :each || o[:after] == :each }
# extract all provisioners
all_provs = pvs.find_all { |_,o| o[:before] == :all || o[:after] == :all }
# insert provisioners in order
final_provs = root_provs
dep_provs.each do |p,options|
idx = 0
if options[:before]
idx = final_provs.index { |_, o| o[:name].to_s == options[:before] }
final_provs.insert(idx, [p, options])
elsif options[:after]
idx = final_provs.index { |_, o| o[:name].to_s == options[:after] }
idx += 1
final_provs.insert(idx, [p, options])
end
end
# Add :each and :all provisioners in reverse to preserve order in Vagrantfile
tmp_final_provs = []
final_provs.each_with_index do |(prv,o), i|
tmp_before = []
tmp_after = []
each_provs.reverse_each do |p, options|
if options[:before]
tmp_before << [p,options]
elsif options[:after]
tmp_after << [p,options]
end
end
tmp_final_provs += tmp_before unless tmp_before.empty?
tmp_final_provs += [[prv,o]]
tmp_final_provs += tmp_after unless tmp_after.empty?
end
final_provs = tmp_final_provs
# Add all to final array
all_provs.reverse_each do |p,options|
if options[:before]
final_provs.insert(0, [p,options])
elsif options[:after]
final_provs.push([p,options])
end
end
return final_provs
end
# This will return a mapping of a provisioner instance to its
# type.
def provisioner_type_map(env)
@ -47,6 +124,13 @@ module Vagrant
# Return the type map
@_provisioner_types
end
# @private
# Reset the cached values for platform. This is not considered a public
# API and should only be used for testing.
def self.reset!
instance_variables.each(&method(:remove_instance_variable))
end
end
end
end

View File

@ -7,6 +7,7 @@ require "vagrant/action/builtin/mixin_synced_folders"
require "vagrant/config/v2/util"
require "vagrant/util/platform"
require "vagrant/util/presence"
require "vagrant/util/experimental"
require File.expand_path("../vm_provisioner", __FILE__)
require File.expand_path("../vm_subvm", __FILE__)
@ -331,7 +332,19 @@ module VagrantPlugins
end
if !prov
prov = VagrantConfigProvisioner.new(name, type.to_sym)
if options.key?(:before)
before = options.delete(:before)
end
if options.key?(:after)
after = options.delete(:after)
end
if Vagrant::Util::Experimental.feature_enabled?("dependency_provisioners")
opts = {before: before, after: after}
prov = VagrantConfigProvisioner.new(name, type.to_sym, opts)
else
prov = VagrantConfigProvisioner.new(name, type.to_sym)
end
@provisioners << prov
end
@ -760,6 +773,11 @@ module VagrantPlugins
next
end
provisioner_errors = vm_provisioner.validate(machine, @provisioners)
if provisioner_errors
errors = Vagrant::Config::V2::Util.merge_errors(errors, provisioner_errors)
end
if vm_provisioner.config
provisioner_errors = vm_provisioner.config.validate(machine)
if provisioner_errors

View File

@ -3,7 +3,10 @@ require 'log4r'
module VagrantPlugins
module Kernel_V2
# Represents a single configured provisioner for a VM.
class VagrantConfigProvisioner
class VagrantConfigProvisioner < Vagrant.plugin("2", :config)
# Defaults
VALID_BEFORE_AFTER_TYPES = [:each, :all].freeze
# Unique name for this provisioner
#
# @return [String]
@ -29,7 +32,7 @@ module VagrantPlugins
# @return [Object]
attr_accessor :config
# When to run this provisioner. Either "once" or "always"
# When to run this provisioner. Either "once", "always", or "never"
#
# @return [String]
attr_accessor :run
@ -40,7 +43,17 @@ module VagrantPlugins
# @return [Boolean]
attr_accessor :preserve_order
def initialize(name, type)
# The name of a provisioner to run before it has started
#
# @return [String, Symbol]
attr_accessor :before
# The name of a provisioner to run after it is finished
#
# @return [String, Symbol]
attr_accessor :after
def initialize(name, type, **options)
@logger = Log4r::Logger.new("vagrant::config::vm::provisioner")
@logger.debug("Provisioner defined: #{name}")
@ -51,6 +64,8 @@ module VagrantPlugins
@preserve_order = false
@run = nil
@type = type
@before = options[:before]
@after = options[:after]
# Attempt to find the provisioner...
if !Vagrant.plugin("2").manager.provisioners[type]
@ -90,6 +105,75 @@ module VagrantPlugins
@config.finalize!
end
# Validates the before/after options
#
# @param [Vagrant::Machine] machine - machine to validate against
# @param [Array] provisioners - Array of defined provisioners for the guest machine
# @return [Array] array of strings of error messages from config option validation
def validate(machine, provisioners)
errors = _detected_errors
provisioner_names = provisioners.map { |i| i.name.to_s if i.name != name }.compact
if @before && @after
errors << I18n.t("vagrant.provisioners.base.both_before_after_set")
end
if @before
if !VALID_BEFORE_AFTER_TYPES.include?(@before)
if @before.is_a?(Symbol) && !VALID_BEFORE_AFTER_TYPES.include?(@before)
errors << I18n.t("vagrant.provisioners.base.invalid_alias_value", opt: "before", alias: VALID_BEFORE_AFTER_TYPES.join(", "))
elsif !@before.is_a?(String) && !VALID_BEFORE_AFTER_TYPES.include?(@before)
errors << I18n.t("vagrant.provisioners.base.wrong_type", opt: "before")
end
if !provisioner_names.include?(@before)
errors << I18n.t("vagrant.provisioners.base.missing_provisioner_name",
name: @before,
machine_name: machine.name,
action: "before",
provisioner_name: @name)
end
dep_prov = provisioners.find_all { |i| i.name.to_s == @before && (i.before || i.after) }
if !dep_prov.empty?
errors << I18n.t("vagrant.provisioners.base.dependency_provisioner_dependency",
name: @name,
dep_name: dep_prov.first.name.to_s)
end
end
end
if @after
if !VALID_BEFORE_AFTER_TYPES.include?(@after)
if @after.is_a?(Symbol)
errors << I18n.t("vagrant.provisioners.base.invalid_alias_value", opt: "after", alias: VALID_BEFORE_AFTER_TYPES.join(", "))
elsif !@after.is_a?(String)
errors << I18n.t("vagrant.provisioners.base.wrong_type", opt: "after")
end
if !provisioner_names.include?(@after)
errors << I18n.t("vagrant.provisioners.base.missing_provisioner_name",
name: @after,
machine_name: machine.name,
action: "after",
provisioner_name: @name)
end
dep_prov = provisioners.find_all { |i| i.name.to_s == @after && (i.before || i.after) }
if !dep_prov.empty?
errors << I18n.t("vagrant.provisioners.base.dependency_provisioner_dependency",
name: @name,
dep_name: dep_prov.first.name.to_s)
end
end
end
{"provisioner" => errors}
end
# Returns whether the provisioner used was invalid or not. A provisioner
# is invalid if it can't be found.
#

View File

@ -2486,6 +2486,17 @@ en:
VirtualBox has successfully been installed!
provisioners:
base:
both_before_after_set: |-
Dependency provisioners cannot currently set both `before` and `after` options.
dependency_provisioner_dependency: |-
Dependency provisioner "%{name}" relies on another dependency provisioner "%{dep_name}". This is currently not supported.
invalid_alias_value: |-
Provisioner option `%{opt}` is not set as a valid type. Must be a string, or one of the alias shortcuts: %{alias}
missing_provisioner_name: |-
Could not find provisioner name `%{name}` defined for machine `%{machine_name}` to run provisioner "%{provisioner_name}" `%{action}`.
wrong_type: |-
Provisioner option `%{opt}` is not set as a valid type. Must be a string.
chef:
chef_not_detected: |-
The chef binary (either `chef-solo` or `chef-client`) was not found on

View File

@ -0,0 +1,238 @@
require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/kernel_v2/config/vm")
require "vagrant/action/builtin/mixin_provisioners"
describe Vagrant::Action::Builtin::MixinProvisioners do
include_context "unit"
let(:sandbox) { isolated_environment }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
sandbox.vagrantfile("")
sandbox.create_vagrant_env
end
let(:provisioner_config){ {} }
let(:provisioner_one) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("spec-test", :shell)
prov.config = provisioner_config
prov
end
let(:provisioner_two) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("spec-test", :shell)
prov.config = provisioner_config
prov
end
let(:provisioner_instances) { [provisioner_one,provisioner_two] }
let(:ui) { double("ui") }
let(:vm) { double("vm", provisioners: provisioner_instances) }
let(:config) { double("config", vm: vm) }
let(:machine) { double("machine", ui: ui, config: config) }
let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new(".") }}
subject do
Class.new do
extend Vagrant::Action::Builtin::MixinProvisioners
end
end
after do
sandbox.close
described_class.reset!
end
describe "#provisioner_instances" do
it "returns all the instances of configured provisioners" do
result = subject.provisioner_instances(env)
expect(result.size).to eq(provisioner_instances.size)
shell_one = result.first
expect(shell_one.first).to be_a(VagrantPlugins::Shell::Provisioner)
shell_two = result[1]
expect(shell_two.first).to be_a(VagrantPlugins::Shell::Provisioner)
end
end
context "#sort_provisioner_instances" do
describe "with no dependency provisioners" do
it "returns the original array" do
result = subject.provisioner_instances(env)
expect(result.size).to eq(provisioner_instances.size)
shell_one = result.first
expect(shell_one.first).to be_a(VagrantPlugins::Shell::Provisioner)
shell_two = result[1]
expect(shell_two.first).to be_a(VagrantPlugins::Shell::Provisioner)
end
end
describe "with before and after dependency provisioners" do
let(:provisioner_config){ {} }
let(:provisioner_root) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root-test", :shell)
prov.config = provisioner_config
prov
end
let(:provisioner_before) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("before-test", :shell)
prov.config = provisioner_config
prov.before = "root-test"
prov
end
let(:provisioner_after) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("after-test", :shell)
prov.config = provisioner_config
prov.after = "root-test"
prov
end
let(:provisioner_instances) { [provisioner_root,provisioner_before,provisioner_after] }
it "returns the array in the correct order" do
result = subject.provisioner_instances(env)
expect(result[0].last[:name]).to eq("before-test")
expect(result[1].last[:name]).to eq("root-test")
expect(result[2].last[:name]).to eq("after-test")
end
end
describe "with before :each dependency provisioners" do
let(:provisioner_config){ {} }
let(:provisioner_root) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root-test", :shell)
prov.config = provisioner_config
prov
end
let(:provisioner_root2) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root2-test", :shell)
prov.config = provisioner_config
prov
end
let(:provisioner_before) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("before-test", :shell)
prov.config = provisioner_config
prov.before = :each
prov
end
let(:provisioner_instances) { [provisioner_root,provisioner_root2,provisioner_before] }
it "puts the each shortcut provisioners in place" do
result = subject.provisioner_instances(env)
expect(result[0].last[:name]).to eq("before-test")
expect(result[1].last[:name]).to eq("root-test")
expect(result[2].last[:name]).to eq("before-test")
expect(result[3].last[:name]).to eq("root2-test")
end
end
describe "with after :each dependency provisioners" do
let(:provisioner_config){ {} }
let(:provisioner_root) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root-test", :shell)
prov.config = provisioner_config
prov
end
let(:provisioner_root2) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root2-test", :shell)
prov.config = provisioner_config
prov
end
let(:provisioner_after) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("after-test", :shell)
prov.config = provisioner_config
prov.after = :each
prov
end
let(:provisioner_instances) { [provisioner_root,provisioner_root2,provisioner_after] }
it "puts the each shortcut provisioners in place" do
result = subject.provisioner_instances(env)
expect(result[0].last[:name]).to eq("root-test")
expect(result[1].last[:name]).to eq("after-test")
expect(result[2].last[:name]).to eq("root2-test")
expect(result[3].last[:name]).to eq("after-test")
end
end
describe "with before and after :each dependency provisioners" do
let(:provisioner_config){ {} }
let(:provisioner_root) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root-test", :shell)
prov.config = provisioner_config
prov
end
let(:provisioner_root2) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root2-test", :shell)
prov.config = provisioner_config
prov
end
let(:provisioner_after) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("after-test", :shell)
prov.config = provisioner_config
prov.after = :each
prov
end
let(:provisioner_before) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("before-test", :shell)
prov.config = provisioner_config
prov.before = :each
prov
end
let(:provisioner_instances) { [provisioner_root,provisioner_root2,provisioner_before,provisioner_after] }
it "puts the each shortcut provisioners in place" do
result = subject.provisioner_instances(env)
expect(result[0].last[:name]).to eq("before-test")
expect(result[1].last[:name]).to eq("root-test")
expect(result[2].last[:name]).to eq("after-test")
expect(result[3].last[:name]).to eq("before-test")
expect(result[4].last[:name]).to eq("root2-test")
expect(result[5].last[:name]).to eq("after-test")
end
end
describe "with before and after :all dependency provisioners" do
let(:provisioner_config){ {} }
let(:provisioner_root) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root-test", :shell)
prov.config = provisioner_config
prov
end
let(:provisioner_root2) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root2-test", :shell)
prov.config = provisioner_config
prov
end
let(:provisioner_after) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("after-test", :shell)
prov.config = provisioner_config
prov.after = :all
prov
end
let(:provisioner_before) do
prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("before-test", :shell)
prov.config = provisioner_config
prov.before = :all
prov
end
let(:provisioner_instances) { [provisioner_root,provisioner_root2,provisioner_before,provisioner_after] }
it "puts the each shortcut provisioners in place" do
result = subject.provisioner_instances(env)
expect(result[0].last[:name]).to eq("before-test")
expect(result[1].last[:name]).to eq("root-test")
expect(result[2].last[:name]).to eq("root2-test")
expect(result[3].last[:name]).to eq("after-test")
end
end
end
end

View File

@ -47,3 +47,10 @@ This is a list of all the valid experimental features that Vagrant recognizes:
Enabling this feature allows triggers to recognize and execute `:type` triggers.
More information about how these should be used can be found on the [trigger documentation page](/docs/triggers/configuration.html#trigger-types)
### `dependency_provisioners`
Enabling this feature allows all provisioners to specify `before` and `after`
options. These options allow provisioners to be configured to run before or after
any given "root" provisioner. more information about these options can be found
on the [base provisioner documentation page](/docs/provisioning/basic_usage.html)

View File

@ -14,6 +14,31 @@ While Vagrant offers multiple options for how you are able to provision
your machine, there is a standard usage pattern as well as some important
points common to all provisioners that are important to know.
## Options
Every Vagrant provisioner accepts a few base options. The only required
option is what type a provisioner is:
* `name` (string) - The name of the provisioner. Note: if no `type` option is given,
this option _must_ be the type of provisioner it is. If you wish to give it a
different name you must also set the `type` option to define the kind of provisioner.
* `type` (string) - The class of provisioner to configure. (i.e. `"shell"` or `"file"`)
* `before` (string or symbol) - The exact name of an already defined provisioner
that _this_ provisioner should run before. If defined as a symbol, its only valid
values are `:each` or `:all`, which makes the provisioner run before each and
every root provisioner, or before all provisioners respectively.
__Note__: This option is currently experimental, so it needs to be explicitly
enabled to work. More info can be found [here](/docs/experimental/index.html).
* `after` (string or symbol) - The exact name of an already defined provisioner
that _this_ provisioner should run after. If defined as a symbol, its only valid
values are `:each` or `:all`, which makes the provisioner run after each and
every root provisioner, or before all provisioners respectively.
__Note__: This option is currently experimental, so it needs to be explicitly
enabled to work. More info can be found [here](/docs/experimental/index.html).
More information about how to use `before` and `after` options can be read [below](#dependency-provisioners).
## Configuration
First, every provisioner is configured within your
@ -226,3 +251,118 @@ Vagrant.configure("2") do |config|
inline: "echo SECOND!"
end
```
## Dependency Provisioners
<div class="alert alert-warning">
<strong>Warning: Advanced Topic!</strong> Dependency provisioners are
an advanced topic. If you are just getting started with Vagrant, you can
safely skip this.
</div>
<div class="alert alert-warning">
<strong>Warning!</strong> This feature is still experimental and may break or
change in between releases. Use at your own risk.
This feature currently reqiures the experimental flag to be used. To explicitly enable this feature, you can set the experimental flag to:
```
VAGRANT_EXPERIMENTAL="dependency_provisioners"
```
Please note that `VAGRANT_EXPERIMENTAL` is an environment variable. For more
information about this flag visit the [Experimental docs page](/docs/experimental/)
for more info. Without this flag enabled, provisioners with the `before` and
`after` option will be ignored.
</div>
If a provisioner has been configured using the `before` or `after` options, it
is considered a _Dependency Provisioner_. This means it has been configured to
run before or after a _Root Provisioner_, which does not have the `before` or
`after` options configured.
Dependency provisioners also have two valid shortcuts:
`:each` and `:all`.
**Note**: As of 2.2.6, dependency provisioners cannot rely on other dependency
provisioners and is considered a configuration state error in Vagrant. If you must
order dependency provisioners, you can still order them by the order they are defined
inside your Vagrantfile.
An example of these dependency provisioners can be seen below:
```ruby
Vagrant.configure("2") do |config|
config.vm.provision "C", after: "B", type: "shell", inline:<<-SHELL
echo 'C'
SHELL
config.vm.provision "B", type: "shell", inline:<<-SHELL
echo 'B'
SHELL
config.vm.provision "D", type: "shell", inline:<<-SHELL
echo 'D'
SHELL
config.vm.provision "A", before: "B", type: "shell", inline:<<-SHELL
echo 'A'
SHELL
config.vm.provision "Separate After", after: :each, type: "shell", inline:<<-SHELL
echo '=============================='
SHELL
config.vm.provision "Separate Before", before: :each, type: "shell", inline:<<-SHELL
echo '++++++++++++++++++++++++++++++'
SHELL
config.vm.provision "Hello", before: :all, type: "shell", inline:<<-SHELL
echo 'HERE WE GO!!'
SHELL
config.vm.provision "Goodbye", after: :all, type: "shell", inline:<<-SHELL
echo 'The end'
SHELL
end
```
The result of running `vagrant provision` with a guest configured above:
```
==> default: Running provisioner: Hello (shell)...
default: Running: inline script
default: HERE WE GO!!
==> default: Running provisioner: Separate Before (shell)...
default: Running: inline script
default: ++++++++++++++++++++++++++++++
==> default: Running provisioner: A (shell)...
default: Running: inline script
default: A
==> default: Running provisioner: Separate After (shell)...
default: Running: inline script
default: ==============================
==> default: Running provisioner: Separate Before (shell)...
default: Running: inline script
default: ++++++++++++++++++++++++++++++
==> default: Running provisioner: B (shell)...
default: Running: inline script
default: B
==> default: Running provisioner: Separate After (shell)...
default: Running: inline script
default: ==============================
==> default: Running provisioner: Separate Before (shell)...
default: Running: inline script
default: ++++++++++++++++++++++++++++++
==> default: Running provisioner: C (shell)...
default: Running: inline script
default: C
==> default: Running provisioner: Separate After (shell)...
default: Running: inline script
default: ==============================
==> default: Running provisioner: Separate Before (shell)...
default: Running: inline script
default: ++++++++++++++++++++++++++++++
==> default: Running provisioner: D (shell)...
default: Running: inline script
default: D
==> default: Running provisioner: Separate After (shell)...
default: Running: inline script
default: ==============================
==> default: Running provisioner: Goodbye (shell)...
default: Running: inline script
default: The end
```