class Cfhighlander::Util::CloudFormation

Public Class Methods

collect_output_values(template) click to toggle source
# File lib/util/cloudformation.util.rb, line 328
def self.collect_output_values(template)
  output_vals = {}
  template.subcomponents.each do |sub_component|
    # we collect outputs only from inlined components
    model = sub_component.component_loaded.cfn_model_raw
    model['Outputs'].each do |name, value|
      output_vals[sub_component.component_loaded.name] = {} unless output_vals.key? sub_component.component_loaded.name
      output_vals[sub_component.component_loaded.name][name] = value['Value']
    end if model.key? 'Outputs'
  end
  return output_vals
end
collect_replacements(component, template, output_values) click to toggle source
# File lib/util/cloudformation.util.rb, line 240
def self.collect_replacements(component, template, output_values)
  replacements = {}

  # collect replacements for inlined components
  template.subcomponents.each do |sub_component|
    next unless sub_component.inlined
    component_loaded = sub_component.component_loaded
    replacements[component_loaded.name] = []
    sub_stack_def = component.cfn_model_raw['Resources'][sub_component.cfn_name]
    next unless sub_stack_def['Properties'].key? 'Parameters'
    params = sub_stack_def['Properties']['Parameters']
    params.each do |param_name, param_value|
      # if param value is hash, we may find output values
      # these should be replaced with inlined values
      if param_value.is_a? Hash
        outval_refs = find_outval_refs(param_value)
        outval_refs.each do |out_ref|
          # replacement only takes place if
          # source component is inlined as well
          # if source component is not inlined
          # it's output won't be collected
          source_sub_component = template.subcomponents.find {|sc| sc.component_loaded.name == out_ref[:component]}

          # if source component is not inlined we can replacement as-is
          next unless source_sub_component.inlined
          search = { 'Fn::GetAtt' => [
              out_ref[:component],
              "Outputs.#{out_ref[:outputName]}"
          ] }
          replacement = output_values[out_ref[:component]][out_ref[:outputName]]
          if param_value == search
            param_value = replacement
          else
            # parameter value may be deeper in the structure, e.g.
            # member of Fn::If intrinsic function
            node_replace(
                param_value,
                search,
                replacement
            )
          end if output_values.key? out_ref[:component]

        end
      end
      replacements[component_loaded.name] << {
          search: { 'Ref' => param_name },
          replace: param_value
      }
    end

  end

  # collect replacements to be performed on parameters of non-inlined components
  # that are referencing inlined components
  replacements[component.name] = []
  template.subcomponents.each do |sub_component|
    next if sub_component.inlined
    sub_stack_def = component.cfn_model_raw['Resources'][sub_component.cfn_name]
    next unless sub_stack_def['Properties'].key? 'Parameters'
    params = sub_stack_def['Properties']['Parameters']
    params.each do |param_name, param_value|
      if param_value.is_a? Hash
        outval_refs = find_outval_refs(param_value)

        # component is NOT inlined and has out references to components that MAY be inlined
        outval_refs.each do |out_ref|
          component_name = out_ref[:component]
          ref_sub_component = template.subcomponents.find {|sc| sc.name == component_name}

          if ref_sub_component.nil?
            raise Cfhighlander::Error, "unable to find outputs from component #{component_name} reference by parameters in component #{sub_component.name}"
          end

          if ref_sub_component.inlined
            # out refs here need to be replaced with actual values
            replacement = output_values[out_ref[:component]][out_ref[:outputName]]
            replacements[component.name] << {
                search: { 'Fn::GetAtt' => [component_name, "Outputs.#{out_ref[:outputName]}"] },
                replace: replacement
            }
          end
        end
      end
    end
  end
  return replacements
