Class: Cfer::Cfn::Client

Inherits:
Cfer::Core::Client show all
Defined in:
lib/cfer/cfn/client.rb

Instance Attribute Summary collapse

Attributes inherited from Cfer::Core::Client

#git

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ Client

Returns a new instance of Client.



10
11
12
13
14
15
16
17
# File 'lib/cfer/cfn/client.rb', line 10

def initialize(options)
  super(options)
  @name = options[:stack_name]
  @options = options
  @options.delete :stack_name
  @cfn = Aws::CloudFormation::Client.new(@options)
  flush_cache
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object



31
32
33
# File 'lib/cfer/cfn/client.rb', line 31

def method_missing(method, *args, &block)
  @cfn.send(method, *args, &block)
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name.



7
8
9
# File 'lib/cfer/cfn/client.rb', line 7

def name
  @name
end

#stackObject (readonly)

Returns the value of attribute stack.



8
9
10
# File 'lib/cfer/cfn/client.rb', line 8

def stack
  @stack
end

Instance Method Details

#converge(stack, options = {}) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/cfer/cfn/client.rb', line 59

def converge(stack, options = {})
  Preconditions.check(@name).is_not_nil
  Preconditions.check(stack) { is_not_nil and has_type(Cfer::Core::Stack) }

  template_options = upload_or_return_template(stack.to_cfn, options)

  response = validate_template(template_options)

  create_params = []
  update_params = []

  previous_parameters = fetch_parameters rescue nil

  current_version = Cfer::SEMANTIC_VERSION
  previous_version = fetch_cfer_version rescue nil

  current_hash = stack.git_state.sha rescue nil
  previous_hash = fetch_git_hash rescue nil

  # Compare current and previous versions and hashes?

  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

  Cfer::LOGGER.debug "==================="

  stack_options = options[:stack_options] || {}

  stack_options.merge! stack_name: name, capabilities: response.capabilities

  stack_options[:on_failure] = options[:on_failure] if options[:on_failure]
  stack_options[:timeout_in_minutes] = options[:timeout] if options[:timeout]
  stack_options[:role_arn] = options[:role_arn] if options[:role_arn]
  stack_options[:notification_arns] = options[:notification_arns] if options[:notification_arns]
  stack_options[:enable_termination_protection] = options[:enable_termination_protection] if options[:enable_termination_protection]

  stack_options.merge! parse_stack_policy(:stack_policy, options[:stack_policy])

  stack_options.merge! template_options

  cfn_stack =
    begin
      create_stack stack_options.merge parameters: create_params
      :created
    rescue Cfer::Util::StackExistsError
      if options[:change]
        create_change_set stack_options.merge change_set_name: options[:change], description: options[:change_description], parameters: update_params
      else
        stack_options.merge! parse_stack_policy(:stack_policy_during_update, options[:stack_policy_during_update])
        update_stack stack_options.merge parameters: update_params
      end
      :updated
    end

  flush_cache
  cfn_stack
end

#create_stack(*args) ⇒ Object



19
20
21
22
23
24
25
# File 'lib/cfer/cfn/client.rb', line 19

def create_stack(*args)
  begin
    @cfn.create_stack(*args)
  rescue Aws::CloudFormation::Errors::AlreadyExistsException
    raise Cfer::Util::StackExistsError
  end
end

#estimate(stack, options = {}) ⇒ Object



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/cfer/cfn/client.rb', line 35

def estimate(stack, options = {})
  estimate_options = upload_or_return_template(stack.to_cfn, options)
  response = validate_template(estimate_options)

  estimate_params = []
  response.parameters.each do |tmpl_param|
    input_param = stack.input_parameters[tmpl_param.parameter_key]
    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
      }

      estimate_params << p
    end
  end

  estimate_response = estimate_template_cost(estimate_options.merge(parameters: estimate_params))
  estimate_response.url
end

#fetch_cfer_version(stack_name = @name) ⇒ Object



244
245
246
247
248
249
250
# File 'lib/cfer/cfn/client.rb', line 244

def fetch_cfer_version(stack_name = @name)
  previous_version = Semantic::Version.new('0.0.0')
  if previous_version_hash = (stack_name).fetch('Cfer', {}).fetch('Version', nil)
    previous_version_hash.each { |k, v| previous_version.send(k + '=', v) }
    previous_version
  end
end

#fetch_git_hash(stack_name = @name) ⇒ Object



252
253
254
# File 'lib/cfer/cfn/client.rb', line 252

def fetch_git_hash(stack_name = @name)
  (stack_name).fetch('Cfer', {}).fetch('Git', {}).fetch('Rev', nil)
end

#fetch_metadata(stack_name = @name) ⇒ Object



230
231
232
233
234
235
236
237
238
# File 'lib/cfer/cfn/client.rb', line 230

