class Stax::Stack
Constants
- COLORS
Public Instance Methods
return option or method
# File lib/stax/stack/crud.rb, line 70 def _use_previous_value @_use_previous_value ||= (options[:use_previous_value] || use_previous_value.map(&:to_s)) end
# File lib/stax/stack/crud.rb, line 224 def cancel debug("Cancelling update for #{stack_name}") Aws::Cfn.cancel(stack_name) tail rescue ::Aws::CloudFormation::Errors::ValidationError => e fail_task(e.message) end
temporarily grab stdout to a string
# File lib/stax/cfer.rb, line 62 def capture_stdout stdout, $stdout = $stdout, StringIO.new yield $stdout.string ensure $stdout = stdout end
# File lib/stax/cfer.rb, line 50 def cfer_client @_cfer_client ||= Cfer::Cfn::Client.new({}) end
generate JSON for stack without sending to cloudformation
# File lib/stax/cfer.rb, line 55 def cfer_generate(filename) Cfer::stack_from_file(filename, client: cfer_client, parameters: stringify_keys(cfn_parameters)).to_json rescue Cfer::Util::FileDoesNotExistError => e fail_task(e.message) end
backward-compatibility
# File lib/stax/cfer.rb, line 41 def cfer_parameters cfn_parameters end
override with S3
bucket for upload of large templates as needed
# File lib/stax/cfer.rb, line 46 def cfer_s3_path nil end
validate template, and return list of require capabilities
# File lib/stax/stack/crud.rb, line 147 def cfn_capabilities validate.capabilities end
by default look for cdk templates in same dir as Staxfile
# File lib/stax/stack/template.rb, line 37 def cfn_cdk_dir Stax.root_path end
set this to always do an S3
upload of template
# File lib/stax/stack/crud.rb, line 93 def cfn_force_s3? false end
override with SNS ARNs as needed
# File lib/stax/stack/crud.rb, line 117 def cfn_notification_arns if self.class.method_defined?(:cfer_notification_arns) warn('Method cfer_notification_arns deprecated, please use cfn_notification_arns') cfer_notification_arns else [] end end
by default we pass names of imported stacks; you are encouraged to override or extend this method
# File lib/stax/stack/crud.rb, line 8 def cfn_parameters stack_imports.each_with_object({}) do |i, h| h[i.to_sym] = stack(i).stack_name end end
get array of params for stack create
# File lib/stax/stack/crud.rb, line 75 def cfn_parameters_create @_cfn_parameters_create ||= cfn_parameters.map { |k,v| { parameter_key: k, parameter_value: v } } end
get array of params for stack update, use previous where requested
# File lib/stax/stack/crud.rb, line 82 def cfn_parameters_update @_cfn_parameters_update ||= cfn_parameters.map { |k,v| if _use_previous_value.include?(k.to_s) { parameter_key: k, use_previous_value: true } else { parameter_key: k, parameter_value: v } end } end
set this for template uploads as needed, e.g. s3://bucket-name/stax/#{stack_name}“
# File lib/stax/stack/crud.rb, line 103 def cfn_s3_path nil end
upload template to S3
and return public url of new object
# File lib/stax/stack/crud.rb, line 108 def cfn_s3_upload fail_task('No S3 bucket set for template upload: please set cfn_s3_path') unless cfn_s3_path uri = URI(cfn_s3_path) obj = ::Aws::S3::Object.new(bucket_name: uri.host, key: uri.path.sub(/^\//, '')) obj.put(body: cfn_template) obj.public_url + ((v = obj.version_id) ? "?versionId=#{v}" : '') end
get cfn template based on stack type
# File lib/stax/stack/template.rb, line 55 def cfn_template @_cfn_template ||= \ begin if stack_type send("cfn_template_#{stack_type}") else cfn_template_guess || fail_task('cannot find template') end end end
template body, or nil if uploading to S3
# File lib/stax/stack/crud.rb, line 137 def cfn_template_body @_cfn_template_body ||= cfn_use_s3? ? nil : cfn_template end
transcompile and load a cdk template
# File lib/stax/stack/template.rb, line 42 def cfn_template_cdk Dir.chdir(cfn_cdk_dir) do %x[npm run build] %x[cdk synth] end end
load a ruby cfer template
# File lib/stax/stack/template.rb, line 30 def cfn_template_cfer if File.exists?(f = "#{cfn_template_stub}.rb") cfer_generate(f) end end
location of templates relative to Staxfile
# File lib/stax/stack/template.rb, line 6 def cfn_template_dir 'cf' end
try to guess template by filename
# File lib/stax/stack/template.rb, line 50 def cfn_template_guess cfn_template_cfer || cfn_template_yaml || cfn_template_json end
load a json template
# File lib/stax/stack/template.rb, line 23 def cfn_template_json if File.exists?(f = "#{cfn_template_stub}.json") File.read(f) end end
template filename without extension
# File lib/stax/stack/template.rb, line 11 def cfn_template_stub @_cfn_template_stub ||= File.join(cfn_template_dir, "#{class_name}") end
load a yaml template
# File lib/stax/stack/template.rb, line 16 def cfn_template_yaml if File.exists?(f = "#{cfn_template_stub}.yaml") File.read(f) end end
set true to protect stack
# File lib/stax/stack/crud.rb, line 127 def cfn_termination_protection if self.class.method_defined?(:cfer_termination_protection) warn('Method cfer_termination_protection deprecated, please use cfn_termination_protection') cfer_termination_protection else false end end
decide if we are uploading template to S3
# File lib/stax/stack/crud.rb, line 98 def cfn_use_s3? cfn_force_s3? || (cfn_template.bytesize > 51200) end
# File lib/stax/stack/changeset.rb, line 87 def change id = change_set_update change_set_complete?(id) || fail_task(change_set_reason(id)) change_set_changes(id) change_set_unlock change_set_execute(id) && tail && update_warn_imports ensure change_set_lock end
display planned changes
# File lib/stax/stack/changeset.rb, line 52 def change_set_changes(id) debug("Changes to #{stack_name}") print_table Aws::Cfn.changes(stack_name: stack_name, change_set_name: id).map { |c| r = c.resource_change replacement = set_color(change_set_replacement(r.replacement), :red) [color(r.action, Aws::Cfn::COLORS), r.logical_resource_id, r.physical_resource_id, r.resource_type, replacement] } end
wait and return true if changeset ready for execute
# File lib/stax/stack/changeset.rb, line 34 def change_set_complete?(id) begin Aws::Cfn.client.wait_until(:change_set_create_complete, stack_name: stack_name, change_set_name: id) { |w| w.delay = 1 } rescue ::Aws::Waiters::Errors::FailureStateError => e false # no changes to apply end end
confirm and execute the change set
# File lib/stax/stack/changeset.rb, line 67 def change_set_execute(id) if yes?("Apply these changes to stack #{stack_name}?", :yellow) Aws::Cfn.execute(stack_name: stack_name, change_set_name: id) end end
# File lib/stax/stack/changeset.rb, line 79 def change_set_lock unless stack_policy.nil? Aws::Cfn.set_policy(stack_name: stack_name, stack_policy_body: stack_policy) end end
can be anything unique
# File lib/stax/stack/changeset.rb, line 12 def change_set_name stack_name + '-' + Time.now.strftime('%Y%m%d%H%M%S') end
get status reason, used for a failure message
# File lib/stax/stack/changeset.rb, line 62 def change_set_reason(id) Aws::Cfn.client.describe_change_set(stack_name: stack_name, change_set_name: id).status_reason end
string to print for replacement flag
# File lib/stax/stack/changeset.rb, line 43 def change_set_replacement(string) case string when 'True' then 'Replace' when 'Conditional' then 'May replace' else '' end end
# File lib/stax/stack/changeset.rb, line 73 def change_set_unlock unless stack_policy_during_update.nil? Aws::Cfn.set_policy(stack_name: stack_name, stack_policy_body: stack_policy_during_update) end end
create a change set to update existing stack
# File lib/stax/stack/changeset.rb, line 17 def change_set_update Aws::Cfn.changeset( stack_name: stack_name, template_body: cfn_template_body, template_url: cfn_template_url, parameters: cfn_parameters_update, capabilities: cfn_capabilities, notification_arns: cfn_notification_arns, change_set_name: change_set_name, change_set_type: :UPDATE, tags: cfn_tags_array, ).id rescue ::Aws::CloudFormation::Errors::ValidationError => e fail_task(e.message) end
get name of stack in Staxfile, or infer it from class
# File lib/stax/stack.rb, line 7 def class_name @_class_name ||= self.class.instance_variable_get(:@name).to_s || self.class.to_s.split('::').last.underscore end
# File lib/stax/stack/crud.rb, line 239 def continue Aws::Cfn.client.continue_update_rollback( stack_name: stack_name, resources_to_skip: options[:skip], ) tail rescue ::Aws::CloudFormation::Errors::ValidationError => e fail_task(e.message) end
# File lib/stax/stack/crud.rb, line 164 def create debug("Creating stack #{stack_name}") ## ensure stacks we import exist ensure_stack(*stack_imports) ## create the stack Aws::Cfn.create( stack_name: stack_name, template_body: cfn_template_body, template_url: cfn_template_url, parameters: cfn_parameters_create, capabilities: cfn_capabilities, stack_policy_body: stack_policy, notification_arns: cfn_notification_arns, enable_termination_protection: cfn_termination_protection, tags: cfn_tags_array, ) ## show stack events tail rescue ::Aws::CloudFormation::Errors::AlreadyExistsException => e fail_task(e.message) rescue ::Aws::CloudFormation::Errors::ValidationError => e warn(e.message) end
# File lib/stax/stack/crud.rb, line 213 def delete delete_warn_imports if yes? "Really delete stack #{stack_name}?", :yellow Aws::Cfn.delete(stack_name) tail unless options[:notail] end rescue ::Aws::CloudFormation::Errors::ValidationError => e fail_task(e.message) end
# File lib/stax/stack/imports.rb, line 17 def delete_warn_imports unless import_stacks.empty? warn("The following stacks import from this one: #{import_stacks.join(',')}") end end
# File lib/stax/stack/drift.rb, line 49 def drifts run_drift_detection drifts = show_drifts show_drifts_details(drifts) end
# File lib/stax/stack/cfn.rb, line 6 def event_fields(e) [e.timestamp, color(e.resource_status, Aws::Cfn::COLORS), e.resource_type, e.logical_resource_id, e.resource_status_reason] end
# File lib/stax/stack/cfn.rb, line 34 def events print_events(Aws::Cfn.events(stack_name)[0..options[:number]-1]) rescue ::Aws::CloudFormation::Errors::ValidationError => e puts e.message end
# File lib/stax/stack.rb, line 47 def exists puts exists? end
# File lib/stax/stack.rb, line 29 def exists? Aws::Cfn.exists?(stack_name) end
# File lib/stax/stack/crud.rb, line 233 def generate puts cfn_template end
# File lib/stax/stack/resources.rb, line 30 def id(resource) puts Aws::Cfn.id(stack_name, resource) end
# File lib/stax/stack/imports.rb, line 5 def import_stacks @_import_stacks ||= Aws::Cfn.exports(stack_name).map do |e| Aws::Cfn.imports(e.export_name) end.flatten.uniq end
# File lib/stax/stack/imports.rb, line 25 def imports debug("Stacks that import from #{stack_name}") print_table Aws::Cfn.exports(stack_name).map { |e| imports = (i = Aws::Cfn.imports(e.export_name)).empty? ? '-' : i.join(' ') [e.output_key, imports] }.sort end
# File lib/stax/cli/info.rb, line 11 def info ## get mixins in the order we declared them self.class.subcommands.reverse.each do |cmd| begin invoke cmd, [:info] rescue Thor::UndefinedCommandError => e # no info no problem end end end
# File lib/stax/stack/crud.rb, line 291 def lint require 'open3' Open3.popen3('cfn-lint -') do |stdin, stdout| stdin.write(cfn_template) stdin.close puts stdout.read end rescue Errno::ENOENT => e fail_task(e.message) end
# File lib/stax/stack/outputs.rb, line 15 def outputs(key = nil) if key puts stack_output(key) else print_table Aws::Cfn.describe(stack_name).outputs.map { |o| [o.output_key, o.output_value, o.description, o.export_name] }.sort end end
# File lib/stax/stack/parameters.rb, line 17 def parameters print_table stack_parameters.each_with_object({}) { |p, h| h[p.parameter_key] = p.parameter_value }.sort end
# File lib/stax/stack/crud.rb, line 263 def policy(json = nil) if json Aws::Cfn.set_policy(stack_name: stack_name, stack_policy_body: json) else puts Aws::Cfn.get_policy(stack_name: stack_name) end end
# File lib/stax/stack/cfn.rb, line 10 def print_events(events) events.reverse.each do |e| puts "%s %-44s %-40s %-20s %s" % event_fields(e) end end
# File lib/stax/stack/crud.rb, line 252 def protection if options[:enable] Aws::Cfn.protection(stack_name, true) elsif options[:disable] Aws::Cfn.protection(stack_name, false) end debug("Termination protection for #{stack_name}") puts Aws::Cfn.describe(stack_name)&.enable_termination_protection end
# File lib/stax/stack.rb, line 41 def resource(id) Aws::Cfn.id(stack_name, id) end
# File lib/stax/stack/resources.rb, line 18 def resources print_table stack_resources.tap { |resources| if options[:match] m = Regexp.new(options[:match], Regexp::IGNORECASE) resources.select! { |r| m.match(r.resource_type) } end }.map { |r| [r.logical_resource_id, r.resource_type, color(r.resource_status, Aws::Cfn::COLORS), r.physical_resource_id] } end
start a drift detection job and wait for it to complete
# File lib/stax/stack/drift.rb, line 13 def run_drift_detection debug("Running drift detection for #{stack_name}") id = Aws::Cfn.detect_drift(stack_name: stack_name) puts "waiting for #{id}" loop do sleep(1) break unless Aws::Cfn.drift_status(id).detection_status == 'DETECTION_IN_PROGRESS' end end
show the latest drift status for each resource
# File lib/stax/stack/drift.rb, line 24 def show_drifts debug("Resource drift status for #{stack_name}") Aws::Cfn.drifts(stack_name: stack_name).tap do |drifts| print_table drifts.map { |d| [d.logical_resource_id, d.resource_type, color(d.stack_resource_drift_status, COLORS), d.timestamp] } end end
show drift diffs for out of sync resources
# File lib/stax/stack/drift.rb, line 34 def show_drifts_details(drifts) drifts.select{ |d| d.stack_resource_drift_status == 'MODIFIED' }.each do |r| debug("Property differences for #{r.logical_resource_id}") r.property_differences.each do |p| puts( p.property_path + ' ' + color(p.difference_type, COLORS), ' ' + set_color('-' + p.expected_value, :red), ' ' + set_color('+' + p.actual_value, :green) ) end end end
# File lib/stax/stack/crud.rb, line 273 def skeleton skel = { StackName: stack_name, TemplateBody: cfn_template_body, TemplateURL: cfn_template_url, Parameters: cfn_parameters_create, Capabilities: cfn_capabilities, StackPolicyBody: stack_policy, NotificationARNs: cfn_notification_arns, EnableTerminationProtection: cfn_termination_protection, Tags: cfn_tags_array, }.compact method = options[:pretty] ? :pretty_generate : :generate puts JSON.send(method, skel) end
set this in stack to force changesets on update
# File lib/stax/stack/changeset.rb, line 7 def stack_force_changeset false end
# File lib/stax/stack.rb, line 25 def stack_groups self.class.instance_variable_get(:@groups) || [:default] end
list of other stacks we need to reference
# File lib/stax/stack.rb, line 17 def stack_imports self.class.instance_variable_get(:@imports) end
build valid name for the stack
# File lib/stax/stack.rb, line 12 def stack_name @_stack_name ||= stack_prefix + cfn_safe(class_name) end
# File lib/stax/stack.rb, line 37 def stack_notification_arns Aws::Cfn.describe(stack_name).notification_arns end
# File lib/stax/stack/outputs.rb, line 9 def stack_output(key) stack_outputs.fetch(key.to_s, nil) end
# File lib/stax/stack/outputs.rb, line 5 def stack_outputs @_stack_outputs ||= Aws::Cfn.outputs(stack_name) end
# File lib/stax/stack/parameters.rb, line 9 def stack_parameter(key) stack_parameters.find do |p| p.parameter_key == key.to_s end&.parameter_value end
# File lib/stax/stack/parameters.rb, line 5 def stack_parameters @_stack_parameters ||= Aws::Cfn.parameters(stack_name) end
policy to lock the stack to all updates, for example: {
Statement: [ Effect: 'Deny', Action: 'Update:*', Principal: '*', Resource: '*' ]
}.to_json
# File lib/stax/stack/crud.rb, line 36 def stack_policy nil end
tmp policy during updates, in case a deny was set in stack_policy
() for example: {
Statement: [ Effect: 'Allow', Action: 'Update:*', Principal: '*', Resource: '*' ]
}.to_json
# File lib/stax/stack/crud.rb, line 50 def stack_policy_during_update nil end
# File lib/stax/stack/resources.rb, line 5 def stack_resources @_stack_resources ||= Aws::Cfn.resources(stack_name) end
# File lib/stax/stack/resources.rb, line 9 def stack_resources_by_type(type) stack_resources.select do |r| r.resource_type == type end end
# File lib/stax/stack.rb, line 33 def stack_status Aws::Cfn.describe(stack_name).stack_status end
# File lib/stax/stack.rb, line 21 def stack_type self.class.instance_variable_get(:@type) end
# File lib/stax/stack/cfn.rb, line 42 def tail trap('SIGINT', 'EXIT') # clean exit with ctrl-c ## print some historical events events = Aws::Cfn.events(stack_name).first(options[:number] || 1) return unless events print_events(events) last_seen = events&.first&.event_id loop do sleep(1) events = [] Aws::Cfn.events(stack_name).each do |e| (last_seen == e.event_id) ? break : events << e end unless events.empty? print_events(events) last_seen = events.first.event_id end ## get stack status and break if stack gone, or delete complete/failed s = Aws::Cfn.describe(stack_name) break if s.nil? || s.stack_status.end_with?('COMPLETE', 'FAILED') end rescue ::Aws::CloudFormation::Errors::ValidationError => e puts e.message end
# File lib/stax/stack/cfn.rb, line 20 def template body = Aws::Cfn.template(stack_name) if options[:pretty] begin body = JSON.pretty_generate(JSON.parse(body)) rescue JSON::ParserError ## not valid json, may be yaml end end puts body end
# File lib/stax/stack/crud.rb, line 192 def update return change if stack_force_changeset debug("Updating stack #{stack_name}") Aws::Cfn.update( stack_name: stack_name, template_body: cfn_template_body, template_url: cfn_template_url, parameters: cfn_parameters_update, capabilities: cfn_capabilities, stack_policy_during_update_body: stack_policy_during_update, notification_arns: cfn_notification_arns, tags: cfn_tags_array, ) tail update_warn_imports rescue ::Aws::CloudFormation::Errors::ValidationError => e warn(e.message) end
# File lib/stax/stack/imports.rb, line 11 def update_warn_imports unless import_stacks.empty? warn("You may also need to update stacks that import from this one: #{import_stacks.join(',')}") end end
stack should monkey-patch with list of params to keep on update
# File lib/stax/stack/crud.rb, line 65 def use_previous_value [] end
# File lib/stax/stack/crud.rb, line 154 def validate Aws::Cfn.validate( template_body: cfn_template_body, template_url: cfn_template_url, ) rescue ::Aws::CloudFormation::Errors::ValidationError => e fail_task(e.message) end
cleanup sometimes needs to wait
# File lib/stax/stack/crud.rb, line 55 def wait_for_delete(seconds = 5) return unless exists? debug("Waiting for #{stack_name} to delete") loop do sleep(seconds) break unless exists? end end