end
find_outval_refs(tree, outval_refs = []) click to toggle source
# File lib/util/cloudformation.util.rb, line 214
def self.find_outval_refs(tree, outval_refs = [])
  tree.each do |key, val|

    # if we have located get att, it may be output value
    if key == 'Fn::GetAtt'
      if val.is_a? Array and val.size == 2
        if val[1].start_with? 'Outputs.'
          component = val[0]
          output = val[1].split('.')[1]
          outval_refs << { component: component, outputName: output }
        end
      end
    elsif val.is_a? Hash or val.is_a? Array
      # however we may also find output deeper in the tree
      # example being FnIf(condition, out1, out2)
      find_outval_refs(val, outval_refs)
    end

  end if tree.is_a? Hash

  tree.each do |element|
    find_outval_refs(element, outval_refs)
  end if tree.is_a? Array
  return outval_refs
end
flattenCloudformation(args = {}) click to toggle source
# File lib/util/cloudformation.util.rb, line 12
def self.flattenCloudformation(args = {})

  component = args.fetch(:component)
  template = component.highlander_dsl

  # make sure all mappings, resources and conditions
  # are named uniquely in all of the templates
  flatten_key_names(component, template)
  Debug.debug_dump_cfn(template, 'namespace_flat')

  # collect output values
  output_values = collect_output_values(template)
  Debug.debug_dump(output_values, 'outputs')

  # collect referenced parameters and convert to replacements
  component_replacements = collect_replacements(component, template, output_values)
  Debug.debug_dump(component_replacements, 'replacements')

  # apply replacements in referenced templates
  process_replacements(component, template, component_replacements)
  Debug.debug_dump_cfn(template, 'transformed')

  # inline all of the resources
  inline_resources(component, template)

  # remove substacks
  remove_inlined_component_stacks(component, template)

  # return inlined model
  return component.cfn_model_raw
end
flatten_key_names(component, template) click to toggle source
# File lib/util/cloudformation.util.rb, line 460
def self.flatten_key_names(component, template)
  flatten_namespace('Conditions', component, template)
  Debug.debug_dump_cfn(template, 'flat.conditions')

  flatten_namespace('Mappings', component, template)
  Debug.debug_dump_cfn(template, 'flat.mappings')

  flatten_namespace('Resources', component, template)
  Debug.debug_dump_cfn(template, 'flat.resources')
end
flatten_namespace(element_type, component, template) click to toggle source
# File lib/util/cloudformation.util.rb, line 52
def self.flatten_namespace(element_type, component, template)
  if component.cfn_model_raw.key? element_type
    keys_taken = component.cfn_model_raw[element_type].keys
  else
    keys_taken = []
  end
  template.subcomponents.each do |sub_component|
    next unless sub_component.inlined
    model = sub_component.component_loaded.cfn_model_raw
    model[element_type].keys.each do |key|
      if keys_taken.include? key
        candidate = "#{sub_component.component_loaded.name}#{key}"
        counter = 1
        while keys_taken.include? candidate
          candidate = "#{sub_component.component_loaded.name}#{key}#{counter}"
          counter = counter + 1
        end
        actual_key = candidate
        # we need to replace all as
        # resources can reference conditions
        # outputs can and will reference resources
        model[element_type][actual_key] = model[element_type][key]
        model[element_type].delete(key)
        case element_type
        when 'Resources'
          rename_resource(model, key, actual_key)
        when 'Mappings'
          rename_mapping(model, key, actual_key)
        when 'Conditions'
          rename_condition(model, key, actual_key)
        when 'Outputs'
          # outputs are not effecting anything within the same template
        end
        keys_taken << actual_key
      else
        keys_taken << key
      end
    end if model.key? element_type
  end
end
inline_elements(element_name, component, template) click to toggle source
# File lib/util/cloudformation.util.rb, line 106
def self.inline_elements(element_name, component, template)
  parent_model = component.cfn_model_raw
  template.subcomponents.each do |sub_component|
    next unless sub_component.inlined
    model = sub_component.component_loaded.cfn_model_raw
    model[element_name].each do |resource, value|
      if sub_component.conditional
        # If the resource already has a conditon we need to combine it with the stack condition
        if element_name == 'Conditions'
          value = { "Fn::And" => [{"Condtion" => sub_component.condition}, value]}
        end
        # Adds the condition to the inlined resource if it doesn't already have a condition
        if element_name == 'Resources'
          value['Condition'] = sub_component.condition unless value.has_key?('Condition')
        end
      end
      # effective extraction of child resource into parent
      # allows for line components to use - or _ in the component name
      # and still generate valid references
      safe_resource_name = resource.gsub('-','').gsub('_','')
      unless element_name == 'Outputs' && resource.end_with?('CfTemplateUrl')
        parent_model[element_name] = {} unless parent_model.key? element_name
        parent_model[element_name][safe_resource_name] = value
      end
    end if model.key? element_name
  end
