lib/avsh/vagrantfile_environment.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Avsh
  # This module is a hack to parse out the relevant config details from a
  # Vagrantfile, without incurring the overhead of loading Vagrant, which is
  # quite substantial.
  #
  # I tried making this work as a Vagrant plugin, but I couldn't get the
  # overhead below 900ms, and my goal is <100ms. Just doing "require 'vagrant'"
  # is nearly 100ms on my PC.
  module VagrantfileEnvironment
    # Fake Vagrant module that stubs out everything except what's needed to
    # extract config details.
    module Vagrant
      VERSION = '1.8.3'.freeze

      def self.has_plugin?(*) # rubocop:disable Style/PredicateName
        # Just lie and say we have the plugin, because a Vagrantfile that calls
        # this is probably doing dependency checking, and will terminate if this
        # doesn't return true. However, this could result in unwanted behavior
        # if the Vagrantfile does weird things like ensuring a plugin DOESN'T
        # exist. I can't think of any reason for doing that.
        true
      end

      def self.configure(*)
        yield FakeVagrantConfig
      end

      def self.method_missing(*) end # ignore everything else
    end

    # Based on https://github.com/mitchellh/vagrant/blob/v1.8.4/lib/vagrant/config/v2/dummy_config.rb
    module DummyConfig
      def self.method_missing(*)
        DummyConfig
      end
    end

    # Fake Vagrant::Config module
    module FakeVagrantConfig
      def self.vm
        @@fake_vm_config
      end

      # The FakeVMConfig instance is used to collect the config details we
      # care about. It's set as a class variable because we need to access it
      # after the Vagrantfile is loaded, and we can't tell the Vagrantfile to
      # use a specific instance of anything.
      # rubocop:disable Style/ClassVars
      def self.init_fake_vm_config(vagrantfile_path)
        @@fake_vm_config = FakeVMConfig.new(File.dirname(vagrantfile_path))
      end
      # rubocop:enable all

      def self.method_missing(*)
        DummyConfig
      end
    end

    # Collects config details for vm definitions
    class FakeVMConfig
      def initialize(vagrantfile_dir)
        @vagrantfile_dir = vagrantfile_dir
        @synced_folders = {}
        @machines = {}
        @primary_machine = nil
      end

      def synced_folder(src, dest, options = nil)
        # Hash by the guest directory because that's what Vagrant does:
        # https://github.com/mitchellh/vagrant/blob/v1.8.4/plugins/kernel_v2/config/vm.rb#L217
        @synced_folders[File.expand_path(dest, @vagrantfile_dir)] = {
          host_path: File.expand_path(src, @vagrantfile_dir),
          disabled: options && options.key?(:disabled)
        }
      end

      def define(machine_name, options = nil)
        @primary_machine = machine_name.to_s if options && options[:primary]

        machine_config = FakeVMConfig.new(@vagrantfile_dir)
        @machines[machine_name.to_s] = machine_config
        yield machine_config
      end

      def vm
        self
      end

      def method_missing(*)
        DummyConfig
      end

      def parsed_config(parsed_config_class = ParsedConfig)
        machine_synced_folders = @machines.map do |machine_name, machine_config|
          [machine_name, machine_config.synced_folders]
        end
        parsed_config_class.new(@vagrantfile_dir, synced_folders,
                                Hash[machine_synced_folders], @primary_machine)
      end

      protected

      attr_reader :synced_folders
    end

    # Handles loading a Vagrantfile so it'll communicate with this environment
    class Loader
      def initialize(logger)
        @logger = logger
      end

      def load_vagrantfile(vagrantfile_path)
        @logger.debug "Parsing Vagrantfile '#{vagrantfile_path}'"

        fake_vm_config = FakeVagrantConfig.init_fake_vm_config(vagrantfile_path)

        # Prep global namespace
        Object.const_set(:Vagrant, Vagrant)

        begin
          Kernel.load(vagrantfile_path)
        rescue ScriptError, StandardError => e
          # Re-raise with a more specific exception
          raise VagrantfileEvalError.new(vagrantfile_path, e)
        ensure
          # Clean up global namespace. This is really just for the tests, since
          # this isn't going to be called multiple times when executed normally.
          #
          # We could remove _all_ constants that were added by calling
          # Object.constants before and after the load and doing a diff, but
          # that will cause issues with classes that are require()d, since they
          # won't get reloaded if require() is called again.
          Object.send(:remove_const, :Vagrant)
          if Object.const_defined?(:VAGRANTFILE_API_VERSION)
            Object.send(:remove_const, :VAGRANTFILE_API_VERSION)
          end
        end

        fake_vm_config.parsed_config
      end
    end
  end
end