class Kumogata2::Client

Public Class Methods

new(options) click to toggle source
# File lib/kumogata2/client.rb, line 4
def initialize(options)
  @options = options.kind_of?(Hashie::Mash) ? options : Hashie::Mash.new(options)
  @client = nil
  @resource = nil
  @plugin_by_ext = {}
end

Public Instance Methods

convert(path_or_url) click to toggle source
# File lib/kumogata2/client.rb, line 83
def convert(path_or_url)
  template = open_template(path_or_url)
  convert0(template)
end
create(path_or_url, stack_name = nil) click to toggle source
# File lib/kumogata2/client.rb, line 18
def create(path_or_url, stack_name = nil)
  stack_name = normalize_stack_name(stack_name)
  validate_stack_name(stack_name) if stack_name
  template = open_template(path_or_url)
  update_deletion_policy(template, delete_stack: !stack_name)

  outputs = create_stack(template, stack_name)

  unless @options.detach?
    post_process(path_or_url, outputs)
  end
end
delete(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 44
def delete(stack_name)
  stack_name = normalize_stack_name(stack_name)
  validate_stack_name(stack_name)
  get_resource.stack(stack_name).stack_status

  if @options.force? or agree("Are you sure you want to delete `#{stack_name}`? ".yellow)
    delete_stack(stack_name)
  end
end
deploy(path_or_url, stack_name = nil) click to toggle source
# File lib/kumogata2/client.rb, line 54
def deploy(path_or_url, stack_name = nil)
  stack_name = normalize_stack_name(stack_name)
  validate_stack_name(stack_name) if stack_name
  template = open_template(path_or_url)
  update_deletion_policy(template, delete_stack: !stack_name)

  change_set = create_change_set(template, stack_name)
  execute_change_set(change_set)
end
describe(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 11
def describe(stack_name)
  stack_name = normalize_stack_name(stack_name)
  validate_stack_name(stack_name)
  stack = describe_stack(stack_name)
  JSON.pretty_generate(stack).colorize_as(:json)
end
diff(path_or_url1, path_or_url2) click to toggle source
# File lib/kumogata2/client.rb, line 88
def diff(path_or_url1, path_or_url2)
  templates = [path_or_url1, path_or_url2].map do |path_or_url|
    template = nil

    if path_or_url =~ %r|\Astack://(.*)|
      stack_name = $1 || ''
      validate_stack_name(stack_name)
      template = export_template(stack_name)
    else
      template = open_template(path_or_url)
    end

    template = Kumogata2::Utils.stringify(template)
    JSON.pretty_generate(template)
  end

  diff_opts = @options.ignore_all_space? ? '-uw' : '-u'
  opts = {:include_diff_info => true, :diff => diff_opts}
  diff = Diffy::Diff.new(*templates, opts).to_s

  diff.sub(/^(\e\[\d+m)?\-\-\-(\s+)(\S+)/m) { "#{$1}---#{$2}#{path_or_url1}"}
      .sub(/^(\e\[\d+m)?\+\+\+(\s+)(\S+)/m) { "#{$1}+++#{$2}#{path_or_url2}"}
end
dry_run(path_or_url, stack_name = nil) click to toggle source
# File lib/kumogata2/client.rb, line 112
def dry_run(path_or_url, stack_name = nil)
  stack_name = normalize_stack_name(stack_name)
  validate_stack_name(stack_name) if stack_name
  template = open_template(path_or_url)
  update_deletion_policy(template, delete_stack: !stack_name)
  changes = show_change_set(template, stack_name)
  changes = JSON.pretty_generate(changes).colorize_as(:json) if changes
  changes
end
export(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 76
def export(stack_name)
  stack_name = normalize_stack_name(stack_name)
  validate_stack_name(stack_name)
  template = export_template(stack_name)
  convert0(template)
end
list(stack_name = nil) click to toggle source
# File lib/kumogata2/client.rb, line 69
def list(stack_name = nil)
  stack_name = normalize_stack_name(stack_name)
  validate_stack_name(stack_name) if stack_name
  stacks = describe_stacks(stack_name)
  JSON.pretty_generate(stacks).colorize_as(:json)
end
show_events(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 122
def show_events(stack_name)
  stack_name = normalize_stack_name(stack_name)
  validate_stack_name(stack_name)
  events = describe_events(stack_name)
  JSON.pretty_generate(events).colorize_as(:json)
end
show_outputs(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 129
def show_outputs(stack_name)
  stack_name = normalize_stack_name(stack_name)
  validate_stack_name(stack_name)
  outputs = describe_outputs(stack_name)
  JSON.pretty_generate(outputs).colorize_as(:json)
end
show_resources(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 136
def show_resources(stack_name)
  stack_name = normalize_stack_name(stack_name)
  validate_stack_name(stack_name)
  resources = describe_resources(stack_name)
  JSON.pretty_generate(resources).colorize_as(:json)
end
template_summary(path_or_url) click to toggle source
# File lib/kumogata2/client.rb, line 143
def template_summary(path_or_url)
  params = {}

  if path_or_url =~ %r|\Astack://(.*)|
    stack_name = $1 || ''
    validate_stack_name(stack_name)
    params[:stack_name] = stack_name
  else
    template = open_template(path_or_url)
    params[:template_body] = JSON.pretty_generate(template)
  end

  summary = describe_template_summary(params)
  JSON.pretty_generate(summary).colorize_as(:json)
end
update(path_or_url, stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 31
def update(path_or_url, stack_name)
  stack_name = normalize_stack_name(stack_name)
  validate_stack_name(stack_name)
  template = open_template(path_or_url)
  update_deletion_policy(template, update_metadate: true)

  outputs = update_stack(template, stack_name)

  unless @options.detach?
    post_process(path_or_url, outputs)
  end
end
validate(path_or_url) click to toggle source
# File lib/kumogata2/client.rb, line 64
def validate(path_or_url)
  template = open_template(path_or_url)
  validate_template(template)
end

Private Instance Methods

changes_for(change_set) click to toggle source
# File lib/kumogata2/client.rb, line 653
def changes_for(change_set)
  change_set.changes.map do |change|
    resource_change = change.resource_change
    change_hash = {}

    [
      :action,
      :logical_resource_id,
      :physical_resource_id,
      :resource_type,
    ].each do |k|
      change_hash[Kumogata2::Utils.camelize(k)] = resource_change[k]
    end

    change_hash['Details'] = resource_change.details.map do |detail|
      {
        attribute: detail.target.attribute,
        name: detail.target.name,
      }
    end

    change_hash
  end
end
convert0(template) click to toggle source
# File lib/kumogata2/client.rb, line 462
def convert0(template)
  ext = get_output_format
  plugin = find_or_create_plugin('xxx.' + ext)

  if plugin
    plugin.dump(template)
  else
    raise "Unknown format: #{ext}"
  end
end
convert_output_value(value, color = true) click to toggle source
# File lib/kumogata2/client.rb, line 705
def convert_output_value(value, color = true)
  ext = get_output_format
  Kumogata2::Plugin.plugin_by_name.each do |type, plugin|
    next unless plugin[:ext].include? ext

    plugin_instance = find_or_create_plugin('xxx.' + ext)
    case type
    when 'json'
      return plugin_instance.dump(value, color)
    when 'yaml'
      return plugin_instance.dump(value, color)
    end
  end
  value
end
create_change_set(template, stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 331
def create_change_set(template, stack_name)
  change_set_name = [stack_name, SecureRandom.uuid].join('-')

  begin
    stack = describe_stack(stack_name)
    case stack[:stack_status]
    when /_FAILED\z/
      log(:error, "Stack #{stack_name} status is now failed - #{stack[:stack_status]}", color: :red)
      return
    when /^DELETE_/
      log(:error, "Stack #{stack_name} statis is now delete - #{stack[:stack_status]}", color: :red)
      return
    when 'REVIEW_IN_PROGRESS'
      change_set_type = 'CREATE'
    else
      change_set_type = 'UPDATE'
    end
  rescue
    change_set_type = 'CREATE'
  end

  log(:info, "Creating ChangeSet: #{change_set_name} for #{stack_name}", color: :cyan)

  params = {
    stack_name: stack_name,
    change_set_name: change_set_name,
    template_body: convert_output_value(template, false),
    parameters: parameters_array,
    change_set_type: change_set_type,
  }

  params.merge!(set_api_params(params,
    :use_previous_template,
    :notification_arns,
    :capabilities,
    :resource_types,
    :tags)
  )

  resp = get_client.create_change_set(params)
  change_set_arn = resp.id

  completed, change_set = wait_change_set(change_set_arn, 'CREATE_COMPLETE')

  unless completed
    log(:error, "Create ChangeSet failed: #{change_set.status_reason}", color: :red)
  end

  change_set
end
create_event_log(stack) click to toggle source
# File lib/kumogata2/client.rb, line 590
def create_event_log(stack)
  event_log = {}

  events_for(stack).sort_by {|i| i['Timestamp'] }.each do |event|
    event_id = event['EventId']
    event_log[event_id] = event
  end

  return event_log
end
create_stack(template, stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 183
def create_stack(template, stack_name)
  stack_will_be_deleted = !stack_name

  unless stack_name
    stack_name = random_stack_name
  end

  log(:info, "Creating stack: #{stack_name}", color: :cyan)

  params = {
    stack_name: stack_name,
    template_body: convert_output_value(template, false),
    parameters: parameters_array,
  }

  params.merge!(set_api_params(params,
    :disable_rollback,
    :timeout_in_minutes,
    :notification_arns,
    :capabilities,
    :resource_types,
    :on_failure,
    :stack_policy_body,
    :stack_policy_url,
    :tags)
  )

  stack = get_resource.create_stack(params)

  return if @options.detach?

  completed = wait(stack, 'CREATE_COMPLETE')

  unless completed
    raise_stack_error!(stack, 'Create failed')
  end

  outputs = outputs_for(stack)
  summaries = resource_summaries_for(stack)

  if stack_will_be_deleted
    delete_stack(stack_name)
  end

  output_result(stack_name, outputs, summaries)

  outputs
end
delete_change_set(change_set) click to toggle source
# File lib/kumogata2/client.rb, line 405
def delete_change_set(change_set)
  log(:info, "Deleting ChangeSet: #{change_set.change_set_name}", color: :red)

  get_client.delete_change_set(stack_name: change_set.stack_name,
                               change_set_name: change_set.change_set_id)

  begin
    completed, _ = wait_change_set(change_set.change_set_id, 'DELETE_COMPLETE')
  rescue Aws::CloudFormation::Errors::ChangeSetNotFound
    # Handle `ChangeSet does not exist`
    completed = true
  end

  completed
end
delete_stack(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 277
def delete_stack(stack_name)
  stack = get_resource.stack(stack_name)
  stack.stack_status

  log(:info, "Deleting stack: #{stack_name}", color: :red)
  event_log = create_event_log(stack)
  stack.delete

  return if @options.detach?

  completed = false

  begin
    # XXX: Reacquire the stack
    stack = get_resource.stack(stack_name)
    completed = wait(stack, 'DELETE_COMPLETE', event_log)
  rescue Aws::CloudFormation::Errors::ValidationError
    # Handle `Stack does not exist`
    completed = true
  end

  unless completed
    raise_stack_error!(stack, 'Delete failed')
  end

  log(:info, 'Delete stack successfully')
end
describe_events(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 435
def describe_events(stack_name)
  stack = get_resource.stack(stack_name)
  stack.stack_status
  events_for(stack)
end
describe_outputs(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 441
def describe_outputs(stack_name)
  stack = get_resource.stack(stack_name)
  stack.stack_status
  outputs_for(stack)
end
describe_resources(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 447
def describe_resources(stack_name)
  stack = get_resource.stack(stack_name)
  stack.stack_status
  resource_summaries_for(stack)
end
describe_stack(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 178
def describe_stack(stack_name)
  resp = get_client.describe_stacks(stack_name: stack_name)
  resp.stacks.first.to_h
end
describe_stacks(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 310
def describe_stacks(stack_name)
  params = {}
  params[:stack_name] = stack_name if stack_name

  get_resource.stacks(params).map do |stack|
    {
      'StackName'    => stack.name,
      'CreationTime' => stack.creation_time,
      'StackStatus'  => stack.stack_status,
      'Description'  => stack.description,
    }
  end
end
describe_template_summary(params) click to toggle source
# File lib/kumogata2/client.rb, line 453
def describe_template_summary(params)
  resp = get_client.get_template_summary(params)
  resp.to_h
end
events_for(stack) click to toggle source
# File lib/kumogata2/client.rb, line 601
def events_for(stack)
  stack.events.map do |event|
    event_hash = {}

    [
      :event_id,
      :logical_resource_id,
      :physical_resource_id,
      :resource_properties,
      :resource_status,
      :resource_status_reason,
      :resource_type,
      :stack_id,
      :stack_name,
      :timestamp,
    ].each do |k|
      event_hash[Kumogata2::Utils.camelize(k)] = event.send(k)
    end

    event_hash
  end
end
execute_change_set(change_set) click to toggle source
# File lib/kumogata2/client.rb, line 382
def execute_change_set(change_set)
  if change_set.status != 'CREATE_COMPLETE'
    log(:info, "ChangeSet status is #{change_set.status}", color: :red)
    delete_change_set(change_set)
    return
  end

  log(:info, "Executing ChangeSet: #{change_set.change_set_name} for #{change_set.stack_name}", color: :cyan)

  get_client.execute_change_set(change_set_name: change_set.change_set_name,
                                stack_name: change_set.stack_name)

  stack = get_resource.stack(change_set.stack_name)

  event_log = create_event_log(stack)

  completed = wait(stack, stack.stack_status, event_log)

  unless completed
    raise_stack_error!(stack, 'executing change set failed')
  end
end
export_template(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 324
def export_template(stack_name)
  stack = get_resource.stack(stack_name)
  stack.stack_status
  template = stack.client.get_template(stack_name: stack_name).template_body
  JSON.parse(template)
end
find_or_create_plugin(path_or_url) click to toggle source
# File lib/kumogata2/client.rb, line 484
def find_or_create_plugin(path_or_url)
  ext = File.extname(path_or_url).sub(/\A\./, '')

  if @plugin_by_ext.has_key?(ext)
    return @plugin_by_ext.fetch(ext)
  end

  plugin_class = Kumogata2::Plugin.find_by_ext(ext)
  plugin = plugin_class ? plugin_class.new(@options) : nil
  @plugin_by_ext[ext] = plugin
end
get_client() click to toggle source
# File lib/kumogata2/client.rb, line 161
def get_client
  return @client unless @client.nil?

  # https://github.com/aws/aws-sdk-ruby/blob/v2.3.11/aws-sdk-core/lib/aws-sdk-core/plugins/regional_endpoint.rb#L29
  unless @options[:aws][:region]
    raise "missing region; use '--region' option or export region name to ENV['AWS_REGION']"
  end

  @client = Aws::CloudFormation::Client.new(@options.aws)
end
get_output_filename(value, stack_name = '') click to toggle source
# File lib/kumogata2/client.rb, line 721
def get_output_filename(value, stack_name = '')
  ext = get_output_format
  Kumogata2::Plugin.plugin_by_name.each do |type, plugin|
    if plugin[:ext].include? ext
      plugin_ext = plugin[:ext].first
      filename = stack_name.empty? ? File.basename(value, '.yaml') : stack_name
      return "#{filename}.#{plugin_ext}"
    end
  end
  value
end
get_output_format() click to toggle source
# File lib/kumogata2/client.rb, line 458
def get_output_format
  @options.output_format || 'template'
end
get_resource() click to toggle source
# File lib/kumogata2/client.rb, line 172
def get_resource
  return @resource unless @resource.nil?
  get_client if @client.nil?
  @resource = Aws::CloudFormation::Resource.new(client: @client)
end
normalize_stack_name(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 757
def normalize_stack_name(stack_name)
  if %r|\Astack://| =~ stack_name
    stack_name.sub(%r|\Astack://|, '')
  else
    stack_name
  end
end
open_template(path_or_url) click to toggle source
# File lib/kumogata2/client.rb, line 473
def open_template(path_or_url)
  plugin = find_or_create_plugin(path_or_url)

  if plugin
    @options.path_or_url = path_or_url
    plugin.parse(open(path_or_url, &:read))
  else
    raise "Unknown format: #{path_or_url}"
  end
end
output_result(stack_name, outputs, summaries) click to toggle source
# File lib/kumogata2/client.rb, line 678
  def output_result(stack_name, outputs, summaries)
    puts <<-EOS

Stack Resource Summaries:
#{JSON.pretty_generate(summaries).colorize_as(:json)}

Outputs:
#{JSON.pretty_generate(outputs).colorize_as(:json)}
EOS

    if @options.result_log?
      logname = get_output_filename(@options.result_log, stack_name)
      puts <<-EOS

(Save to `#{logname}`)
      EOS

      open(logname, 'wb') do |f|
        f.puts convert_output_value({
          'StackName' => stack_name,
          'StackResourceSummaries' => summaries,
          'Outputs' => outputs,
        })
      end
    end
  end
outputs_for(stack) click to toggle source
# File lib/kumogata2/client.rb, line 624
def outputs_for(stack)
  outputs_hash = {}

  stack.outputs.each do |output|
    outputs_hash[output.output_key] = output.output_value
  end

  outputs_hash
end
parameters_array() click to toggle source
# File lib/kumogata2/client.rb, line 516
def parameters_array
  @options.parameters.map do |key, value|
    {parameter_key: key, parameter_value: value}
  end
end
post_process(path_or_url, outputs) click to toggle source
# File lib/kumogata2/client.rb, line 733
def post_process(path_or_url, outputs)
  plugin = find_or_create_plugin(path_or_url)

  if plugin and plugin.respond_to?(:post)
    plugin.post(outputs)
  end
end
print_event_log(stack, event_log) click to toggle source
raise_stack_error!(stack, message) click to toggle source
# File lib/kumogata2/client.rb, line 741
def raise_stack_error!(stack, message)
  errmsgs = [message]
  errmsgs << stack.name
  errmsgs << stack.stack_status_reason if stack.stack_status_reason
  raise errmsgs.join(': ')
end
random_stack_name() click to toggle source
# File lib/kumogata2/client.rb, line 748
def random_stack_name
  stack_name = ['kumogata']
  user_host = Kumogata2::Utils.get_user_host
  stack_name << user_host if user_host
  stack_name << SecureRandom.uuid
  stack_name = stack_name.join('-')
  stack_name.gsub(/[^-a-zA-Z0-9]+/, '-').gsub(/-+/, '-')
end
resource_summaries_for(stack) click to toggle source
# File lib/kumogata2/client.rb, line 634
def resource_summaries_for(stack)
  stack.resource_summaries.map do |summary|
    summary_hash = {}

    [
      :logical_resource_id,
      :physical_resource_id,
      :resource_type,
      :resource_status,
      :resource_status_reason,
      :last_updated_timestamp
    ].each do |k|
      summary_hash[Kumogata2::Utils.camelize(k)] = summary.send(k)
    end

    summary_hash
  end
end
set_api_params(params, *keys) click to toggle source
# File lib/kumogata2/client.rb, line 522
def set_api_params(params, *keys)
  {}.tap do |h|
    keys.each do |k|
      @options[k].collect! do |v|
        key, value = v.split('=')
        { key: key, value: value }
       end if k == :tags and not @options[k].nil?
      h[k] = @options[k] if @options[k]
    end
  end
end
show_change_set(template, stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 421
def show_change_set(template, stack_name)
  output = nil

  change_set = create_change_set(template, stack_name)
  output = changes_for(change_set) unless change_set.nil?

  delete_change_set(change_set)

  stack = get_resource.stack(stack_name)
  delete_stack(stack_name) if stack.stack_status == 'REVIEW_IN_PROGRESS'

  output
end
update_deletion_policy(template, options = {}) click to toggle source
# File lib/kumogata2/client.rb, line 496
def update_deletion_policy(template, options = {})
  if options[:delete_stack] or @options.deletion_policy_retain?
    template['Resources'].each do |k, v|
      next if /\AAWS::CloudFormation::/ =~ v['Type']
      v['DeletionPolicy'] ||= 'Retain'

      if options[:update_metadate]
        v['Metadata'] ||= {}
        v['Metadata']['DeletionPolicyUpdateKeyForKumogata'] = "DeletionPolicyUpdateValueForKumogata#{Time.now.to_i}"
      end
    end
  end
end
update_stack(template, stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 232
def update_stack(template, stack_name)
  stack = get_resource.stack(stack_name)
  stack.stack_status

  log(:info, "Updating stack: #{stack_name}", color: :green)

  params = {
    stack_name: stack_name,
    template_body: convert_output_value(template, false),
    parameters: parameters_array,
  }

  params.merge!(set_api_params(params,
    :use_previous_template,
    :stack_policy_during_update_body,
    :stack_policy_during_update_url,
    :notification_arns,
    :capabilities,
    :resource_types,
    :stack_policy_body,
    :stack_policy_url,
    :tags)
  )

  event_log = create_event_log(stack)
  stack.update(params)

  return if @options.detach?

  # XXX: Reacquire the stack
  stack = get_resource.stack(stack_name)
  completed = wait(stack, 'UPDATE_COMPLETE', event_log)

  unless completed
    raise_stack_error!(stack, 'Update failed')
  end

  outputs = outputs_for(stack)
  summaries = resource_summaries_for(stack)

  output_result(stack_name, outputs, summaries)

  outputs
end
validate_stack_name(stack_name) click to toggle source
# File lib/kumogata2/client.rb, line 510
def validate_stack_name(stack_name)
  unless /\A[a-zA-Z][-a-zA-Z0-9]*\Z/i =~ stack_name
    raise "1 validation error detected: Value '#{stack_name}' at 'stackName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z][-a-zA-Z0-9]*"
  end
end
validate_template(template) click to toggle source
# File lib/kumogata2/client.rb, line 305
def validate_template(template)
  get_client.validate_template(template_body: convert_output_value(template, false))
  log(:info, 'Template validated successfully', color: :green)
end
wait(stack, complete_status, event_log = {}) click to toggle source
# File lib/kumogata2/client.rb, line 534
def wait(stack, complete_status, event_log = {})
  before_wait = proc do |attempts, response|
    print_event_log(stack, event_log)
  end

  stack.wait_until(before_wait: before_wait, max_attempts: nil, delay: 1) do |s|
    s.stack_status !~ /_IN_PROGRESS\z/
  end

  print_event_log(stack, event_log)

  completed = (stack.stack_status == complete_status)
  log(:info, completed ? 'Success' : 'Failure')

  completed
end
wait_change_set(change_set_name, complete_status) click to toggle source
# File lib/kumogata2/client.rb, line 551
def wait_change_set(change_set_name, complete_status)
  change_set = nil

  loop do
    change_set = get_client.describe_change_set(change_set_name: change_set_name)

    if change_set.status !~ /(_PENDING|_IN_PROGRESS)\z/
      break
    end

    sleep 1
  end

  completed = (change_set.status == complete_status)
  [completed, change_set]
end