require 'sparkle_formation' require 'multi_json' require 'logger' class SparkleFormation # Translator class Translation autoload :Heat, 'sparkle_formation/translation/heat' autoload :Rackspace, 'sparkle_formation/translation/rackspace' include SparkleFormation::Utils::AnimalStrings include SparkleFormation::SparkleAttribute # @return [Hash] original template attr_reader :original # @return [Hash] current translation attr_reader :translated # @return [Hash] duplicated template (full deep copy) attr_reader :template # @return [Logger] current logger attr_reader :logger # @return [Hash] extra options (generally used by translation implementations) attr_reader :options # Create new instance # # @param template_hash [Hash] stack template # @param args [Hash] # @option args [Logger] :logger custom logger # @option args [Hash] :parameters parameters for stack creation # @option args [Hash] :options options for translation def initialize(template_hash, args={}) @original = template_hash.dup @template = template_hash.to_smash @translated = {} @logger = args.fetch(:logger, Logger.new($stdout)) @parameters = args[:parameters] || {} @options = args[:options] || {} end # @return [Hash] parameters for template def parameters Hash[ @original.fetch('Parameters', {}).map do |k,v| [k, v.fetch('Default', '')] end ].merge(@parameters) end # @return [Hash] mappings for template def mappings @original.fetch('Mappings', {}) end # @return [Hash] resources for template def resources @original.fetch('Resources', {}) end # @return [Hash] outputs for template def outputs @original.fetch('Outputs', {}) end # @return [Hash] resource mapping def map self.class.const_get(:MAP) end # Translate stack definition # # @return [TrueClass] def translate! template.each do |key, value| translate_method = "translate_#{snake(key.to_s)}".to_sym if(respond_to?(translate_method)) send(translate_method, value) else translate_default(key, value) end end true end # Default translation action if no mapping is provided # # @return [Object] value def translate_default(key, value) translated[key] = value end # Translate resource # # @param resource_name [String] # @param resource_args [Hash] # @return [Hash, NilClass] new resource Hash or nil def resource_translation(resource_name, resource_args) new_resource = {} lookup = map[:resources][resource_args['Type']] if(lookup.nil?) logger.warn "Failed to locate resource type: #{resource_args['Type']}" nil elsif(lookup == :delete) logger.warn "Deleting resource #{resource_name} due to configuration" nil else new_resource['Type'] = lookup[:name] if(resource_args['Properties']) new_resource['Properties'] = format_properties( :original_properties => resource_args['Properties'], :property_map => lookup[:properties], :new_resource => new_resource, :original_resource => resource_args ) end if(lookup[:finalizer]) send(lookup[:finalizer], resource_name, new_resource, resource_args) end resource_finalizer(resource_name, new_resource, resource_args) new_resource end end # Format the properties of the new resource # # @param args [Hash] # @option args [Hash] :original_properties # @option args [Hash] :property_map # @option args [Hash] :new_resource # @option args [Hash] :original_resource # @return [Hash] def format_properties(args) args[:new_resource]['Properties'] = {}.tap do |new_properties| args[:original_properties].each do |property_name, property_value| new_key = args[:property_map][property_name] if(new_key) if(new_key.is_a?(Symbol)) unless(new_key == :delete) new_key, new_value = send(new_key, property_value, :new_resource => args[:new_resource], :new_properties => new_properties, :original_resource => args[:original_resource] ) new_properties[new_key] = new_value end else new_properties[new_key] = property_value end else logger.warn "Failed to locate property conversion for `#{property_name}` on resource type `#{args[:new_resource]['Type']}`. Passing directly." new_properties[default_key_format(property_name)] = property_value end end end end # Translate provided resources # # @param value [Hash] resources hash # @return [Hash] def translate_resources(value) translated['Resources'] = {} translated['Resources'].tap do |modified_resources| value.each do |resource_name, resource_args| new_resource = resource_translation(resource_name, resource_args) if(new_resource) modified_resources[resource_name] = new_resource end end end end # Default formatting for keys # # @param key [String, Symbol] # @return [String, Symbol] def default_key_format(key) key end # Attempt to dereference name # # @param obj [Object] # @return [Object] def dereference(obj) result = obj if(obj.is_a?(Hash)) name = obj['Ref'] || obj['get_param'] if(name) p_val = parameters[name.to_s] if(p_val) result = p_val end end end result end # Provide name of resource # # @param obj [Object] # @return [String] name def resource_name(obj) case obj when Hash obj['Ref'] || obj['get_resource'] else obj.to_s end end # Process object through dereferencer. This will dereference names # and apply functions if possible. # # @param obj [Object] # @return [Object] def dereference_processor(obj, funcs=[]) case obj when Array obj = obj.map{|v| dereference_processor(v, funcs)} when Hash new_hash = {} obj.each do |k,v| new_hash[k] = dereference_processor(v, funcs) end obj = apply_function(new_hash, funcs) end obj end # Process object through name mapping # # @param obj [Object] # @param names [Array] enable renaming (:ref, :fn) # @return [Object] def rename_processor(obj, names=[]) case obj when Array obj = obj.map{|v| rename_processor(v, names)} when Hash new_hash = {} obj.each do |k,v| new_hash[k] = rename_processor(v, names) end obj = apply_rename(new_hash, names) end obj end # Apply function if possible # # @param hash [Hash] # @param names [Array] enable renaming (:ref, :fn) # @return [Hash] # @note remapping references two constants: # REF_MAPPING for Ref maps # FN_MAPPING for Fn maps def apply_rename(hash, names=[]) k,v = hash.first if(hash.size == 1) if(k.start_with?('Fn::')) {self.class.const_get(:FN_MAPPING).fetch(k, k) => attr_mapping(*v)} elsif(k == 'Ref') if(resources.has_key?(v)) {'get_resource' => v} else {'get_param' => self.class.const_get(:REF_MAPPING).fetch(v, v)} end else hash end else hash end end # Apply `GetAttr` mapping if available # # @param resource_name [String] # @param value [String] # @return [Array] def attr_mapping(resource_name, value) result = [resource_name, value] if(r = resources[resource_name]) attr_map = self.class.const_get(:FN_ATT_MAPPING) if(attr_map[r['Type']] && replacement = attr_map[r['Type']][value]) result = [resource_name, *[replacement].flatten.compact] end end result end # Apply function if possible # # @param hash [Hash] # @param funcs [Array] allowed functions # @return [Hash] # @note also allows 'Ref' within funcs to provide mapping # replacements using the REF_MAPPING constant def apply_function(hash, funcs=[]) k,v = hash.first if(hash.size == 1 && (k.start_with?('Fn') || k == 'Ref') && (funcs.empty? || funcs.include?(k))) case k when 'Fn::Join' v.last.join(v.first) when 'Fn::FindInMap' map_holder = mappings[v[0]] if(map_holder) map_item = map_holder[dereference(v[1])] if(map_item) map_item[v[2]] else raise "Failed to find mapping item! (#{v[0]} -> #{v[1]})" end else raise "Failed to find mapping! (#{v[0]})" end when 'Ref' {'Ref' => self.class.const_get(:REF_MAPPING).fetch(v, v)} else hash end else hash end end # @return [Hash] mapping for pseudo-parameters REF_MAPPING = {} # @return [Hash] mapping for intrinsic functions FN_MAPPING = {} end end