end
inline_resources(component, template) click to toggle source
# File lib/util/cloudformation.util.rb, line 93
def self.inline_resources(component, template)
  inline_elements('Conditions', component, template)
  inline_elements('Mappings', component, template)
  inline_elements('Resources', component, template)

  # outputs are renamed AFTER all of the other processing
  # has been done, as outputs are referenced. Only
  # outputs of inlined components are renamed
  flatten_namespace('Outputs', component, template)
  Debug.debug_dump_cfn(template, 'flat.outputs')
  inline_elements('Outputs', component, template)
end
node_replace(tree, search, replacement) click to toggle source

if hash is treated as collection of tree structures where each key in Hash is root of the tree and value is subtree replace hash subtree with another subtree

# File lib/util/cloudformation.util.rb, line 346
def self.node_replace(tree, search, replacement)
  if tree.is_a? Hash
    tree.each do |root, subtree|
      if subtree == search
        tree[root] = replacement
      elsif subtree.is_a? Hash or subtree.is_a? Array
        node_replace(subtree, search, replacement)
      end
    end
  elsif tree.is_a? Array
    tree.each do |element|
      if element == search
        tree[tree.index element] = replacement
      elsif element.is_a? Hash or element.is_a? Array
        node_replace(element, search, replacement)
      end
    end
  end
end
process_replacements(component, template, component_replacements) click to toggle source
# File lib/util/cloudformation.util.rb, line 134
def self.process_replacements(component, template, component_replacements)

  # replacement processing is done from least to most dependant component
  dependency_sorted_subcomponents = template.subcomponents.sort {|sc1, sc2|
    sc1_params = component.cfn_model_raw['Resources'][sc1.cfn_name]['Properties']['Parameters']
    sc2_params = component.cfn_model_raw['Resources'][sc2.cfn_name]['Properties']['Parameters']
    outval_refs_sc1 = find_outval_refs(sc1_params)
    outval_refs_sc2 = find_outval_refs(sc2_params)

    # if sc1 is dependant on sc2,
    # sc2 param outval refs should have sc1 output
    # and vice versa
    sc1_depends_sc2 = if outval_refs_sc1.find{|oref| oref[:component] == sc2.cfn_name}.nil? then false else true end
    sc2_depends_sc1 = if outval_refs_sc2.find{|oref| oref[:component] == sc1.cfn_name}.nil? then false else true end

    if (sc1_depends_sc2 and sc2_depends_sc1)
      raise StandardError, "Components #{sc1.cfn_name} and #{sc2.cfn_name} have circular dependency!!"
    end
    if sc1_depends_sc2 then
      +1
    elsif sc2_depends_sc1 then
      -1
    else
      0
    end
  }

  # process replacements in order from least dependant to
  # most dependant
  dependency_sorted_subcomponents.each_with_index do |sub_component, index|
    next unless sub_component.inlined
    component_name = sub_component.component_loaded.name
    if component_replacements.key? component_name
      if sub_component.component_loaded.cfn_model_raw.key? 'Outputs'
        outputs_apriori = duplicate(sub_component.component_loaded.cfn_model_raw['Outputs'])
      else
        outputs_apriori = {}
      end
      component_replacements[component_name].each do |replacement|
        node_replace(
            sub_component.component_loaded.cfn_model_raw,
            replacement[:search],
            replacement[:replace]
        ) # some of the component outputs may be changed and thus replacements need be updated
      end
      iteration_index = 2
      outputs_apriori.each do |out_name, out_value|
        value_after_transform = sub_component.component_loaded.cfn_model_raw['Outputs'][out_name]
        # value of the output was changed by replacement
        unless out_value == value_after_transform
          # for all downstream dependant components
          propagated_update_index = index + 1
          while propagated_update_index < dependency_sorted_subcomponents.size
            pc_name = dependency_sorted_subcomponents[propagated_update_index].component_loaded.name
            component_replacements[pc_name].each do |replacement|
              # replacements for dependant component needs to be updated as well
              replace = replacement[:replace]

              if out_value['Value'] == replace
                replacement[:replace] = value_after_transform['Value']
              else
                node_replace(replacement[:replace], out_value['Value'], value_after_transform['Value'])
              end

            end if component_replacements.include? pc_name
            propagated_update_index += 1
          end
          Debug.debug_dump(component_replacements, "replacements.#{iteration_index}")
          iteration_index += 1
        end
      end
    end
  end

  # process replacements on component itself
  component_replacements[component.name].each do |replacement|
    node_replace(component.cfn_model_raw, replacement[:search], replacement[:replace])
  end
