class Sfn::Planner::Aws

AWS specific planner

Constants

PLAN_CLASS
PLAN_MAP
REPLACE_ON_CFN_INIT_UPDATE

Resources that will be replaced on metadata init updates

Attributes

translators[RW]

@return [Smash] initialized translators

Public Class Methods

new(*_) click to toggle source

Simple overload to load in aws resource set from sparkleformation

Calls superclass method Sfn::Planner::new
# File lib/sfn/planner/aws.rb, line 236
def initialize(*_)
  super
  SfnAws.load!
  @translators = Smash.new
end

Public Instance Methods

generate_plan(template, parameters) click to toggle source

Generate update report

@param template [Hash] updated template @param parameters [Hash] runtime parameters for update

@return [Hash] report

# File lib/sfn/planner/aws.rb, line 248
def generate_plan(template, parameters)
  parameters = Smash[parameters.map { |k, v| [k, v.to_s] }]
  result = Smash.new(
    :stacks => Smash.new(
      origin_stack.name => plan_stack(
        origin_stack,
        template,
        parameters
      ),
    ),
    :added => Smash.new,
    :removed => Smash.new,
    :replace => Smash.new,
    :interrupt => Smash.new,
    :unavailable => Smash.new,
    :unknown => Smash.new,
  )
  convert_to_plan(result)
end

Protected Instance Methods

convert_to_plan(result) click to toggle source

Convert result hash into plan object

@param [Hash] plan hash data @return [Miasma::Models::Orchestration::Stack::Plan]

# File lib/sfn/planner/aws.rb, line 284
def convert_to_plan(result)
  plan = PLAN_CLASS.new(origin_stack)
  PLAN_MAP.each do |src, dst|
    collection = result[src].map do |name, info|
      item = PLAN_CLASS::Item.new(
        :name => name,
        :type => info[:type],
      )
      unless info[:diffs].empty?
        item.diffs = info[:diffs].map do |d_info|
          diff = PLAN_CLASS::Diff.new(
            :name => d_info.fetch(:property_name, d_info[:path]),
            :current => d_info[:original].to_json,
            :proposed => d_info[:updated].to_json,
          )
          diff.valid_state
        end
      end
      item.valid_state
    end
    plan.send("#{dst}=", collection)
  end
  plan.stacks = Smash[
    result.fetch(:stacks, {}).map { |name, info|
      [name, convert_to_plan(info)]
    }
  ]
  plan.valid_state
end
dereference_template(t_key, template, parameters, flagged = []) click to toggle source

Dereference all parameters within template to allow for processing using real values

@param t_key [String] @param template [Hash] @param parameters [Hash] @param flagged [Array<String>]

@return [Hash]

# File lib/sfn/planner/aws.rb, line 659
def dereference_template(t_key, template, parameters, flagged = [])
  template = template.to_smash
  translator = translator_for(t_key, template, parameters)
  flagged.each do |item|
    translator.flag_ref(item)
  end
  template.keys.each do |t_key|
    next if ["Outputs", "Resources"].include?(t_key)
    template[t_key] = translator.dereference_processor(
      template[t_key], ["DEREF"]
    )
  end
  translator.original.replace(template)
  ["Outputs", "Resources"].each do |t_key|
    if template[t_key]
      template[t_key] = translator.dereference_processor(
        template[t_key], ["DEREF", :all]
      )
    end
  end
  if template["Resources"]
    valid_resources = template["Resources"].map do |resource_name, resource_value|
      if resource_value["OnCondition"]
        if translator.apply_condition(resource_value["OnCondition"])
          resource_name
        end
      else
        resource_name
      end
    end.compact
    (template["Resources"].keys - valid_resources).each do |resource_to_remove|
      template["Resources"].delete(resource_to_remove)
    end
  end
  translator.original.replace({})
  template
end
diff_init(diff, path) click to toggle source

Initialize the diff result hash

@param diff [Array] Hashdiff result entry @param path [String] modification path within structure @return [Smash]