def (stack_name = @name)
  md = fetch_summary(stack_name).
  stack_cache(stack_name)[:metadata] ||=
    if md
      JSON.parse(md)
    else
      {}
    end
end

#fetch_output(stack_name, output_name) ⇒ Object



264
265
266
# File 'lib/cfer/cfn/client.rb', line 264

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

#fetch_outputs(stack_name = @name) ⇒ Object



260
261
262
# File 'lib/cfer/cfn/client.rb', line 260

def fetch_outputs(stack_name = @name)
  stack_cache(stack_name)[:outputs] ||= cfn_list_to_hash('output', fetch_stack(stack_name)[:outputs])
end

#fetch_parameter(stack_name, param_name) ⇒ Object



268
269
270
# File 'lib/cfer/cfn/client.rb', line 268

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

#fetch_parameters(stack_name = @name) ⇒ Object



256
257
258
# File 'lib/cfer/cfn/client.rb', line 256

def fetch_parameters(stack_name = @name)
  stack_cache(stack_name)[:parameters] ||= cfn_list_to_hash('parameter', fetch_stack(stack_name)[:parameters])
end

#fetch_stack(stack_name = @name) ⇒ Object



213
214
215
216
217
218
219
220
# File 'lib/cfer/cfn/client.rb', line 213

def fetch_stack(stack_name = @name)
  raise Cfer::Util::StackDoesNotExistError, 'Stack name must be specified' if stack_name == nil
  begin
    stack_cache(stack_name)[:stack] ||= describe_stacks(stack_name: stack_name).stacks.first.to_h
  rescue Aws::CloudFormation::Errors::ValidationError => e
    raise Cfer::Util::StackDoesNotExistError, e.message
  end
end

#fetch_summary(stack_name = @name) ⇒ Object



222
223
224
225
226
227
228
# File 'lib/cfer/cfn/client.rb', line 222

def fetch_summary(stack_name = @name)
  begin
    stack_cache(stack_name)[:summary] ||= get_template_summary(stack_name: stack_name)
  rescue Aws::CloudFormation::Errors::ValidationError => e
    raise Cfer::Util::StackDoesNotExistError, e.message
  end
end

#remove(stack_name, options = {}) ⇒ Object



240
241
242
# File 'lib/cfer/cfn/client.rb', line 240

def remove(stack_name, options = {})
  delete_stack(stack_name)
end

#responds_to?(method) ⇒ Boolean

Returns:

  • (Boolean)


27
28
29
# File 'lib/cfer/cfn/client.rb', line 27

def responds_to?(method)
  @cfn.responds_to? method
end

#stack_cache(stack_name) ⇒ Object



209
210
211
# File 'lib/cfer/cfn/client.rb', line 209

def stack_cache(stack_name)
  @stack_cache[stack_name] ||= {}
end

#tail(options = {}) ⇒ Object

Yields to the given block for each CloudFormation event that qualifies, given the specified options.

Parameters:

  • options (Hash) (defaults to: {})

    The options hash

Options Hash (options):

  • :number (Fixnum)

    The maximum number of already-existing CloudFormation events to yield.

  • :follow (Boolean)

    Set to true to wait until the stack enters a COMPLETE or FAILED state, yielding events as they occur.

  • :no_sleep (Boolean)

    Don't pause between polling. This is used for tests, and shouldn't be when polling the AWS API.

  • :backoff (Fixnum)

    The exponential backoff factor (default 1.5)

  • :backoff_max_wait (Fixnum)

    The maximum amount of time that exponential backoff will wait before polling agian (default 15s)



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/cfer/cfn/client.rb', line 154

def tail(options = {})
  q = []
  event_id_highwater = nil
  counter = 0
  number = options[:number] || 0
  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
    yield event
    event_id_highwater = event.event_id
  end

  sleep_time = 1

  running = true
  if options[:follow]
    while running
      sleep_time = [sleep_time * (options[:backoff] || 1), options[:backoff_max_wait] || 15].min
      begin
        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 |fetched_event|
          if event_id_highwater == fetched_event.event_id
            yielding = false
          end

          if yielding
            q.unshift fetched_event
          end
        end
      rescue Aws::CloudFormation::Errors::Throttling
        Cfer::LOGGER.debug "AWS SDK is being throttled..."
        # Keep going though.
      rescue Aws::CloudFormation::Errors::ValidationError
        running = false
      end

      while q.size > 0
        event = q.shift
        yield event
        event_id_highwater = event.event_id
        sleep_time = 1
      end

      sleep sleep_time if running unless options[:no_sleep]
    end
  end
end

#to_hObject



272
273
274
# File 'lib/cfer/cfn/client.rb', line 272

def to_h
  @stack.to_h
end