end
remove_inlined_component_stacks(component, template) click to toggle source
# File lib/util/cloudformation.util.rb, line 44
def self.remove_inlined_component_stacks(component, template)
  model = component.cfn_model_raw
  template.subcomponents.each do |sub_component|
    next unless sub_component.inlined
    model['Resources'].delete(sub_component.cfn_name)
  end
end
rename_condition(tree, search, replacement) click to toggle source

rename cloudformation condition in cfn model

# File lib/util/cloudformation.util.rb, line 409
def self.rename_condition(tree, search, replacement)
  # conditions can be referenced by Fn::If and Condition => cond
  tree.keys.each do |k|
    v = tree[k]
    if k == 'Fn::If' and v[0] == search
      tree[k] = [replacement, v[1], v[2]]
    end

    if k == 'Condition' and v == search
      tree[k] = replacement
    end

    if v.is_a? Array or v.is_a? Hash
      rename_condition(v, search, replacement)
    end
  end if tree.is_a? Hash

  tree.each do |element|
    rename_condition(element, search, replacement)
  end if tree.is_a? Array

end
rename_mapping(tree, search, replacement) click to toggle source

rename cloudformation mapping in cfn model

# File lib/util/cloudformation.util.rb, line 390
def self.rename_mapping(tree, search, replacement)
  tree.keys.each do |k|
    v = tree[k]
    if k == 'Fn::FindInMap' and v[0] == search
      tree[k] = [replacement, v[1], v[2]]
    end

    if v.is_a? Array or v.is_a? Hash
      rename_mapping(v, search, replacement)
    end
  end if tree.is_a? Hash

  tree.each do |element|
    rename_mapping(element, search, replacement)
  end if tree.is_a? Array

end
rename_resource(tree, search, replacement) click to toggle source

rename cloudformation resource in model

# File lib/util/cloudformation.util.rb, line 367
def self.rename_resource(tree, search, replacement)
  tree.keys.each do |k|
    v = tree[k]
    if k == 'Ref' and v == search
      tree[k] = replacement
    end

    if k == 'Fn::GetAtt' and v[0] == search
      tree[k] = [replacement, v[1]]
    end

    if v.is_a? Array or v.is_a? Hash
      rename_resource(v, search, replacement)
    end
  end if tree.is_a? Hash

  tree.each do |element|
    rename_resource(element, search, replacement)
  end if tree.is_a? Array

end
value_replace(tree, search, replacement) click to toggle source
Replace single value in tree structure (represented)

either as Hash or Array with another value

# File lib/util/cloudformation.util.rb, line 434
def self.value_replace(tree, search, replacement)
  if tree.is_a? Hash
    tree.keys.each do |root|
      subtree = tree[root]
      if root == search
        tree[replacement] = subtree
        tree.delete(root)
      end
      if subtree == search
        tree[root] = replacement
      end
      if subtree.is_a? Hash or subtree.is_a? Array
        value_replace(subtree, search, replacement)
      end
    end
  elsif tree.is_a? Array
    tree.each do |element|
      if element == search
        tree[tree.index element] = replacement
      elsif element.is_a? Hash or element.is_a? Array
        value_replace(element, search, replacement)
      end
    end
  end
end