# File lib/sfn/planner/aws.rb, line 512
def diff_init(diff, path)
  Smash.new.tap do |di|
    if diff.size > 1
      updated = diff.find_all { |x| x.first == "+" }
      original = diff.find_all { |x| x.first == "-" }
      di[:original] = Array(original).map(&:last).join(", ")
      di[:updated] = Array(updated).map(&:last).join(", ")
    else
      diff_data = diff.first
      di[:path] = path
      if diff_data.size == 3
        di[diff_data.first == "+" ? :updated : :original] = diff_data.last
      else
        di[:original] = diff_data[diff_data.size - 2].to_s
        di[:updated] = diff_data.last.to_s
      end
    end
  end
end
get_global_parameters(stack) click to toggle source

Set global parameters available for all template translations. These are pseudo-parameters that are provided by the orchestration api runtime.

@return [Hash]

# File lib/sfn/planner/aws.rb, line 334
def get_global_parameters(stack)
  Smash.new(
    "AWS::Region" => stack.api.aws_region,
    "AWS::AccountId" => stack.id.split(":")[4],
    "AWS::NotificationARNs" => stack.notification_topics,
    "AWS::StackId" => stack.id,
    "AWS::StackName" => stack.name,
  ).merge(config.fetch(:planner, :global_parameters, {}))
end
is_stack?(type) click to toggle source

Check if resource type is stack resource type

@param type [String] @return [TrueClass, FalseClass]

# File lib/sfn/planner/aws.rb, line 391
def is_stack?(type)
  origin_stack.api.data.fetch(:stack_types, ["AWS::CloudFormation::Stack"]).include?(type)
end
plan_nested_stacks(stack, translator, origin_template, new_template_hash, plan_results) click to toggle source

Extract nested stacks and generate plans

@param stack [Miasma::Orchestration::Models::Stack] @param translator [Translator] @param origin_template [Smash] @param new_template_hash [Smash] @param plan_results [Smash] @return [NilClass]

# File lib/sfn/planner/aws.rb, line 466
def plan_nested_stacks(stack, translator, origin_template, new_template_hash, plan_results)
  origin_stacks = origin_template.fetch("Resources", {}).find_all do |s_name, s_val|
    is_stack?(s_val["Type"])
  end.map(&:first)
  new_stacks = (new_template_hash["Resources"] || {}).find_all do |s_name, s_val|
    is_stack?(s_val["Type"])
  end.map(&:first)
  [origin_stacks + new_stacks].flatten.compact.uniq.each do |stack_name|
    original_stack = stack.nested_stacks(false).detect do |stk|
      stk.data[:logical_id] == stack_name
    end
    new_stack_exists = is_stack?(new_template_hash.get("Resources", stack_name, "Type"))
    new_stack_template = new_template_hash.fetch("Resources", stack_name, "Properties", "Stack", Smash.new)
    new_stack_parameters = new_template_hash.fetch("Resources", stack_name, "Properties", "Parameters", Smash.new)
    new_stack_type = new_template_hash.fetch("Resources", stack_name, "Type",
                                             origin_template.get("Resources", stack_name, "Type"))
    resource = Smash.new(
      :name => stack_name,
      :type => new_stack_type,
      :properties => [],
    )
    if original_stack && new_stack_template
      new_stack_parameters = Smash[
        new_stack_parameters.map do |new_param_key, new_param_value|
          [new_param_key, translator.dereference(new_param_value)]
        end
      ]
      result = plan_stack(original_stack, new_stack_template, new_stack_parameters)
      result[:outputs].keys.each do |modified_output|
        translator.flag_ref("#{stack_name}_Outputs.#{modified_output}")
      end
      plan_results[:stacks][stack_name] = result
    elsif original_stack && (!new_stack_template && !new_stack_exists)
      plan_results[:removed][stack_name] = resource
    elsif new_stack_template && !original_stack
      plan_results[:added][stack_name] = resource
    end
  end
  nil
end
plan_stack(stack, new_template, new_parameters) click to toggle source

Generate plan for stack

@param stack [Miasma::Models::Orchestration::Stack] @param new_template [Hash] @param new_parameters [Hash] @return [Hash]

