429 lines
16 KiB
Ruby
429 lines
16 KiB
Ruby
require "digest/sha2"
|
|
require "google/protobuf/wrappers_pb"
|
|
|
|
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::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::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,
|
|
}
|
|
|
|
# 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?("::") }
|
|
|
|
# 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)
|
|
name.to_s.split(".").inject(Object) { |memo, n|
|
|
c = memo.constants.detect { |mc| mc.to_s.downcase == n.to_s.downcase }
|
|
raise NameError,
|
|
"Failed to find constant for `#{name}'" if c.nil?
|
|
memo.const_get(c)
|
|
}
|
|
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}" }
|
|
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? && value.is_a?(to)
|
|
|
|
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.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} -> #{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}" }
|
|
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.trace { "#{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.inspect} 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} -> #{result}" }
|
|
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/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/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/wrappers.rb").to_s
|