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
# File lib/kumogata2/client.rb, line 568 def print_event_log(stack, event_log) events_for(stack).sort_by {|i| i['Timestamp'] }.each do |event| event_id = event['EventId'] unless event_log[event_id] event_log[event_id] = event timestamp = event['Timestamp'] summary = {} ['LogicalResourceId', 'ResourceStatus', 'ResourceStatusReason'].map do |k| summary[k] = event[k] end puts [ timestamp.getlocal.strftime('%Y/%m/%d %H:%M:%S %Z'), summary.to_json.colorize_as(:json), ].join(': ') end end end
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