diff --git a/lib/vagrant/action/builtin/mixin_provisioners.rb b/lib/vagrant/action/builtin/mixin_provisioners.rb index 3d712c954..ac5c89cab 100644 --- a/lib/vagrant/action/builtin/mixin_provisioners.rb +++ b/lib/vagrant/action/builtin/mixin_provisioners.rb @@ -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] + 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 diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index fc8b5d5b2..3169b0552 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -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 diff --git a/plugins/kernel_v2/config/vm_provisioner.rb b/plugins/kernel_v2/config/vm_provisioner.rb index 59833c850..d0c545ba9 100644 --- a/plugins/kernel_v2/config/vm_provisioner.rb +++ b/plugins/kernel_v2/config/vm_provisioner.rb @@ -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. # diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 2b63dd383..49d99187d 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -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 diff --git a/test/unit/vagrant/action/builtin/mixin_provisioners_test.rb b/test/unit/vagrant/action/builtin/mixin_provisioners_test.rb new file mode 100644 index 000000000..63cb4def3 --- /dev/null +++ b/test/unit/vagrant/action/builtin/mixin_provisioners_test.rb @@ -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 diff --git a/website/source/docs/experimental/index.html.md b/website/source/docs/experimental/index.html.md index 5d0c7d34b..5de2365d7 100644 --- a/website/source/docs/experimental/index.html.md +++ b/website/source/docs/experimental/index.html.md @@ -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) diff --git a/website/source/docs/provisioning/basic_usage.html.md b/website/source/docs/provisioning/basic_usage.html.md index b4d3dfe44..43c20cb55 100644 --- a/website/source/docs/provisioning/basic_usage.html.md +++ b/website/source/docs/provisioning/basic_usage.html.md @@ -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 + +
+ Warning: Advanced Topic! Dependency provisioners are + an advanced topic. If you are just getting started with Vagrant, you can + safely skip this. +
+ +
+ Warning! 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. +
+ +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 +```