diff --git a/lib/vagrant/vagrantfile.rb b/lib/vagrant/vagrantfile.rb new file mode 100644 index 000000000..0857dc807 --- /dev/null +++ b/lib/vagrant/vagrantfile.rb @@ -0,0 +1,157 @@ +module Vagrant + # This class provides a way to load and access the contents + # of a Vagrantfile. + # + # This class doesn't actually load Vagrantfiles, parse them, + # merge them, etc. That is the job of {Config::Loader}. This + # class, on the other hand, has higher-level operations on + # a loaded Vagrantfile such as looking up the defined machines, + # loading the configuration of a specific machine/provider combo, + # etc. + class Vagrantfile + # Initializes by loading a Vagrantfile. + # + # @param [Config::Loader] loader Configuration loader that should + # already be configured with the proper Vagrantflie locations. + # This usually comes from {Vagrant::Environment} + # @param [Array] keys The Vagrantfiles to load and the + # order to load them in (keys within the loader). + def initialize(loader, keys) + @keys = keys + @loader = loader + @config, _ = loader.load(keys) + end + + # Returns the configuration for a single machine. + # + # When loading a box Vagrantfile, it will be prepended to the + # key order specified when initializing this class. Sub-machine + # and provider-specific overrides are appended at the end. The + # actual order is: + # + # - box + # - keys specified for #initialize + # - sub-machine + # - provider + # + # @param [Symbol] name Name of the machine. + # @param [Symbol] provider The provider the machine should + # be backed by (required for provider overrides). + # @param [BoxCollection] boxes BoxCollection to look up the + # box Vagrantfile. + def machine_config(name, provider, boxes) + keys = @keys.dup + + sub_machine = @config.vm.defined_vms[name] + if !sub_machine + raise Errors::MachineNotFound, + :name => name, :provider => provider + end + + provider_plugin = Vagrant.plugin("2").manager.providers[provider] + if !provider_plugin + raise Errors::ProviderNotFound, + :machine => name, :provider => provider + end + + box_formats = provider_plugin[1][:box_format] || provider + + # Add the sub-machine configuration to the loader and keys + vm_config_key = "#{object_id}_machine_#{name}" + @loader.set(vm_config_key, sub_machine.config_procs) + keys << vm_config_key + + # Load once so that we can get the proper box value + config, config_warnings, config_errors = @loader.load(keys) + + # Track the original box so we know if we changed + original_box = config.vm.box + + # The proc below loads the box and provider overrides. This is + # in a proc because it may have to recurse if the provider override + # changes the box. + load_box_proc = lambda do + local_keys = keys.dup + + # Load the box Vagrantfile, if there is one + if config.vm.box + box = boxes.find(config.vm.box, box_formats) + if box + box_vagrantfile = find_vagrantfile(box.directory) + if box_vagrantfile + box_config_key = + "#{boxes.object_id}_#{box.name}_#{box.provider}".to_sym + @loader.set(box_config_key, box_vagrantfile) + local_keys.unshift(box_config_key) + config, config_warnings, config_errors = @loader.load(local_keys) + end + end + end + + # Load provider overrides + provider_overrides = config.vm.get_provider_overrides(provider) + if !provider_overrides.empty? + config_key = + "#{object_id}_vm_#{name}_#{config.vm.box}_#{provider}".to_sym + @loader.set(config_key, provider_overrides) + local_keys << config_key + config, config_warnings, config_errors = @loader.load(local_keys) + end + + # If the box changed, then we need to reload + if original_box != config.vm.box + # TODO: infinite loop protection? + + original_box = config.vm.box + load_box_proc.call + end + end + + # Load the box and provider overrides + load_box_proc.call + + return config, config_warnings, config_errors + end + + # Returns a list of the machines that are defined within this + # Vagrantfile. + # + # @return [Array] + def machine_names + @config.vm.defined_vm_keys.dup + end + + # Returns the name of the machine that is designated as the + # "primary." + # + # In the case of a single-machine environment, this is just the + # single machine name. In the case of a multi-machine environment, + # then this is the machine that is marked as primary, or nil if + # no primary machine was specified. + # + # @return [Symbol] + def primary_machine_name + # If it is a single machine environment, then return the name + return machine_names.first if machine_names.length == 1 + + # If it is a multi-machine environment, then return the primary + @config.vm.defined_vms.each do |name, subvm| + return name if subvm.options[:primary] + end + + # If no primary was specified, nil it is + nil + end + + protected + + def find_vagrantfile(search_path) + ["Vagrantfile", "vagrantfile"].each do |vagrantfile| + current_path = search_path.join(vagrantfile) + return current_path if current_path.file? + end + + nil + end + end +end diff --git a/test/unit/vagrant/vagrantfile_test.rb b/test/unit/vagrant/vagrantfile_test.rb new file mode 100644 index 000000000..04885a0ae --- /dev/null +++ b/test/unit/vagrant/vagrantfile_test.rb @@ -0,0 +1,230 @@ +require File.expand_path("../../base", __FILE__) + +require "vagrant/vagrantfile" + +describe Vagrant::Vagrantfile do + include_context "unit" + + let(:keys) { [] } + let(:loader) { + Vagrant::Config::Loader.new( + Vagrant::Config::VERSIONS, Vagrant::Config::VERSIONS_ORDER) + } + + subject { described_class.new(loader, keys) } + + describe "#machine_config" do + let(:iso_env) { isolated_environment } + let(:boxes) { Vagrant::BoxCollection.new(iso_env.boxes_dir) } + + before do + keys << :test + end + + def configure(&block) + loader.set(:test, [["2", block]]) + end + + # A helper to register a provider for use in tests. + def register_provider(name, config_class=nil, options=nil) + provider_cls = Class.new(Vagrant.plugin("2", :provider)) + + register_plugin("2") do |p| + p.provider(name, options) { provider_cls } + + if config_class + p.config(name, :provider) { config_class } + end + end + + provider_cls + end + + it "should return a basic configured machine" do + register_provider("foo") + + configure do |config| + config.vm.box = "foo" + end + + config, _ = subject.machine_config(:default, :foo, boxes) + expect(config.vm.box).to eq("foo") + end + + it "configures with sub-machine config" do + register_provider("foo") + + configure do |config| + config.ssh.port = "1" + config.vm.box = "base" + + config.vm.define "foo" do |f| + f.ssh.port = 100 + end + end + + config, _ = subject.machine_config(:foo, :foo, boxes) + expect(config.vm.box).to eq("base") + expect(config.ssh.port).to eq(100) + end + + it "configures with box configuration if it exists" do + register_provider("foo") + + configure do |config| + config.vm.box = "base" + end + + iso_env.box2("base", :foo, vagrantfile: <<-VF) + Vagrant.configure("2") do |config| + config.ssh.port = 123 + end + VF + + config, _ = subject.machine_config(:default, :foo, boxes) + expect(config.vm.box).to eq("base") + expect(config.ssh.port).to eq(123) + end + + it "configures with box config of other supported formats" do + register_provider("foo", nil, box_format: "bar") + + configure do |config| + config.vm.box = "base" + end + + iso_env.box2("base", :bar, vagrantfile: <<-VF) + Vagrant.configure("2") do |config| + config.ssh.port = 123 + end + VF + + config, _ = subject.machine_config(:default, :foo, boxes) + expect(config.vm.box).to eq("base") + expect(config.ssh.port).to eq(123) + end + + it "loads provider overrides if set" do + register_provider("foo") + register_provider("bar") + + configure do |config| + config.ssh.port = 1 + config.vm.box = "base" + + config.vm.provider "foo" do |_, c| + c.ssh.port = 100 + end + end + + # Test with the override + config, _ = subject.machine_config(:default, :foo, boxes) + expect(config.vm.box).to eq("base") + expect(config.ssh.port).to eq(100) + + # Test without the override + config, _ = subject.machine_config(:default, :bar, boxes) + expect(config.vm.box).to eq("base") + expect(config.ssh.port).to eq(1) + end + + it "loads the proper box if in a provider override" do + register_provider("foo") + + configure do |config| + config.vm.box = "base" + + config.vm.provider "foo" do |_, c| + c.vm.box = "foobox" + end + end + + iso_env.box2("base", :foo, vagrantfile: <<-VF) + Vagrant.configure("2") do |config| + config.ssh.port = 123 + end + VF + + iso_env.box2("foobox", :foo, vagrantfile: <<-VF) + Vagrant.configure("2") do |config| + config.ssh.port = 234 + end + VF + + config, _ = subject.machine_config(:default, :foo, boxes) + expect(config.vm.box).to eq("foobox") + expect(config.ssh.port).to eq(234) + end + + it "raises an error if the machine is not found" do + expect { subject.machine_config(:foo, :foo, boxes) }. + to raise_error(Vagrant::Errors::MachineNotFound) + end + + it "raises an error if the provider is not found" do + expect { subject.machine_config(:default, :foo, boxes) }. + to raise_error(Vagrant::Errors::ProviderNotFound) + end + end + + describe "#machine_names" do + before do + keys << :test + end + + def configure(&block) + loader.set(:test, [["2", block]]) + end + + it "returns the default name when single-VM" do + configure { |config| } + + expect(subject.machine_names).to eq([:default]) + end + + it "returns all of the names in a multi-VM" do + configure do |config| + config.vm.define "foo" + config.vm.define "bar" + end + + expect(subject.machine_names).to eq( + [:foo, :bar]) + end + end + + describe "#primary_machine_name" do + before do + keys << :test + end + + def configure(&block) + loader.set(:test, [["2", block]]) + end + + it "returns the default name when single-VM" do + configure { |config| } + + expect(subject.primary_machine_name).to eq(:default) + end + + it "returns the designated machine in multi-VM" do + configure do |config| + config.vm.define "foo" + config.vm.define "bar", primary: true + config.vm.define "baz" + end + + expect(subject.primary_machine_name).to eq(:bar) + end + + it "returns nil if no designation in multi-VM" do + configure do |config| + config.vm.define "foo" + config.vm.define "baz" + end + + expect(subject.primary_machine_name).to be_nil + end + end +end