module Cfer::Core # Defines the structure of a CloudFormation stack class Stack < Cfer::Block include Cfer::Core::Functions include Cfer::Core::Hooks # The parameters strictly as passed via command line attr_reader :input_parameters # The fully resolved parameters, including defaults and parameters fetched from an existing stack during an update attr_reader :parameters attr_reader :options attr_reader :git_state def client @options[:client] || raise('No client set on this stack') end def converge!(options = {}) client.converge self, options end def tail!(options = {}, &block) client.tail self, options, &block end def initialize(options = {}) self[:AWSTemplateFormatVersion] = '2010-09-09' self[:Description] = '' @options = options self[:Metadata] = { :Cfer => { :Version => Cfer::SEMANTIC_VERSION.to_h.delete_if { |k, v| v === nil } } } self[:Parameters] = {} self[:Mappings] = {} self[:Conditions] = {} self[:Resources] = {} self[:Outputs] = {} if options[:client] && git = options[:client].git begin @git_state = git.object('HEAD^') self[:Metadata][:Cfer][:Git] = { Rev: git_state.sha, Clean: git.status.changed.empty? } rescue => e Cfer::LOGGER.warn("Unable to add Git information to CloudFormation Metadata. #{e}") end end @parameters = HashWithIndifferentAccess.new @input_parameters = HashWithIndifferentAccess.new if options[:client] begin @parameters.merge! options[:client].fetch_parameters rescue Cfer::Util::StackDoesNotExistError Cfer::LOGGER.debug "Can't include current stack parameters because the stack doesn't exist yet." end end if options[:parameters] options[:parameters].each do |key, val| @input_parameters[key] = @parameters[key] = val end end end # Sets the description for this CloudFormation stack def description(desc) self[:Description] = desc end # Declares a CloudFormation parameter # # @param name [String] The parameter name # @param options [Hash] # @option options [String] :type The type for the CloudFormation parameter # @option options [String] :default A value of the appropriate type for the template to use if no value is specified when a stack is created. If you define constraints for the parameter, you must specify a value that adheres to those constraints. # @option options [String] :no_echo Whether to mask the parameter value whenever anyone makes a call that describes the stack. If you set the value to `true`, the parameter value is masked with asterisks (*****). # @option options [String] :allowed_values An array containing the list of values allowed for the parameter. # @option options [String] :allowed_pattern A regular expression that represents the patterns you want to allow for String types. # @option options [Number] :max_length An integer value that determines the largest number of characters you want to allow for String types. # @option options [Number] :min_length An integer value that determines the smallest number of characters you want to allow for String types. # @option options [Number] :max_value A numeric value that determines the largest numeric value you want to allow for Number types. # @option options [Number] :min_value A numeric value that determines the smallest numeric value you want to allow for Number types. # @option options [String] :description A string of up to 4000 characters that describes the parameter. # @option options [String] :constraint_description A string that explains the constraint when the constraint is violated. For example, without a constraint description, a parameter that has an allowed pattern of `[A-Za-z0-9]+` displays the following error message when the user specifies an invalid value: # # ```Malformed input-Parameter MyParameter must match pattern [A-Za-z0-9]+``` # # By adding a constraint description, such as must only contain upper- and lowercase letters, and numbers, you can display a customized error message: # # ```Malformed input-Parameter MyParameter must only contain upper and lower case letters and numbers``` def parameter(name, options = {}) param = {} options.each do |key, v| next if v === nil k = key.to_s.camelize.to_sym param[k] = case k when :AllowedPattern if v.class == Regexp v.source end when :Default @parameters[name] ||= v end param[k] ||= v end param[:Type] ||= 'String' self[:Parameters][name] = param end # Sets the mappings block for this stack. See [The CloudFormation Documentation](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html) for more details def mappings(mappings) self[:Mappings] = mappings end # Adds a condition to the template. # @param name [String] The name of the condition. # @param expr [Hash] The CloudFormation condition to add. See [The Cloudformation Documentation](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html) for more details def condition(name, expr) self[:Conditions][name] = expr end # Creates a CloudFormation resource # @param name [String] The name of the resource (must be alphanumeric) # @param type [String] The type of CloudFormation resource to create. # @param options [Hash] Additional attributes to add to the resource block (such as the `UpdatePolicy` for an `AWS::AutoScaling::AutoScalingGroup`) def resource(name, type, options = {}, &block) Preconditions.check_argument(/[[:alnum:]]+/ =~ name, "Resource name must be alphanumeric") clazz = Cfer::Core::Resource.resource_class(type) rc = clazz.new(name, type, self, options, &block) self[:Resources][name] = rc rc.handle end # Adds an output to the CloudFormation stack. # @param name [String] The Logical ID of the output parameter # @param value [String] Value to return # @param options [Hash] Extra options for this output parameter # @option options [String] :Description Information about the value def output(name, value, options = {}) self[:Outputs][name] = options.merge('Value' => value) end # Renders the stack into a CloudFormation template. # @return [String] The final template def to_cfn if @options[:pretty_print] JSON.pretty_generate(to_h) else to_h.to_json end end # Gets the Cfn client, if one exists, or throws an error if one does not def client @options[:client] || raise(Cfer::Util::CferError, "Stack has no associated client.") end # Includes template code from one or more files, and evals it in the context of this stack. # Filenames are relative to the file containing the invocation of this method. def include_template(*files) include_base = options[:include_base] || File.dirname(caller.first.split(/:\d/,2).first) files.each do |file| path = File.join(include_base, file) include_file(path) end end # Looks up a specific output of another CloudFormation stack in the same region. # @param stack [String] The name of the stack to fetch an output from # @param out [String] The name of the output to fetch from the stack def lookup_output(stack, out) lookup_outputs(stack).fetch(out) end # Looks up a hash of all outputs from another CloudFormation stack in the same region. # @param stack [String] The name of the stack to fetch outputs from def lookup_outputs(stack) client = @options[:client] || raise(Cfer::Util::CferError, "Can not fetch stack outputs without a client") client.fetch_outputs(stack) end class << self def extend_stack(&block) class_eval(&block) end end end end