lib/cfer/cfn/client.rb in cfer-0.2.0 vs lib/cfer/cfn/client.rb in cfer-0.3.0
- old
+ new
@@ -1,10 +1,11 @@
require_relative '../core/client'
module Cfer::Cfn
class Client < Cfer::Core::Client
attr_reader :name
+ attr_reader :stack
def initialize(options)
@name = options[:stack_name]
options.delete :stack_name
@cfn = Aws::CloudFormation::Client.new(options)
@@ -25,72 +26,95 @@
def method_missing(method, *args, &block)
@cfn.send(method, *args, &block)
end
- def resolve(param)
- # See if the value follows the form @<stack>.<output>
- m = /^@(.+?)\.(.+)$/.match(param)
-
- if m
- fetch_output(m[1], m[2])
- else
- param
- end
- end
-
-
def converge(stack, options = {})
Preconditions.check(@name).is_not_nil
Preconditions.check(stack) { is_not_nil and has_type(Cfer::Core::Stack) }
response = validate_template(template_body: stack.to_cfn)
- parameters = response.parameters.map do |tmpl_param|
- cfn_param = stack.parameters[tmpl_param.parameter_key] || raise(Cfer::Util::CferError, "Parameter #{tmpl_param.parameter_key} was required, but not specified")
- cfn_param = resolve(cfn_param)
+ create_params = []
+ update_params = []
- output_val = tmpl_param.no_echo ? '*****' : cfn_param
- Cfer::LOGGER.debug "Parameter #{tmpl_param.parameter_key}=#{output_val}"
+ previous_parameters =
+ begin
+ fetch_parameters
+ rescue Cfer::Util::StackDoesNotExistError
+ nil
+ end
- {
- parameter_key: tmpl_param.parameter_key,
- parameter_value: cfn_param,
- use_previous_value: false
- }
+ response.parameters.each do |tmpl_param|
+ input_param = stack.input_parameters[tmpl_param.parameter_key]
+ old_param = previous_parameters[tmpl_param.parameter_key] if previous_parameters
+
+ Cfer::LOGGER.debug "== Evaluating Parameter '#{tmpl_param.parameter_key.to_s}':"
+ Cfer::LOGGER.debug "Input value: #{input_param.to_s || 'nil'}"
+ Cfer::LOGGER.debug "Previous value: #{old_param.to_s || 'nil'}"
+
+
+ if input_param
+ output_val = tmpl_param.no_echo ? '*****' : input_param
+ Cfer::LOGGER.debug "Parameter #{tmpl_param.parameter_key}=#{output_val}"
+ p = {
+ parameter_key: tmpl_param.parameter_key,
+ parameter_value: input_param,
+ use_previous_value: false
+ }
+
+ create_params << p
+ update_params << p
+ else
+ if old_param
+ Cfer::LOGGER.debug "Parameter #{tmpl_param.parameter_key} is unspecified (unchanged)"
+ update_params << {
+ parameter_key: tmpl_param.parameter_key,
+ use_previous_value: true
+ }
+ else
+ Cfer::LOGGER.debug "Parameter #{tmpl_param.parameter_key} is unspecified (default)"
+ end
+ end
end
- options = {
+ Cfer::LOGGER.debug "==================="
+
+ stack_options = {
stack_name: name,
template_body: stack.to_cfn,
- parameters: parameters,
capabilities: response.capabilities
}
- created = false
- cfn_stack = begin
- created = true
- create_stack options
+ stack_options[:on_failure] = options[:on_failure] if options[:on_failure]
+ stack_options[:timeout_in_minutes] = options[:timeout] if options[:timeout]
+
+ stack_options.merge! parse_stack_policy(:stack_policy, options[:stack_policy])
+ stack_options.merge! parse_stack_policy(:stack_policy_during_update, options[:stack_policy_during_update])
+
+ cfn_stack =
+ begin
+ create_stack stack_options.merge parameters: create_params
rescue Cfer::Util::StackExistsError
- update_stack options
+ update_stack stack_options.merge parameters: update_params
end
flush_cache
cfn_stack
end
# Yields to the given block for each CloudFormation event that qualifies, given the specified options.
# @param options [Hash] The options hash
# @option options [Fixnum] :number The maximum number of already-existing CloudFormation events to yield.
# @option options [Boolean] :follow Set to true to wait until the stack enters a `COMPLETE` or `FAILED` state, yielding events as they occur.
- def tail(options = {}, &block)
+ def tail(options = {})
q = []
event_id_highwater = nil
counter = 0
number = options[:number] || 0
- for_each_event name do |event|
- q.unshift event if counter < number
+ for_each_event name do |fetched_event|
+ q.unshift fetched_event if counter < number
counter = counter + 1
end
while q.size > 0
event = q.shift
@@ -103,17 +127,17 @@
while running
stack_status = describe_stacks(stack_name: name).stacks.first.stack_status
running = running && (/.+_(COMPLETE|FAILED)$/.match(stack_status) == nil)
yielding = true
- for_each_event name do |event|
- if event_id_highwater == event.event_id
+ for_each_event name do |fetched_event|
+ if event_id_highwater == fetched_event.event_id
yielding = false
end
if yielding
- q.unshift event
+ q.unshift fetched_event
end
end
while q.size > 0
event = q.shift
@@ -125,40 +149,90 @@
end
end
end
def fetch_stack(stack_name = @name)
- @stack_cache[stack_name] ||= describe_stacks(stack_name: stack_name).stacks.first.to_h
+ raise Cfer::Util::StackDoesNotExistError, 'Stack name must be specified' if stack_name == nil
+ begin
+ @stack_cache[stack_name] ||= describe_stacks(stack_name: stack_name).stacks.first.to_h
+ rescue Aws::CloudFormation::Errors::ValidationError => e
+ raise Cfer::Util::StackDoesNotExistError, e.message
+ end
end
+ def fetch_parameters(stack_name = @name)
+ @stack_parameters[stack_name] ||= cfn_list_to_hash('parameter', fetch_stack(stack_name)[:parameters])
+ end
+
+ def fetch_outputs(stack_name = @name)
+ @stack_outputs[stack_name] ||= cfn_list_to_hash('output', fetch_stack(stack_name)[:outputs])
+ end
+
+ def fetch_output(stack_name, output_name)
+ fetch_outputs(stack_name)[output_name] || raise(Cfer::Util::CferError, "Stack #{stack_name} has no output named `#{output_name}`")
+ end
+
+ def fetch_parameter(stack_name, param_name)
+ fetch_parameters(stack_name)[param_name] || raise(Cfer::Util::CferError, "Stack #{stack_name} has no parameter named `#{param_name}`")
+ end
+
def to_h
@stack.to_h
end
private
+ def cfn_list_to_hash(attribute, list)
+ key = :"#{attribute}_key"
+ value = :"#{attribute}_value"
+
+ Hash[ *list.map { |kv| [ kv[key].to_s, kv[value].to_s ] }.flatten ]
+ end
+
def flush_cache
+ Cfer::LOGGER.debug "*********** FLUSH CACHE ***************"
+ Cfer::LOGGER.debug "Stack cache: #{@stack_cache}"
+ Cfer::LOGGER.debug "Stack parameters: #{@stack_parameters}"
+ Cfer::LOGGER.debug "Stack outputs: #{@stack_outputs}"
+ Cfer::LOGGER.debug "***************************************"
@stack_cache = {}
+ @stack_parameters = {}
+ @stack_outputs = {}
end
- def fetch_output(stack_name, output_name)
- stack = fetch_stack(stack_name)
-
- output = stack[:outputs].find do |o|
- o[:output_key] == output_name
+ def for_each_event(stack_name)
+ describe_stack_events(stack_name: stack_name).stack_events.each do |event|
+ yield event
end
+ end
- if output
- output[:output_value]
- else
- raise CferError, "Stack #{stack_name} has no output value named `#{output_name}`"
- end
+ # Validates a string as json
+ #
+ # @param string [String]
+ def is_json?(string)
+ JSON.parse(string)
+ true
+ rescue JSON::ParserError
+ false
end
- def for_each_event(stack_name)
- describe_stack_events(stack_name: stack_name).stack_events.each do |event|
- yield event
+ # Parses stack-policy-* options as an S3 URL, file to read, or JSON string
+ #
+ # @param name [String] Name of option: 'stack_policy' or 'stack_policy_during_update'
+ # @param value [String] String containing URL, filename or JSON string
+ # @return [Hash] Hash suitable for merging into options for create_stack or update_stack
+ def parse_stack_policy(name, value)
+ Cfer::LOGGER.debug "Using #{name} from: #{value}"
+ if value.nil?
+ {}
+ elsif value.match(/\A#{URI::regexp(%w[http https s3])}\z/) # looks like a URL
+ {"#{name}_url".to_sym => value}
+ elsif File.exist?(value) # looks like a file to read
+ {"#{name}_body".to_sym => File.read(value)}
+ elsif is_json?(value) # looks like a JSON string
+ {"#{name}_body".to_sym => value}
+ else # none of the above
+ raise Cfer::Util::CferError, "Stack policy must be an S3 url, a filename, or a valid json string"
end
end
end
end
-