# File lib/sfn/planner/aws.rb, line 350
def plan_stack(stack, new_template, new_parameters)
  plan_results = Smash.new(
    :stacks => Smash.new,
    :added => Smash.new,
    :removed => Smash.new,
    :replace => Smash.new,
    :interrupt => Smash.new,
    :unavailable => Smash.new,
    :unknown => Smash.new,
    :outputs => Smash.new,
    :n_outputs => [],
  )

  origin_template = dereference_template(
    "#{stack.data.checksum}_origin",
    stack.template,
    Smash[
      stack.parameters.map do |k, v|
        [k, v.to_s]
      end
    ].merge(get_global_parameters(stack))
  )

  translator_key = "#{stack.data.checksum}_#{stack.data.fetch(:logical_id, stack.name)}"
  run_stack_diff(stack, translator_key, plan_results, origin_template, new_template, new_parameters)

  new_checksum = nil
  current_checksum = false
  until new_checksum == current_checksum
    current_checksum = plan_results.checksum
    run_stack_diff(stack, translator_key, plan_results, origin_template, new_template, new_parameters)
    new_checksum = plan_results.checksum
  end
  scrub_plan(plan_results)
  plan_results
end
register_diff(results, path, diff, translator, templates) click to toggle source

Register a diff item into the results set

@param results [Hash] @param path [String] @param diff [Array] @param templates [Smash] @option :templates [Smash] :origin @option :templates [Smash] :update

# File lib/sfn/planner/aws.rb, line 540
def register_diff(results, path, diff, translator, templates)
  diff_info = diff_init(diff, path)
  if path.start_with?("Resources")
    p_path = path.split(".")
    if p_path.size == 2
      diff = diff.first
      key = diff.first == "+" ? :added : :removed
      type = (key == :added ? templates[:update] : templates[:origin]).get("Resources", p_path.last, "Type")
      results[key][p_path.last] = Smash.new(
        :name => p_path.last,
        :type => type,
        :properties => [],
        :diffs => [
          diff_info,
        ],
      )
    else
      if p_path.include?("Properties")
        resource_name = p_path[1]
        if p_path.size < 4 && p_path.last == "Properties"
          property_name = diff.flatten.compact.last.keys.first
        else
          property_name = p_path[3].to_s.sub(/\[\d+\]$/, "")
        end
        type = templates.get(:origin, "Resources", resource_name, "Type")
        resource = Smash.new(
          :name => resource_name,
          :type => type,
          :properties => [property_name],
          :diffs => [
            diff_info.merge(:property_name => property_name),
          ],
        )
        begin
          if templates.get(:update, "Resources", resource_name, "Properties", property_name) == Translator::UNKNOWN_RUNTIME_RESULT
            effect = :unknown
          else
            r_info = SparkleFormation::Resources::Aws.resource_lookup(type)
            r_property = r_info.property(property_name)
            if r_property
              effect = r_property.update_causes(
                templates.get(:update, "Resources", resource_name),
                templates.get(:origin, "Resources", resource_name)
              )
            else
              raise KeyError.new "Unknown property"
            end
          end
          case effect.to_sym
          when :replacement
            set_resource(:replace, results, resource_name, resource)
          when :interrupt
            set_resource(:interrupt, results, resource_name, resource)
          when :unavailable
            set_resource(:unavailable, results, resource_name, resource)
          when :none
            # \o/
          else
            set_resource(:unknown, results, resource_name, resource)
          end
        rescue KeyError
          set_resource(:unknown, results, resource_name, resource)
        end
      elsif p_path.include?("AWS::CloudFormation::Init")
        resource_name = p_path[1]
        type = templates[:origin]["Resources"][resource_name]["Type"]
        if REPLACE_ON_CFN_INIT_UPDATE.include?(type)
          set_resource(:replace, results, resource_name,
                       Smash.new(
            :name => resource_name,
            :type => type,
            :properties => ["AWS::CloudFormation::Init"],
            :diffs => [
              diff_info,
            ],
          ))
        end
      end
    end
  elsif path.start_with?("Outputs")
    o_resource_name = path.split(".")[1]
    if o_resource_name
      set_resource(
        :outputs, results, o_resource_name,
        :properties => [],
        :diffs => [
          diff_info,
        ],
      )
    end
  end
