Chris Roberts e958c6183a Adds initial HCP config support
Adds initial basic support for HCP based configuration in vagrant-go.
The initalization process has been updated to remove Vagrantfile parsing
from the client, moving it to the runner using init jobs for the basis
and the project (if there is one). Detection is done on the file based
on extension for Ruby based parsing or HCP based parsing.

Current HCP parsing is extremely simple and currently just a base to
build off. Config components will be able to implement an `Init`
function to handle receiving configuration data from a non-native source
file. This will be extended to include a default approach for injecting
defined data in the future.

Some cleanup was done in the state around validations. Some logging
adjustments were applied on the Ruby side for better behavior
consistency.

VirtualBox provider now caches locale detection to prevent multiple
checks every time the driver is initialized.
2023-09-07 17:26:10 -07:00

531 lines
20 KiB
Ruby

# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
require "digest/sha2"
require "google/protobuf/wrappers_pb"
require "google/protobuf/well_known_types"
module VagrantPlugins
module CommandServe
# Provides value mapping to ease interaction
# with protobuf and clients
class Mappers
# The default maps define the default mapping of proto
# messages to a proper Ruby type. This is used when
# mapping a value and a destination type is not provided.
DEFAULT_MAPS = {
Client::Project => Vagrant::Environment,
Client::Target => Vagrant::Machine,
Client::Terminal => Vagrant::UI::Remote,
Client::SyncedFolder => Vagrant::Plugin::Remote::SyncedFolder,
Google::Protobuf::BoolValue => Type::Boolean,
Google::Protobuf::BytesValue => String,
Google::Protobuf::DoubleValue => Float,
Google::Protobuf::FloatValue => Float,
Google::Protobuf::Int32Value => Integer,
Google::Protobuf::Int64Value => Integer,
Google::Protobuf::UInt32Value => Integer,
Google::Protobuf::UInt64Value => Integer,
Google::Protobuf::StringValue => String,
SDK::Args::Array => Array,
SDK::Args::ConfigData => Vagrant::Plugin::V2::Config,
SDK::Args::Class => Class,
SDK::Args::CorePluginManager => Client::CorePluginManager,
SDK::Args::Direct => Type::Direct,
SDK::Args::Folders => Type::Folders,
SDK::Args::Guest => Client::Guest,
SDK::Args::Hash => Hash,
SDK::Args::Host => Client::Host,
SDK::Args::NamedCapability => Symbol,
SDK::Args::Null => NilClass,
SDK::Args::Options => Type::Options,
SDK::Args::Path => Pathname,
SDK::Args::ProcRef => Proc,
SDK::Args::Project => Vagrant::Environment,
SDK::Args::Provider => Client::Provider,
SDK::Args::StateBag => Client::StateBag,
SDK::Args::SyncedFolder => Vagrant::Plugin::Remote::SyncedFolder,
SDK::Args::Target => Vagrant::Machine,
SDK::Args::TargetIndex => Client::TargetIndex,
SDK::Args::Target::Machine => Vagrant::Machine,
SDK::Args::TimeDuration => Type::Duration,
SDK::Args::TerminalUI => Vagrant::UI::Remote,
SDK::Command::Arguments => Type::CommandArguments,
SDK::Command::CommandInfo => Type::CommandInfo,
SDK::Communicator::Command => Type::CommunicatorCommandArguments,
SDK::Config::RawRubyValue => Object,
}
# The reverse maps define the default mapping from Ruby types
# to proto messages. This map is built by reversing the default
# maps. The key values are checked against the source value's
# class and its ancestors for a match. This is why the UI interface
# is merged into the map.
REVERSE_MAPS = Hash[DEFAULT_MAPS.values.zip(DEFAULT_MAPS.keys)].merge(
Vagrant::UI::Interface => SDK::Args::TerminalUI,
)
# Remove any top level classes
REVERSE_MAPS.delete_if { |k, _| !k.name.include?("::") }
# REVERSE_MAPS.delete(Object)
# @return [Symbol] marker value for failed direct conversions
FAILED_CONVERT = :__FAILED_CONVERT__
# Constant used for generating value
GENERATE_CLASS = Class.new {
def self.to_s
"[Value Generation]"
end
def to_s
"[Value Generation]"
end
def inspect
to_s
end
}
GENERATE = GENERATE_CLASS.new.freeze
include Util::HasLogger
autoload :Internal, Vagrant.source_root.join("plugins/commands/serve/mappers/internal").to_s
autoload :Mapper, Vagrant.source_root.join("plugins/commands/serve/mappers/mapper").to_s
# @return [Array<Object>] arguments provided to all mapper calls
attr_reader :known_arguments
# @return [Array<Mapper>] list of mappers
attr_reader :mappers
# @return [Util::Cacher] cached mapped values
attr_accessor :cacher
class << self
# @return [Array<Mapper>] frozen list of available mappers
def mappers
@mappers ||= Mapper.registered.map(&:new).freeze
end
# @return [Util::Cacher]
def cache
@cache ||= Util::Cacher.new
end
# Register a destination type for blind mappings
#
# @param src [Class] source type
# @param dst [Class] destination type
# @return [Class] destination type
def register_blind_map(src, dst)
@blind_map_registry[src] = dst
end
# Get a destination type for blind mapping if registered
#
# @param src [Class] source type
# @return [Class, NilClass] destination type or nil
def blind_map_for(src)
@blind_map_registry[src]
end
end
# Initialize our lookup table
@blind_map_registry = {}
# Create a new mappers instance. Any arguments provided will be
# available to all mapper calls
def initialize(*args)
@known_arguments = Array(args).compact
Mapper.generate_anys
@mappers = self.class.mappers
@cacher = self.class.cache
end
def initialize_copy(orig)
@mappers = orig.mappers.dup
@cacher = orig.cacher
@known_arguments = orig.known_arguments
end
# Add an argument to be included with mapping calls
#
# @param v [Object] Argument value
# @return [Object]
def add_argument(v)
if v.nil?
raise TypeError,
"Expected valid argument but received nil value"
end
known_arguments << v
v
end
# Convert Any proto message to actual message type
#
# @param any [Google::Protobuf::Any]
# @return [Google::Protobuf::MessageExts]
def unany(any)
type = find_type(any.type_name.split("/").last.to_s)
any.unpack(type)
end
# Get const from name
#
# @param name [String]
# @return [Class]
def find_type(name)
parent_module_options = []
name.to_s.split(".").inject(Object) { |memo, n|
c = memo.constants.detect { |mc| mc.to_s.downcase == n.to_s.downcase }
if c.nil?
parent_module_options.delete(memo)
parent_module_options.each do |pm|
c = pm.constants.detect { |mc| mc.to_s.downcase == n.to_s.downcase }
if !c.nil?
memo = pm
break
end
end
end
raise NameError,
"Failed to find constant for `#{name}'" if c.nil?
parent_module_options = memo.constants.select {
|mc| mc.to_s.downcase == n.to_s.downcase
}.map {
|mc| memo.const_get(mc)
}
memo.const_get(c)
}
end
# Attempt to directly convert value. If a destination type
# is provided, validate the direct conversion matches the
# desired type.
#
# @param value [Object] value to convert
# @param to [Class] Resultant type (optional)
# @return [Object]
def direct_convert(value, to:)
logger.debug { "direct conversion on #{value.class} to destination type #{to}" }
# If we don't have a destination, attempt to do direct conversion
if to.nil?
begin
logger.debug { "running direct blind pre-map on #{value.class}" }
return value.is_a?(Google::Protobuf::MessageExts) ? value.to_ruby : value.to_proto
rescue => err
logger.debug { "direct blind conversion failed in pre-map stage, reason: #{err}" }
end
end
if !to.nil?
# If we are mapping to an any, try doing it directly first
if to == Google::Protobuf::Any
begin
return value.to_any
rescue => err
logger.debug { "direct any conversion failed in pre-map stage, reason: #{err}"}
end
end
# If the destination type is a proto, try doing that directly
if to.ancestors.include?(Google::Protobuf::MessageExts)
begin
proto = value.to_proto
return proto if proto.is_a?(to)
rescue => err
logger.debug { "direct proto conversion failed in pre-map stage, reason: #{err}" }
end
end
# If the destination type is not a proto, but the value is, try that directly
if value.is_a?(Google::Protobuf::MessageExts) && !to.ancestors.include?(Google::Protobuf::MessageExts)
begin
val = value.to_ruby
return val if val.is_a?(to)
rescue => err
logger.debug { "direct ruby conversion failed in pre-map stage, reason: #{err}" }
end
end
end
FAILED_CONVERT
end
# Map a given value
#
# @param value [Object] Value to map
# @param named [String] Named argument to prefer
# @param to [Class] Resultant type (optional)
# @return [Object]
def map(value, *extra_args, named: nil, to: nil)
extra_args = [value, *extra_args].map do |a|
if a.is_a?(Type::NamedArgument)
name = a.name
a = a.value
end
if a.is_a?(Google::Protobuf::Any)
non_any = unany(a)
logger.debug { "extracted any proto message #{a.class} -> #{non_any.class}" }
a = non_any
end
if name
a = Type::NamedArgument.new(value: a, name: name)
end
a
end
value = extra_args.shift if value
# If we don't have a destination type provided, attempt
# to set it using our default maps
to = DEFAULT_MAPS[value.class] if to.nil?
if value != GENERATE && to.nil?
to = REVERSE_MAPS.detect do |k, v|
v if value.class.ancestors.include?(k) &&
v.ancestors.include?(Google::Protobuf::MessageExts)
end&.last
end
# If the value given is the desired type, just return the value
return value if value != GENERATE && !to.nil? && to != Object && value.is_a?(to)
# Let's try some shortcuts before we actually put in the work
# of doing all the mapping stuff
if value == GENERATE
extra_args.each do |ea|
val = direct_convert(ea, to: to)
return val if val != FAILED_CONVERT
end
else
val = direct_convert(value, to: to)
return val if val != FAILED_CONVERT
end
# These only work if we know our destination
logger.debug { "starting value mapping process #{value.class} -> #{to.nil? ? 'unknown' : to.inspect}" }
if value.nil? && to
val = (extra_args + known_arguments).detect do |item|
item.is_a?(to)
end
if val && val != GENERATE
return val
end
end
# NOTE: We set the cacher instance into the extra args
# instead of adding it as a known argument so if it is
# changed the correct instance will be used
extra_args << cacher
extra_args << self
# If the provided value is a protobuf value, just return that value
if value.is_a?(Google::Protobuf::Value)
logger.debug { "direct return of protobuf value contents - #{value.to_ruby}" }
return value.to_ruby
end
args = ([value] + extra_args.compact + known_arguments.compact)
result = nil
# For funcspec values, we want to pre-filter since they use
# a custom validator. This will prevent invalid paths.
if value.is_a?(SDK::FuncSpec::Value)
map_mapper = self.clone
valid_mappers = map_mapper.mappers.map do |m|
next if !m.inputs.first.valid?(SDK::FuncSpec::Value) &&
m.output.ancestors.include?(Google::Protobuf::MessageExts)
next m if !m.inputs.first.valid?(SDK::FuncSpec::Value) ||
m.inputs.first.valid?(value)
logger.trace { "removing mapper - invalid funcspec match - #{m}" }
nil
end.compact
map_mapper.mappers.replace(valid_mappers)
elsif value.is_a?(Google::Protobuf::MessageExts)
map_mapper = self.clone
valid_mappers = map_mapper.mappers.map do |m|
next if value.class == m.output
# next if value.is_a?(m.output)
m
end.compact
map_mapper.mappers.replace(valid_mappers)
else
map_mapper = self
end
# If we don't have a desired final type, test for mappers
# that are satisfied by the arguments we have and run that
# directly
if to.nil? && value != GENERATE && self.class.blind_map_for(value.class)
blind_to = self.class.blind_map_for(value.class)
logger.debug { "found existing blind mapping for type #{value.class} -> #{blind_to}" }
to = blind_to
end
if to.nil?
valid_outputs = []
cb = lambda do |k|
matches = map_mapper.mappers.find_all do |m|
m.inputs.first.valid?(k)
end
outs = matches.map(&:output)
to_search = outs - valid_outputs
valid_outputs |= outs
to_search.each do |o|
cb.call(o)
end
end
cb.call(value)
if valid_outputs.empty?
raise TypeError,
"No valid mappers found for input type `#{value.class}'"
end
valid_outputs.reverse!
valid_outputs.delete_if do |o|
(value.class.ancestors.include?(Google::Protobuf::MessageExts) &&
o.ancestors.include?(Google::Protobuf::MessageExts)) ||
o.ancestors.include?(value.class)
end
last_error = nil
valid_outputs.each do |out|
logger.debug { "attempting blind map #{value.class} -> #{out}" }
begin
m_graph = Internal::Graph::Mappers.new(
output_type: out,
mappers: map_mapper,
named: named,
input_values: args,
source: value != GENERATE ? value.class : nil,
)
result = m_graph.execute
to = out
break
rescue => err
logger.debug { "typeless mapping failure (non-critical): #{err} (input - #{value.class} / output #{out})" }
last_error = err
end
end
raise last_error if result.nil? && last_error
self.class.register_blind_map(value.class, to)
else
m_graph = Internal::Graph::Mappers.new(
output_type: to,
mappers: map_mapper,
named: named,
input_values: args,
source: value != GENERATE ? value.class : nil
)
result = m_graph.execute
end
logger.debug { "map of #{value.class} to #{to.nil? ? 'unknown' : to.inspect} => #{result.class}" }
if !result.is_a?(to)
raise TypeError,
"Value is not expected destination type `#{to}' (actual type: #{result.class})"
end
result
rescue => err
logger.debug { "mapping failed of #{value.class} to #{to.nil? ? 'unknown' : to.inspect} - #{err}" }
logger.debug { "#{err.class}: #{err}\n" + err.backtrace.join("\n") }
raise
end
# Generate the given type based on given and/or
# added arguments
def generate(*args, named: nil, type:)
map(GENERATE, *args, named: named, to: type)
end
# Map values provided by a FuncSpec request into
# actual values
#
# @param spec [SDK::FuncSpec::Spec]
# @param expect [Array<Class>] Expected types for each argument
# @return [Array<Object>, Object]
def funcspec_map(spec, *extra_args, expect: [])
expect = Array(expect)
args = spec.args.dup
# NOTE: the spec will have the order of the arguments
# shifted one. not sure why, but we can just work around
# it here for now.
args.push(args.shift)
# Start with unpacking the funcspec values so the #map method can
# apply known default expectations to values
args = args.map { |a| unfuncspec(a) }
# Now send the arguments through the mapping process
result = Array.new.tap do |result_args|
args.each_with_index do |arg, i|
logger.debug { "mapping funcspec value #{arg.class} to expected type #{expect[i]}" }
result_args << map(arg, *(extra_args + result_args), to: expect[i])
end
end
if result.size == 1
return result.first
end
result
end
# Extracts proto message from funcspec argument proto
#
# @param v [SDK::FuncSpec::Value]
# @return [Google:Protobuf::MessageExts]
def unfuncspec(v)
m = mappers.find_all { |map|
map.inputs.size == 1 &&
map.output.ancestors.include?(Google::Protobuf::MessageExts) &&
map.inputs.first.valid?(v)
}
if m.size > 1
raise TypeError,
"FuncSpec value of type `#{v.class}' matches more than one mapper (#{v})"
end
if m.empty?
raise ArgumentError,
"FuncSpec value of type `#{v.class}' has no valid mappers (#{v})"
end
result = m.first.call(v)
logger.trace { "converted funcspec argument #{v.class} -> #{result.class}" }
result
end
end
end
end
# NOTE: Always directly load mappers so they are automatically registered and
# available. Using autoloading behavior will result in them being unavailable
# until explicitly requested by name
require Vagrant.source_root.join("plugins/commands/serve/mappers/basis.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/box.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/capabilities.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/command.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/communicator.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/config_data.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/core_plugin_manager.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/direct.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/duration.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/environment.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/folders.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/guest.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/host.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/known_types.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/machine.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/options.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/pathname.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/plugin_manager.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/proc.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/project.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/provider.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/provisioner.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/push.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/state_bag.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/synced_folder.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/target.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/target_index.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/terminal.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/ui.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/vagrantfile.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/wrappers.rb").to_s