class Convection::Control::Stack
The Stack
class provides a state wrapper for CloudFormation Stacks. It tracks the state of the managed stack, and creates/updates accordingly. Stack
is also region-aware, and can be used within a template to define resources that depend upon availability-zones or other region-specific neuances that cannot be represented as maps or require iteration.
Monkey patch functions defined on Stack
for use during terraform export.
Constants
- CREATE_COMPLETE
Represents a stack that has successfully been converged.
- CREATE_FAILED
Represents a stack that has not successfully been converged.
- CREATE_IN_PROGRESS
Represents a stack that is currently being converged for the first time.
- DELETE_COMPLETE
Represents a stack that has successfully been deleted.
- DELETE_FAILED
Represents a stack that has not successfully been deleted.
- DELETE_IN_PROGRESS
Represents a stack that is currently being deleted.
- NOT_CREATED
Represents a stack that has not been created. The default state for a convection stack before getting its status.
- ROLLBACK_COMPLETE
Represents a stack that has successfully been rolled back.
- ROLLBACK_FAILED
Represents a stack that has not successfully been rolled back.
- ROLLBACK_IN_PROGRESS
Represents a stack that is currently being rolled back.
- TASK_COMPLETE
Represents a stack task being completed.
- TASK_FAILED
Represents a stack task having failed.
- TASK_IN_PROGRESS
Represents a stack task that is currently in progress.
- UPDATE_COMPLETE
Represents a stack that has successfully been updated (re-converged).
- UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
Represents a stack that is currently performing post-update cleanup.
- UPDATE_FAILED
Represents a stack that has not successfully been updated.
- UPDATE_IN_PROGRESS
Represents a stack that is currently being updated (re-converged).
- UPDATE_ROLLBACK_COMPLETE
Represents a stack that has successfully rolled back an update (re-converge).
- UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS
Represents a stack that is currently performing post-update-rollback cleanup.
- UPDATE_ROLLBACK_FAILED
Represents a stack that has successfully been rolled back after an update.
- UPDATE_ROLLBACK_IN_PROGRESS
Represents a stack that is currently performing a update rollback.
Attributes
AWS-SDK
@return [Boolean] whether the stack exists and has a status
other than DELETED.
@return [Boolean] whether the stack exists and has a status
other than DELETED.
AWS-SDK
@return [String] CloudFormation Stack
status
Public Class Methods
@param name [String] the name of the CloudFormation Stack
@param template [Convection::Model::Template] a wrapper of the
CloudFormation template (can be rendered into CF JSON)
@param options [Hash] an options hash to pass in advanced
configuration options. Undocumented options will be passed directly to CloudFormation's #create_stack or #update_stack.
@option options [String] :region AWS region, format us-east-1. Default us-east-1 @option options [Hash] :credentials optional instance of `Aws::Credentials` @option options [Hash] :parameters CloudFormation Stack
parameters @option options [String] :tags CloudFormation Stack
tags @option options [String] :on_failure the create failure action. Default: `DELETE` @option options [Array<String>] :capabilities A list of capabilities (such as `CAPABILITY_IAM`).
See also {http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Client.html#create_stack-instance_method Aws::CloudFormation::Client#create_stack}
# File lib/convection/control/stack.rb, line 103 def initialize(name, template, options = {}, &block) @name = name @template = template.clone(self) @errors = [] @cloud = options.delete(:cloud) @cloud_name = options.delete(:cloud_name) @region = options.delete(:region) { |_| 'us-east-1' } @exclude_availability_zones = options.delete(:exclude_availability_zones) { |_| [] } # Default empty Array @credentials = options.delete(:credentials) @parameters = options.delete(:parameters) { |_| {} } # Default empty hash @tags = options.delete(:tags) { |_| {} } # Default empty hash options.delete(:disable_rollback) # There can be only one... @on_failure = options.delete(:on_failure) { |_| 'DELETE' } @capabilities = options.delete(:capabilities) { |_| %w(CAPABILITY_IAM CAPABILITY_NAMED_IAM) } @attributes = options.delete(:attributes) { |_| Model::Attributes.new } @options = options @retry_limit = options[:retry_limit] || 7 client_options = {}.tap do |opt| opt[:region] = @region opt[:credentials] = @credentials unless @credentials.nil? opt[:retry_limit] = @retry_limit end @ec2_client = Aws::EC2::Client.new(client_options) @cf_client = Aws::CloudFormation::Client.new(client_options) ## Remote state @exist = false @status = NOT_CREATED @id = nil @outputs = {} @resources = {} @tasks = { after_create: [], after_delete: [], after_update: [], before_create: [], before_delete: [], before_update: [] } instance_exec(&block) if block @current_template = {} @last_event_seen = nil # First pass evaluation of stack # This is important because it: # * Catches syntax errors before starting a converge # * Builds a list of all resources that allows stacks early in # the dependency tree to know about later stacks. Some # clouds use this, for example, to create security groups early # in the dependency tree to avoid the chicken-and-egg problem. @template.execute rescue Aws::Errors::ServiceError => e @errors << e end
Public Instance Methods
@see Convection::Model::Attributes#get
# File lib/convection/control/stack.rb, line 189 def [](key) @attributes.get(name, key) end
@see Convection::Model::Attributes#set
# File lib/convection/control/stack.rb, line 194 def []=(key, value) @attributes.set(name, key, value) end
Register a given task to run after creation of a stack.
# File lib/convection/control/stack.rb, line 462 def after_create_task(task) @tasks[:after_create] << task end
Register a given task to run after deletion of a stack.
# File lib/convection/control/stack.rb, line 467 def after_delete_task(task) @tasks[:after_delete] << task end
Register a given task to run after an update of a stack.
# File lib/convection/control/stack.rb, line 472 def after_update_task(task) @tasks[:after_update] << task end
Render the CloudFormation template and apply the Stack
using the CloudFormation client.
@param block [Proc] a configuration block to pass any
{Convection::Model::Event}s to.
# File lib/convection/control/stack.rb, line 324 def apply(retain: false, &block) request_options = @options.clone.tap do |o| o[:template_body] = to_json(retain: retain) o[:parameters] = cf_parameters o[:capabilities] = capabilities end # Get the state of existence before creation existing_stack = exist? if existing_stack if diff(retain: retain).empty? ## No Changes. Just get resources and move on block.call(Model::Event.new(:complete, "Stack #{ name } has no changes", :info)) if block get_status return elsif !resource_changes? && resource_dependent_changes? message = "Stack #{ name } has no convergable changes (you must update Resources to update Conditions, Metadata, or Outputs)" block.call(Model::Event.new(UPDATE_FAILED, message, :warn)) if block get_status return end ## Execute before update tasks @tasks[:before_update].delete_if do |task| run_task(:before_update, task, &block) end ## Update @cf_client.update_stack(request_options.tap do |o| o[:stack_name] = id end) else ## Execute before create tasks @tasks[:before_create].delete_if do |task| run_task(:before_create, task, &block) end ## Create @cf_client.create_stack(request_options.tap do |o| o[:stack_name] = cloud_name o[:tags] = cf_tags o[:on_failure] = on_failure end) get_status(cloud_name) # Get ID of new stack end watch(&block) if block # Block execution on stack status ## Execute after create tasks after_task_type = existing_stack ? :after_update : :after_create @tasks[after_task_type].delete_if do |task| run_task(after_task_type, task, &block) end rescue Aws::Errors::ServiceError => e @errors << e end
@param block [Proc] a block to pass each availability zone
(with its index)
@return [Array<String>] the list of availability zones found by
the call to ec2 client's describe availability zones
# File lib/convection/control/stack.rb, line 434 def availability_zones(&block) @availability_zones ||= @ec2_client.describe_availability_zones.availability_zones.map(&:zone_name) unless @exclude_availability_zones.empty? @availability_zones.reject! { |az| @exclude_availability_zones.include?(az) } end if @availability_zones.empty? && block fail 'There are no AvailabilityZones, check exclude_availability_zones in the Cloudfile.' end @availability_zones.sort! @availability_zones.each_with_index(&block) if block @availability_zones end
Register a given task to run before creation of a stack.
# File lib/convection/control/stack.rb, line 477 def before_create_task(task) @tasks[:before_create] << task end
Register a given task to run before deletion of a stack.
# File lib/convection/control/stack.rb, line 482 def before_delete_task(task) @tasks[:before_delete] << task end
Register a given task to run before an update of a stack.
# File lib/convection/control/stack.rb, line 487 def before_update_task(task) @tasks[:before_update] << task end
# File lib/convection/control/stack.rb, line 169 def cloud_name return @cloud_name unless @cloud_name.nil? return name if cloud.nil? "#{ cloud }-#{ name }" end
@return [Boolean] whether the CloudFormation Stack
is in one of
the several *_COMPLETE states.
# File lib/convection/control/stack.rb, line 223 def complete? [CREATE_COMPLETE, ROLLBACK_COMPLETE, UPDATE_COMPLETE, UPDATE_ROLLBACK_COMPLETE].include?(status) end
@return [Boolean] whether a credential error occurred is the reason
accessing the CloudFormation stack failed.
# File lib/convection/control/stack.rb, line 229 def credential_error? error? && errors.all? { |e| e.class == Aws::EC2::Errors::RequestExpired } end
Delete the CloudFormation Stack
using the CloudFormation client.
@param block [Proc] a configuration block to pass any
{Convection::Model::Event}s to.
# File lib/convection/control/stack.rb, line 386 def delete(&block) ## Execute before delete tasks @tasks[:before_delete].delete_if do |task| run_task(:before_delete, task, &block) end @cf_client.delete_stack( :stack_name => id ) ## Block execution on stack status watch(&block) if block get_status ## Execute after delete tasks @tasks[:after_delete].delete_if do |task| run_task(:after_delete, task, &block) end rescue Aws::Errors::ServiceError => e @errors << e end
@return [Boolean] whether the CloudFormation Stack
is in one of
the several *_COMPLETE states.
# File lib/convection/control/stack.rb, line 235 def delete_complete? DELETE_COMPLETE == status end
@return [Boolean] whether the Stack
state is now {#delete_complete?} (with no errors present).
# File lib/convection/control/stack.rb, line 240 def delete_success? !error? && delete_complete? end
@return [Hash] a set of differences between the current
template (in CloudFormation) and the state of the rendered template (what *would* be converged).
@see Convection::Model::Template#diff
# File lib/convection/control/stack.rb, line 281 def diff(retain: false) @template.diff(@current_template, retain: retain) end
@return [Boolean] whether any errors occurred modifying the
stack.
# File lib/convection/control/stack.rb, line 252 def error? !errors.empty? end
@return [Boolean] whether the CloudFormation Stack
is in one of
the several *_FAILED states.
# File lib/convection/control/stack.rb, line 246 def fail? [CREATE_FAILED, ROLLBACK_FAILED, DELETE_FAILED, UPDATE_ROLLBACK_FAILED].include?(status) end
@see Convection::Model::Attributes#fetch
# File lib/convection/control/stack.rb, line 199 def fetch(*args) @attributes.fetch(*args) end
@see Convection::Model::Attributes#get
# File lib/convection/control/stack.rb, line 204 def get(*args) @attributes.get(*args) end
@return [Boolean] whether or not the CloudFormation Stack
is in
one of several *IN_PROGRESS states.
# File lib/convection/control/stack.rb, line 214 def in_progress? [CREATE_IN_PROGRESS, ROLLBACK_IN_PROGRESS, DELETE_IN_PROGRESS, UPDATE_IN_PROGRESS, UPDATE_COMPLETE_CLEANUP_IN_PROGRESS, UPDATE_ROLLBACK_IN_PROGRESS, UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS].include?(status) end
@overload include?(key)
@param key [String] the name of the attribute to find
@overload include?(stack_name, key)
@param stack_name [String] the name of the stack to check within @param key [String] the name of the attribute to find
@return [Boolean] whether the stack includes the specified key.
# File lib/convection/control/stack.rb, line 183 def include?(stack, key = nil) return @attributes.include?(name, stack) if key.nil? @attributes.include?(stack, key) end
# File lib/convection/control/stack.rb, line 160 def load_template_info get_resources get_template resource_attributes get_events(1) # Get the latest page of events (Set @last_event_seen before starting) rescue Aws::Errors::ServiceError => e @errors << e end
@see Convection::Model::Template#render
# File lib/convection/control/stack.rb, line 266 def render @template.render end
@return [Boolean] whether the Resources section of the rendered
template has any changes compared to the current template (in CloudFormation).
# File lib/convection/control/stack.rb, line 288 def resource_changes? ours = { 'Resources' => @template.all_resources.map(&:render) } thiers = { 'Resources' => @current_template['Resources'] } ours.diff(thiers).any? end
@return [Boolean] whether the any template sections dependent
on the Resources section of the rendered template has any changes compared to the current template (in CloudFormation). For example Conditions, Metadata, and Outputs depend on changes to resources to be able to converge. See also {https://github.com/rapid7/convection/issues/140 rapid7/convection#140}.
# File lib/convection/control/stack.rb, line 302 def resource_dependent_changes? ours = { 'Conditions' => @template.conditions.map(&:render), 'Outputs' => @template.outputs.map(&:render) } theirs = { 'Conditions' => @current_template['Conditions'], 'Outputs' => @current_template['Outputs'] } ours.diff(theirs).any? end
@return [Boolean] whether the Stack
state is now {#complete?} (with no errors present).
# File lib/convection/control/stack.rb, line 257 def success? !error? && complete? end
# File lib/convection/control/stack.rb, line 154 def template_status get_status(cloud_name) rescue Aws::Errors::ServiceError => e @errors << e end
@param pretty [Boolean] whether to to pretty-print the JSON output @return the renedered CloudFormation Template JSON. @see Convection::Model::Template#to_json
# File lib/convection/control/stack.rb, line 273 def to_json(pretty = false) @template.to_json(nil, pretty) end
Validates a rendered template against the CloudFormation API.
@raise unless the validation was successful
# File lib/convection/control/stack.rb, line 453 def validate result = @cf_client.validate_template(:template_body => template.to_json) fail result.context.http_response.inspect unless result.successful? puts "\nTemplate validated successfully" end
Loops through current events until the CloudFormation Stack
has finished being modified.
# File lib/convection/control/stack.rb, line 413 def watch(poll = 2, &block) get_status loop do get_events.reverse_each do |event| block.call(Model::Event.from_cf(event)) end if block break unless in_progress? sleep poll get_status end rescue Aws::Errors::ServiceError => e @errors << e end
Private Instance Methods
# File lib/convection/control/stack.rb, line 587 def cf_parameters parameters.map do |p| { :parameter_key => p[0].to_s, :parameter_value => p[1].to_s, :use_previous_value => false } end end
Fetch new stack events
# File lib/convection/control/stack.rb, line 538 def get_events(pages = nil, stack_name = id) return [] unless exist? [].tap do |collection| @cf_client.describe_stack_events(:stack_name => stack_name).each do |page| pages -= 1 unless pages.nil? page.stack_events.each do |event| if @last_event_seen == event.event_id pages = 0 # Break page loop break end collection << event end # rubocop:disable Style/NumericPredicate break if pages == 0 # rubocop:enable Style/NumericPredicate end @last_event_seen = collection.first.event_id unless collection.empty? end rescue Aws::CloudFormation::Errors::ValidationError # Stack does not exist end
Fetch current resources
# File lib/convection/control/stack.rb, line 519 def get_resources @resources = {}.tap do |collection| @cf_client.list_stack_resources(:stack_name => @id).each do |page| page.stack_resource_summaries.each do |resource| collection[resource[:logical_resource_id]] = resource end end end rescue Aws::CloudFormation::Errors::ValidationError # Stack does not exist @resources = {} end
@!endgroup
# File lib/convection/control/stack.rb, line 495 def get_status(stack_name = id) cf_stack = @cf_client.describe_stacks(:stack_name => stack_name).stacks.first @id = cf_stack.stack_id @status = cf_stack.stack_status @exist = true ## Parse outputs @outputs = {}.tap do |collection| cf_stack.outputs.each do |output| collection[output[:output_key].to_s] = (JSON.parse(output[:output_value]) rescue output[:output_value]) end end ## Add outputs to attribute set @attributes.load_outputs(self) rescue Aws::CloudFormation::Errors::ValidationError # Stack does not exist @exist = false @status = NOT_CREATED @id = nil @outputs = {} end
# File lib/convection/control/stack.rb, line 531 def get_template @current_template = JSON.parse(@cf_client.get_template(:stack_name => id).template_body) rescue Aws::CloudFormation::Errors::ValidationError # Stack does not exist @current_template = {} end
TODO No. This will become unnecessary as current_state is fleshed out
# File lib/convection/control/stack.rb, line 565 def resource_attributes @attribute_mapping_values = {} @resources.each do |logical, resource| next unless @template.attribute_mappings.include?(logical) attribute_map = @template.attribute_mappings[logical] case attribute_map[:type].to_sym when :string @attribute_mapping_values[attribute_map[:name]] = resource[:physical_resource_id] when :array @attribute_mapping_values[attribute_map[:name]] = [] unless @attribute_mapping_values[attribute_map[:name]].is_a?(Array) @attribute_mapping_values[attribute_map[:name]].push(resource[:physical_resource_id]) else fail TypeError, "Attribute Mapping must be defined with type `string` or `array`, not #{ type }" end end ## Add mapped resource IDs to attributes @attributes.load_resources(self) end
# File lib/convection/control/stack.rb, line 606 def run_task(phase, task, &block) phase = phase.to_s.split.join(' ') block.call(Model::Event.new(TASK_IN_PROGRESS, "Task (#{phase}) #{task} in progress for stack #{name}.", :info)) if block task.call(self) return task.success? unless block if task.success? block.call(Model::Event.new(TASK_COMPLETE, "Task (#{phase}) #{task} successfully completed for stack #{name}.", :info)) true else block.call(Model::Event.new(TASK_FAILED, "Task (#{phase}) #{task} failed to complete for stack #{name}.", :error)) false end end