diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 95e7933c6..45d50068d 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -25,6 +25,7 @@ module Vagrant autoload :HandleBox, "vagrant/action/builtin/handle_box" autoload :HandleBoxUrl, "vagrant/action/builtin/handle_box_url" autoload :HandleForwardedPortCollisions, "vagrant/action/builtin/handle_forwarded_port_collisions" + autoload :HasProvisioner, "vagrant/action/builtin/has_provisioner" autoload :IsEnvSet, "vagrant/action/builtin/is_env_set" autoload :IsState, "vagrant/action/builtin/is_state" autoload :Lock, "vagrant/action/builtin/lock" diff --git a/lib/vagrant/action/builtin/has_provisioner.rb b/lib/vagrant/action/builtin/has_provisioner.rb new file mode 100644 index 000000000..c58abe9b4 --- /dev/null +++ b/lib/vagrant/action/builtin/has_provisioner.rb @@ -0,0 +1,36 @@ +module Vagrant + module Action + module Builtin + # This middleware is used with Call to test if this machine + # has available provisioners + class HasProvisioner + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::action::builtin::has_provisioner") + end + + def call(env) + machine = env[:machine] + + if machine.provider.capability?(:has_communicator) + has_communicator = machine.provider.capability(:has_communicator) + else + has_communicator = true + end + + env[:skip] = [] + if !has_communicator + machine.config.vm.provisioners.each do |p| + if p.communicator_required + env[:skip].push(p) + @logger.info("Skipping running provisioner #{p.name || 'no name'}, type: #{p.type}") + p.run = :never + end + end + end + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/mixin_provisioners.rb b/lib/vagrant/action/builtin/mixin_provisioners.rb index 32e7fa7d5..07f14eb73 100644 --- a/lib/vagrant/action/builtin/mixin_provisioners.rb +++ b/lib/vagrant/action/builtin/mixin_provisioners.rb @@ -49,6 +49,7 @@ module Vagrant run: provisioner.run, before: provisioner.before, after: provisioner.after, + communicator_required: provisioner.communicator_required, } # Return the result diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 3f8e419b3..1fa0d2e5e 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -410,6 +410,8 @@ module VagrantPlugins prov.preserve_order = !!options.delete(:preserve_order) if \ options.key?(:preserve_order) prov.run = options.delete(:run) if options.key?(:run) + prov.communicator_required = options.delete(:communicator_required) if options.key?(:communicator_required) + prov.add_config(options, &block) nil end diff --git a/plugins/kernel_v2/config/vm_provisioner.rb b/plugins/kernel_v2/config/vm_provisioner.rb index 97f838e6e..a99e143d0 100644 --- a/plugins/kernel_v2/config/vm_provisioner.rb +++ b/plugins/kernel_v2/config/vm_provisioner.rb @@ -56,6 +56,12 @@ module VagrantPlugins # @return [String, Symbol] attr_accessor :after + # Boolean, when true signifies that some communicator must + # be available in order for the provisioner to run. + # + # @return [Boolean] + attr_accessor :communicator_required + def initialize(name, type, **options) @logger = Log4r::Logger.new("vagrant::config::vm::provisioner") @logger.debug("Provisioner defined: #{name}") @@ -69,6 +75,7 @@ module VagrantPlugins @type = type @before = options[:before] @after = options[:after] + @communicator_required = options.fetch(:communicator_required, true) # Attempt to find the provisioner... if !Vagrant.plugin("2").manager.provisioners[type] @@ -118,6 +125,10 @@ module VagrantPlugins provisioner_names = provisioners.map { |i| i.name.to_s if i.name != name }.compact + if ![TrueClass, FalseClass].include?(@communicator_required.class) + errors << I18n.t("vagrant.provisioners.base.wrong_type", opt: "communicator_required", type: "boolean") + end + if @before && @after errors << I18n.t("vagrant.provisioners.base.both_before_after_set") end @@ -127,7 +138,7 @@ module VagrantPlugins 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") + errors << I18n.t("vagrant.provisioners.base.wrong_type", opt: "before", type: "string") end if !provisioner_names.include?(@before) @@ -153,7 +164,7 @@ module VagrantPlugins 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") + errors << I18n.t("vagrant.provisioners.base.wrong_type", opt: "after", type: "string") end if !provisioner_names.include?(@after) diff --git a/plugins/providers/docker/action.rb b/plugins/providers/docker/action.rb index f0a2d7a05..2705439f4 100644 --- a/plugins/providers/docker/action.rb +++ b/plugins/providers/docker/action.rb @@ -66,14 +66,8 @@ module VagrantPlugins next end - b3.use Call, HasSSH do |env3, b4| - if env3[:result] - b4.use Provision - else - b4.use Message, - I18n.t("docker_provider.messages.provision_no_ssh"), - post: true - end + b3.use Call, HasProvisioner do |env3, b4| + b4.use Provision end end end @@ -216,14 +210,8 @@ module VagrantPlugins Vagrant::Action::Builder.new.tap do |b| b.use Call, IsState, :running do |env, b2| if env[:machine_action] != :run_command - b2.use Call, HasSSH do |env2, b3| - if env2[:result] - b3.use Provision - else - b3.use Message, - I18n.t("docker_provider.messages.provision_no_ssh"), - post: true - end + b2.use Call, HasProvisioner do |env2, b3| + b3.use Provision end end diff --git a/plugins/providers/docker/cap/has_communicator.rb b/plugins/providers/docker/cap/has_communicator.rb new file mode 100644 index 000000000..42ef6c763 --- /dev/null +++ b/plugins/providers/docker/cap/has_communicator.rb @@ -0,0 +1,11 @@ +module VagrantPlugins + module DockerProvider + module Cap + module HasCommunicator + def self.has_communicator(machine) + return machine.provider_config.has_ssh + end + end + end + end +end diff --git a/plugins/providers/docker/plugin.rb b/plugins/providers/docker/plugin.rb index d5f7c7bd4..b297016ec 100644 --- a/plugins/providers/docker/plugin.rb +++ b/plugins/providers/docker/plugin.rb @@ -67,6 +67,11 @@ module VagrantPlugins Cap::ProxyMachine end + provider_capability("docker", "has_communicator") do + require_relative "cap/has_communicator" + Cap::HasCommunicator + end + protected def self.init! diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 6bdb40967..8ca9b1f1c 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -2697,7 +2697,7 @@ en: 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. + Provisioner option `%{opt}` is not set as a valid type. Must be a %{type}. chef: chef_not_detected: |- The chef binary (either `chef-solo` or `chef-client`) was not found on diff --git a/templates/locales/providers_docker.yml b/templates/locales/providers_docker.yml index 0760b5567..b4709dd2d 100644 --- a/templates/locales/providers_docker.yml +++ b/templates/locales/providers_docker.yml @@ -133,6 +133,10 @@ en: Stopping container... container_ready: |- Container started and ready for use! + not_provisioning: |- + The following provisioners require a communicator, though none is available (this container does not support SSH). + Not running the following provisioners: + - %{provisioners} status: host_state_unknown: |- diff --git a/test/unit/plugins/kernel_v2/config/vm_test.rb b/test/unit/plugins/kernel_v2/config/vm_test.rb index 914d3c74f..b8d06fae4 100644 --- a/test/unit/plugins/kernel_v2/config/vm_test.rb +++ b/test/unit/plugins/kernel_v2/config/vm_test.rb @@ -441,15 +441,18 @@ describe VagrantPlugins::Kernel_V2::VMConfig do it "stores the provisioners" do subject.provision("shell", inline: "foo") subject.provision("shell", inline: "bar", run: "always") { |s| s.path = "baz" } + subject.provision("shell", inline: "foo", communicator_required: false) subject.finalize! r = subject.provisioners - expect(r.length).to eql(2) + expect(r.length).to eql(3) expect(r[0].run).to be_nil expect(r[0].config.inline).to eql("foo") expect(r[1].config.inline).to eql("bar") expect(r[1].config.path).to eql("baz") expect(r[1].run).to eql(:always) + expect(r[1].communicator_required).to eql(true) + expect(r[2].communicator_required).to eql(false) end it "allows provisioner settings to be overridden" do diff --git a/test/unit/vagrant/action/builtin/has_provisioner_test.rb b/test/unit/vagrant/action/builtin/has_provisioner_test.rb new file mode 100644 index 000000000..474b8d60b --- /dev/null +++ b/test/unit/vagrant/action/builtin/has_provisioner_test.rb @@ -0,0 +1,66 @@ +require File.expand_path("../../../../base", __FILE__) + + +describe Vagrant::Action::Builtin::HasProvisioner do + include_context "unit" + + let(:provisioner_one) { double("provisioner_one") } + let(:provisioner_two) { double("provisioner_two") } + let(:provisioners) { [provisioner_one, provisioner_two] } + let(:machine) { double("machine") } + let(:ui) {double("ui") } + let(:env) {{ machine: machine, ui: ui, root_path: Pathname.new(".") }} + let(:app) { lambda { |*args| }} + + subject { described_class.new(app, env) } + + describe "#call" do + before do + allow(provisioner_one).to receive(:communicator_required).and_return(true) + allow(provisioner_one).to receive(:name) + allow(provisioner_one).to receive(:type) + allow(provisioner_two).to receive(:communicator_required).and_return(false) + allow(provisioner_two).to receive(:name) + allow(provisioner_two).to receive(:type) + allow(machine).to receive_message_chain(:config, :vm, :provisioners).and_return(provisioners) + end + + context "provider has capability :has_communicator" do + before do + allow(machine).to receive_message_chain(:provider, :capability?).with(:has_communicator).and_return(true) + end + + it "does not skip any provisioners if provider has ssh" do + allow(machine).to receive_message_chain(:provider, :capability).with(:has_communicator).and_return(true) + expect(provisioner_one).to_not receive(:communicator_required) + expect(provisioner_two).to_not receive(:communicator_required) + + subject.call(env) + expect(env[:skip]).to eq([]) + end + + it "skips provisioners that require a communicator if provider does not have ssh" do + allow(machine).to receive_message_chain(:provider, :capability).with(:has_communicator).and_return(false) + expect(provisioner_one).to receive(:communicator_required) + expect(provisioner_two).to receive(:communicator_required) + expect(provisioner_one).to receive(:run=).with(:never) + + subject.call(env) + expect(env[:skip]).to eq([provisioner_one]) + end + end + + context "provider does not have capability :has_communicator" do + before do + allow(machine).to receive_message_chain(:provider, :capability?).with(:has_communicator).and_return(false) + end + + it "does not skip any provisioners" do + expect(provisioner_one).to_not receive(:communicator_required) + expect(provisioner_two).to_not receive(:communicator_required) + subject.call(env) + expect(env[:skip]).to eq([]) + end + end + end +end diff --git a/website/pages/docs/provisioning/basic_usage.mdx b/website/pages/docs/provisioning/basic_usage.mdx index 8da7f6c06..82b94006d 100644 --- a/website/pages/docs/provisioning/basic_usage.mdx +++ b/website/pages/docs/provisioning/basic_usage.mdx @@ -35,6 +35,11 @@ option is what type a provisioner is: 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). +* `communicator_required` (boolean) - Specifies the machine must be accessible by + Vagrant in order to run the provisioner. If set to true, the provisioner will + only run if Vagrant can establish communication with the guest. If set to false + the provisioner will run regardless of Vagrant's ability to communicate with the + guest. Defaults to true. More information about how to use `before` and `after` options can be read [below](#dependency-provisioners).