end
run_stack_diff(stack, t_key, plan_results, origin_template, new_template, new_parameters) click to toggle source

Run the stack diff and populate the result set

@param stack [Miasma::Models::Orchestration::Stack] existing stack @param t_key [String] translator key @param plan_result [Smash] plan data to populate @param origin_template [Smash] template of existing stack @param new_template [Smash] template to replace existing @param new_parameters [Smash] parameters to be applied to update @return [NilClass]

# File lib/sfn/planner/aws.rb, line 422
def run_stack_diff(stack, t_key, plan_results, origin_template, new_template, new_parameters)
  translator = translator_for(t_key)
  new_parameters = new_parameters.dup
  if stack.parameters
    stack.parameters.each do |k, v|
      if new_parameters[k].is_a?(Hash)
        val = translator.dereference(new_parameters[k])
        new_parameters[k] = val == new_parameters[k] ? v : val
      end
    end
  end
  new_parameters.merge!(get_global_parameters(stack))
  new_template_hash = new_template.to_smash

  plan_nested_stacks(stack, translator, origin_template, new_template_hash, plan_results)

  scrub_stack_properties(new_template_hash)
  update_template = dereference_template(
    t_key, new_template_hash, new_parameters,
    plan_results[:replace].keys + plan_results[:unavailable].keys
  )

  HashDiff.diff(origin_template, MultiJson.load(MultiJson.dump(update_template))).group_by do |item|
    item[1]
  end.each do |a_path, diff_items|
    register_diff(
      plan_results, a_path, diff_items, translator_for(t_key),
      Smash.new(
        :origin => origin_template,
        :update => update_template,
      )
    )
  end
  nil
end
scrub_plan(results) click to toggle source

Scrub the plan results to only provide highest precedence diff items

@param results [Hash] @return [NilClass]

# File lib/sfn/planner/aws.rb, line 400
def scrub_plan(results)
  precedence = [:unavailable, :replace, :interrupt, :unavailable, :unknown]
  until precedence.empty?
    key = precedence.shift
    results[key].keys.each do |k|
      precedence.each do |p_key|
        results[p_key].delete(k)
      end
    end
  end
  nil
end
scrub_stack_properties(template) click to toggle source

Remote custom Stack property from Stack resources within template

@param template [Hash] @return [TrueClass]

# File lib/sfn/planner/aws.rb, line 318
def scrub_stack_properties(template)
  if template["Resources"]
    template["Resources"].each do |name, info|
      if is_stack?(info["Type"]) && info["Properties"].is_a?(Hash)
        info["Properties"].delete("Stack")
      end
    end
  end
  true
end
set_resource(kind, results, name, resource) click to toggle source

Set resource item into result set

@param kind [Symbol] @param results [Hash] @param name [String] @param resource [Hash]

# File lib/sfn/planner/aws.rb, line 639
def set_resource(kind, results, name, resource)
  if results[kind][name]
    results[kind][name][:properties] += resource[:properties]
    results[kind][name][:properties].uniq!
    results[kind][name][:diffs] += resource[:diffs]
    results[kind][name][:diffs].uniq!
  else
    results[kind][name] = resource
  end
end
translator_for(t_key, template = nil, parameters = nil) click to toggle source

Provide a translator instance for given key (new or cached instance)

@param t_key [String] identifier @param template [Hash] stack template @param parameters [Hash] stack parameters @return [Translator]

# File lib/sfn/planner/aws.rb, line 703
def translator_for(t_key, template = nil, parameters = nil)
  o_translator = translators[t_key]
  if template
    translator = Translator.new(template,
                                :parameters => parameters)
    if o_translator
      o_translator.flagged.each do |i|
        translator.flag_ref(i)
      end
    end
    translators[t_key] = translator
    o_translator = translator
  else
    unless o_translator
      o_translator = Translator.new({},
                                    :parameters => {})
    end
  end
  o_translator
end