300 lines
11 KiB
Ruby

require "set"
module Vagrant
module Plugin
module V2
# This is the base class for a configuration key defined for
# V2. Any configuration key plugins for V2 should inherit from this
# class.
class Config
# This constant represents an unset value. This is useful so it is
# possible to know the difference between a configuration value that
# was never set, and a value that is nil (explicitly). Best practice
# is to initialize all variables to this value, then the {#merge}
# method below will "just work" in many cases.
UNSET_VALUE = Object.new
if Vagrant.server_mode?
GENERAL_CONFIG_CLS = Hashicorp::Vagrant::Sdk::Vagrantfile::GeneralConfig
SYMBOL_PROTO = Hashicorp::Vagrant::Sdk::SpecialTypes::Symbol
end
# This is called as a last-minute hook that allows the configuration
# object to finalize itself before it will be put into use. This is
# a useful place to do some defaults in the case the user didn't
# configure something or so on.
#
# An example of where this sort of thing is used or has been used:
# the "vm" configuration key uses this to make sure that at least
# one sub-VM has been defined: the default VM.
#
# The configuration object is expected to mutate itself.
def finalize!
# Default implementation is to do nothing.
end
# Merge another configuration object into this one. This assumes that
# the other object is the same class as this one. This should not
# mutate this object, but instead should return a new, merged object.
#
# The default implementation will simply iterate over the instance
# variables and merge them together, with this object overriding
# any conflicting instance variables of the older object. Instance
# variables starting with "__" (double underscores) will be ignored.
# This lets you set some sort of instance-specific state on your
# configuration keys without them being merged together later.
#
# @param [Object] other The other configuration object to merge from,
# this must be the same type of object as this one.
# @return [Object] The merged object.
def merge(other)
result = self.class.new
# Set all of our instance variables on the new class
[self, other].each do |obj|
obj.instance_variables.each do |key|
# Ignore keys that start with a double underscore. This allows
# configuration classes to still hold around internal state
# that isn't propagated.
if !key.to_s.start_with?("@__")
# Don't set the value if it is the unset value, either.
value = obj.instance_variable_get(key)
result.instance_variable_set(key, value) if value != UNSET_VALUE
end
end
end
# Persist through the set of invalid methods
this_invalid = @__invalid_methods || Set.new
other_invalid = other.instance_variable_get(:"@__invalid_methods") || Set.new
result.instance_variable_set(:"@__invalid_methods", this_invalid + other_invalid)
result
end
# Capture all bad configuration calls and save them for an error
# message later during validation.
def method_missing(name, *args, &block)
return super if @__finalized
# There are a few scenarios where ruby will attempt to implicity
# coerce a given object into a certain type. Configs can end up
# in some of these scenarios when they're being shipped around in
# callbacks with splats. If method_missing allows these methods to be
# called but continues to return Config back, Ruby will raise a
# TypeError. Doing the normal thing of raising NoMethodError allows
# Config to behave normally as its being passed through splats.
#
# For a bit more detail and some keywords for further searching, see:
# https://ruby-doc.org/core-2.7.2/doc/implicit_conversion_rdoc.html
if [:to_hash, :to_ary].include?(name)
return super
end
name = name.to_s
name = name[0...-1] if name.end_with?("=")
@__invalid_methods ||= Set.new
@__invalid_methods.add(name)
# Return the dummy object so that anything else works
::Vagrant::Config::V2::DummyConfig.new
end
# Allows setting options from a hash. By default this simply calls
# the `#{key}=` method on the config class with the value, which is
# the expected behavior most of the time.
#
# This is expected to mutate itself.
#
# @param [Hash] options A hash of options to set on this configuration
# key.
def set_options(options)
options.each do |key, value|
send("#{key}=", value)
end
end
# Converts this configuration object to JSON.
def to_json(*a)
instance_variables_hash.to_json(*a)
end
# A default to_s implementation.
def to_s
self.class.to_s
end
# Returns the instance variables as a hash of key-value pairs.
def instance_variables_hash
instance_variables.inject({}) do |acc, iv|
acc[iv.to_s[1..-1]] = instance_variable_get(iv)
acc
end
end
# Called after the configuration is finalized and loaded to validate
# this object.
#
# @param [Machine] machine Access to the machine that is being
# validated.
# @return [Hash]
def validate(machine)
return { self.to_s => _detected_errors }
end
# This returns any automatically detected errors.
#
# @return [Array<String>]
def _detected_errors
return [] if !@__invalid_methods || @__invalid_methods.empty?
return [I18n.t("vagrant.config.common.bad_field",
fields: @__invalid_methods.to_a.sort.join(", "))]
end
# An internal finalize call that no subclass should override.
def _finalize!
@__finalized = true
end
def stringify_map_keys(m)
if m.is_a?(Array)
m.each do |v|
if v.is_a?(Hash)
v.transform_keys!{|sk| sk.to_s}
stringify_map_keys(v)
next
end
if v.is_a?(Array)
stringify_map_keys(v)
next
end
end
elsif m.is_a?(Hash)
m.each do |k,v|
if v.is_a?(Hash)
v.transform_keys!{|sk| sk.to_s}
stringify_map_keys(v)
next
end
if v.is_a?(Array)
stringify_map_keys(v)
next
end
m[k] = v.to_s if v.is_a?(Symbol)
end
m.transform_keys!{|sk| sk.to_s}
end
end
def transform_symbols(m)
if m.is_a?(Array)
m.each do |v|
if v.is_a?(Hash)
v.transform_keys!{|sk| sk.to_s}
transform_symbols(v)
next
end
if v.is_a?(Array)
v.map!{|sk| sk.is_a?(Symbol) ? SYMBOL_PROTO.new(str: sk.to_s) : sk}
transform_symbols(v)
next
end
end
m.map!{|sk| sk.is_a?(Symbol) ? SYMBOL_PROTO.new(str: sk.to_s) : sk}
elsif m.is_a?(Hash)
m.each do |k,v|
if v.is_a?(Hash)
v.transform_keys!{|sk| sk.to_s}
transform_symbols(v)
next
end
if v.is_a?(Array)
v.map!{|sk| sk.is_a?(Symbol) ? SYMBOL_PROTO.new(str: sk.to_s) : sk}
transform_symbols(v)
next
end
m[k] = SYMBOL_PROTO.new(str: v.to_s) if v.is_a?(Symbol)
end
m.transform_keys!{|sk| sk.to_s}
end
end
def clean_up_config_object(config)
protoize = config
stringify_map_keys(protoize)
transform_symbols(protoize)
# Remote variables that are internal
protoize.delete_if{|k,v| k.start_with?("_") }
protoize
end
def build_proto_array(a)
out = Hashicorp::Vagrant::Sdk::Args::Array.new
a.each do |e|
if e.is_a?(Hash)
out.list << Google::Protobuf::Any.pack(build_proto_hash(e))
next
end
if e.is_a?(Array)
out.list << Google::Protobuf::Any.pack(build_proto_array(e))
next
end
if e.class.ancestors.include?(Google::Protobuf::MessageExts)
out.list << Google::Protobuf::Any.pack(e)
else
val = Google::Protobuf::Value.new
val.from_ruby(e)
out.list << Google::Protobuf::Any.pack(val)
end
end
return out
end
def build_proto_hash(h)
out = Hashicorp::Vagrant::Sdk::Args::Hash.new
h.each do |k, v|
if v.is_a?(Hash)
out.fields[k] = Google::Protobuf::Any.pack(build_proto_hash(v))
next
end
if v.is_a?(Array)
out.fields[k] = Google::Protobuf::Any.pack(build_proto_array(v))
next
end
if v.class.ancestors.include?(Google::Protobuf::MessageExts)
out.fields[k] = Google::Protobuf::Any.pack(v)
else
val = Google::Protobuf::Value.new
val.from_ruby(v)
out.fields[k] = Google::Protobuf::Any.pack(val)
end
end
return out
end
def to_proto(type)
protoize = self.instance_variables_hash
protoize.map do |k,v|
# Get embedded default struct
if v.is_a?(Vagrant.plugin("2", :config))
hashed_config = v.instance_variables_hash
hashed_config.delete_if{|k,v| k.start_with?("_") }
protoize[k] = hashed_config
end
end
protoize = clean_up_config_object(protoize)
config_struct = build_proto_hash(protoize)
config_any = Google::Protobuf::Any.pack(config_struct)
GENERAL_CONFIG_CLS.new(type: type, config: config_any)
end
end
end
end
end