Class: Cfhighlander::Util::CloudFormation

Inherits:
Object
  • Object
show all
Defined in:
lib/util/cloudformation.util.rb

Class Method Summary collapse

Class Method Details

.collect_output_values(template) ⇒ Object



328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/util/cloudformation.util.rb', line 328

def self.collect_output_values(template)
  output_vals = {}
  template.subcomponents.each do |sub_component|
    # we collect outputs only from inlined components
    model = sub_component.component_loaded.cfn_model_raw
    model['Outputs'].each do |name, value|
      output_vals[sub_component.component_loaded.name] = {} unless output_vals.key? sub_component.component_loaded.name
      output_vals[sub_component.component_loaded.name][name] = value['Value']
    end if model.key? 'Outputs'
  end
  return output_vals
end

.collect_replacements(component, template, output_values) ⇒ Object



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/util/cloudformation.util.rb', line 240

def self.collect_replacements(component, template, output_values)
  replacements = {}

  # collect replacements for inlined components
  template.subcomponents.each do |sub_component|
    next unless sub_component.inlined
    component_loaded = sub_component.component_loaded
    replacements[component_loaded.name] = []
    sub_stack_def = component.cfn_model_raw['Resources'][sub_component.cfn_name]
    next unless sub_stack_def['Properties'].key? 'Parameters'
    params = sub_stack_def['Properties']['Parameters']
    params.each do |param_name, param_value|
      # if param value is hash, we may find output values
      # these should be replaced with inlined values
      if param_value.is_a? Hash
        outval_refs = find_outval_refs(param_value)
        outval_refs.each do |out_ref|
          # replacement only takes place if
          # source component is inlined as well
          # if source component is not inlined
          # it's output won't be collected
          source_sub_component = template.subcomponents.find {|sc| sc.component_loaded.name == out_ref[:component]}

          # if source component is not inlined we can replacement as-is
          next unless source_sub_component.inlined
          search = { 'Fn::GetAtt' => [
              out_ref[:component],
              "Outputs.#{out_ref[:outputName]}"
          ] }
          replacement = output_values[out_ref[:component]][out_ref[:outputName]]
          if param_value == search
            param_value = replacement
          else
            # parameter value may be deeper in the structure, e.g.
            # member of Fn::If intrinsic function
            node_replace(
                param_value,
                search,
                replacement
            )
          end if output_values.key? out_ref[:component]

        end
      end
      replacements[component_loaded.name] << {
          search: { 'Ref' => param_name },
          replace: param_value
      }
    end

  end

  # collect replacements to be performed on parameters of non-inlined components
  # that are referencing inlined components
  replacements[component.name] = []
  template.subcomponents.each do |sub_component|
    next if sub_component.inlined
    sub_stack_def = component.cfn_model_raw['Resources'][sub_component.cfn_name]
    next unless sub_stack_def['Properties'].key? 'Parameters'
    params = sub_stack_def['Properties']['Parameters']
    params.each do |param_name, param_value|
      if param_value.is_a? Hash
        outval_refs = find_outval_refs(param_value)

        # component is NOT inlined and has out references to components that MAY be inlined
        outval_refs.each do |out_ref|
          component_name = out_ref[:component]
          ref_sub_component = template.subcomponents.find {|sc| sc.name == component_name}

          if ref_sub_component.nil?
            raise Cfhighlander::Error, "unable to find outputs from component #{component_name} reference by parameters in component #{sub_component.name}"
          end

          if ref_sub_component.inlined
            # out refs here need to be replaced with actual values
            replacement = output_values[out_ref[:component]][out_ref[:outputName]]
            replacements[component.name] << {
                search: { 'Fn::GetAtt' => [component_name, "Outputs.#{out_ref[:outputName]}"] },
                replace: replacement
            }
          end
        end
      end
    end
  end
  return replacements
end

.find_outval_refs(tree, outval_refs = []) ⇒ Object



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/util/cloudformation.util.rb', line 214

def self.find_outval_refs(tree, outval_refs = [])
  tree.each do |key, val|

    # if we have located get att, it may be output value
    if key == 'Fn::GetAtt'
      if val.is_a? Array and val.size == 2
        if val[1].start_with? 'Outputs.'
          component = val[0]
          output = val[1].split('.')[1]
          outval_refs << { component: component, outputName: output }
        end
      end
    elsif val.is_a? Hash or val.is_a? Array
      # however we may also find output deeper in the tree
      # example being FnIf(condition, out1, out2)
      find_outval_refs(val, outval_refs)
    end

  end if tree.is_a? Hash

  tree.each do |element|
    find_outval_refs(element, outval_refs)
  end if tree.is_a? Array
  return outval_refs
end

.flatten_key_names(component, template) ⇒ Object



460
461
462
463
464
465
466
467
468
469
# File 'lib/util/cloudformation.util.rb', line 460

def self.flatten_key_names(component, template)
  flatten_namespace('Conditions', component, template)
  Debug.debug_dump_cfn(template, 'flat.conditions')

  flatten_namespace('Mappings', component, template)
  Debug.debug_dump_cfn(template, 'flat.mappings')

  flatten_namespace('Resources', component, template)
  Debug.debug_dump_cfn(template, 'flat.resources')
end

.flatten_namespace(element_type, component, template) ⇒ Object



52
53
54
55
56
57
58
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
# File 'lib/util/cloudformation.util.rb', line 52

def self.flatten_namespace(element_type, component, template)
  if component.cfn_model_raw.key? element_type
    keys_taken = component.cfn_model_raw[element_type].keys
  else
    keys_taken = []
  end
  template.subcomponents.each do |sub_component|
    next unless sub_component.inlined
    model = sub_component.component_loaded.cfn_model_raw
    model[element_type].keys.each do |key|
      if keys_taken.include? key
        candidate = "#{sub_component.component_loaded.name}#{key}"
        counter = 1
        while keys_taken.include? candidate
          candidate = "#{sub_component.component_loaded.name}#{key}#{counter}"
          counter = counter + 1
        end
        actual_key = candidate
        # we need to replace all as
        # resources can reference conditions
        # outputs can and will reference resources
        model[element_type][actual_key] = model[element_type][key]
        model[element_type].delete(key)
        case element_type
        when 'Resources'
          rename_resource(model, key, actual_key)
        when 'Mappings'
          rename_mapping(model, key, actual_key)
        when 'Conditions'
          rename_condition(model, key, actual_key)
        when 'Outputs'
          # outputs are not effecting anything within the same template
        end
        keys_taken << actual_key
      else
        keys_taken << key
      end
    end if model.key? element_type
  end
end

.flattenCloudformation(args = {}) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/util/cloudformation.util.rb', line 12

def self.flattenCloudformation(args = {})

  component = args.fetch(:component)
  template = component.highlander_dsl

  # make sure all mappings, resources and conditions
  # are named uniquely in all of the templates
  flatten_key_names(component, template)
  Debug.debug_dump_cfn(template, 'namespace_flat')

  # collect output values
  output_values = collect_output_values(template)
  Debug.debug_dump(output_values, 'outputs')

  # collect referenced parameters and convert to replacements
  component_replacements = collect_replacements(component, template, output_values)
  Debug.debug_dump(component_replacements, 'replacements')

  # apply replacements in referenced templates
  process_replacements(component, template, component_replacements)
  Debug.debug_dump_cfn(template, 'transformed')

  # inline all of the resources
  inline_resources(component, template)

  # remove substacks
  remove_inlined_component_stacks(component, template)

  # return inlined model
  return component.cfn_model_raw
end

.inline_elements(element_name, component, template) ⇒ Object



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
# File 'lib/util/cloudformation.util.rb', line 106

def self.inline_elements(element_name, component, template)
  parent_model = component.cfn_model_raw
  template.subcomponents.each do |sub_component|
    next unless sub_component.inlined
    model = sub_component.component_loaded.cfn_model_raw
    model[element_name].each do |resource, value|
      if sub_component.conditional
        # If the resource already has a conditon we need to combine it with the stack condition
        if element_name == 'Conditions'
          value = { "Fn::And" => [{"Condition" => sub_component.condition}, value]}
        end
        # Adds the condition to the inlined resource if it doesn't already have a condition
        if element_name == 'Resources' || element_name == 'Outputs'
          value['Condition'] = sub_component.condition unless value.has_key?('Condition')
        end
      end
      # effective extraction of child resource into parent
      # allows for line components to use - or _ in the component name
      # and still generate valid references
      safe_resource_name = resource.gsub('-','').gsub('_','')
      unless element_name == 'Outputs' && resource.end_with?('CfTemplateUrl')
        parent_model[element_name] = {} unless parent_model.key? element_name
        parent_model[element_name][safe_resource_name] = value
      end
    end if model.key? element_name
  end
end

.inline_resources(component, template) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/util/cloudformation.util.rb', line 93

def self.inline_resources(component, template)
  inline_elements('Conditions', component, template)
  inline_elements('Mappings', component, template)
  inline_elements('Resources', component, template)

  # outputs are renamed AFTER all of the other processing
  # has been done, as outputs are referenced. Only
  # outputs of inlined components are renamed
  flatten_namespace('Outputs', component, template)
  Debug.debug_dump_cfn(template, 'flat.outputs')
  inline_elements('Outputs', component, template)
end

.node_replace(tree, search, replacement) ⇒ Object

if hash is treated as collection of tree structures where each key in Hash is root of the tree and value is subtree replace hash subtree with another subtree



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/util/cloudformation.util.rb', line 346

def self.node_replace(tree, search, replacement)
  if tree.is_a? Hash
    tree.each do |root, subtree|
      if subtree == search
        tree[root] = replacement
      elsif subtree.is_a? Hash or subtree.is_a? Array
        node_replace(subtree, search, replacement)
      end
    end
  elsif tree.is_a? Array
    tree.each do |element|
      if element == search
        tree[tree.index element] = replacement
      elsif element.is_a? Hash or element.is_a? Array
        node_replace(element, search, replacement)
      end
    end
  end
end

.process_replacements(component, template, component_replacements) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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
208
209
210
211
212
# File 'lib/util/cloudformation.util.rb', line 134

def self.process_replacements(component, template, component_replacements)

  # replacement processing is done from least to most dependant component
  dependency_sorted_subcomponents = template.subcomponents.sort {|sc1, sc2|
    sc1_params = component.cfn_model_raw['Resources'][sc1.cfn_name]['Properties']['Parameters']
    sc2_params = component.cfn_model_raw['Resources'][sc2.cfn_name]['Properties']['Parameters']
    outval_refs_sc1 = find_outval_refs(sc1_params)
    outval_refs_sc2 = find_outval_refs(sc2_params)

    # if sc1 is dependant on sc2,
    # sc2 param outval refs should have sc1 output
    # and vice versa
    sc1_depends_sc2 = if outval_refs_sc1.find{|oref| oref[:component] == sc2.cfn_name}.nil? then false else true end
    sc2_depends_sc1 = if outval_refs_sc2.find{|oref| oref[:component] == sc1.cfn_name}.nil? then false else true end

    if (sc1_depends_sc2 and sc2_depends_sc1)
      raise StandardError, "Components #{sc1.cfn_name} and #{sc2.cfn_name} have circular dependency!!"
    end
    if sc1_depends_sc2 then
      +1
    elsif sc2_depends_sc1 then
      -1
    else
      0
    end
  }

  # process replacements in order from least dependant to
  # most dependant
  dependency_sorted_subcomponents.each_with_index do |sub_component, index|
    next unless sub_component.inlined
    component_name = sub_component.component_loaded.name
    if component_replacements.key? component_name
      if sub_component.component_loaded.cfn_model_raw.key? 'Outputs'
        outputs_apriori = duplicate(sub_component.component_loaded.cfn_model_raw['Outputs'])
      else
        outputs_apriori = {}
      end
      component_replacements[component_name].each do |replacement|
        node_replace(
            sub_component.component_loaded.cfn_model_raw,
            replacement[:search],
            replacement[:replace]
        ) # some of the component outputs may be changed and thus replacements need be updated
      end
      iteration_index = 2
      outputs_apriori.each do |out_name, out_value|
        value_after_transform = sub_component.component_loaded.cfn_model_raw['Outputs'][out_name]
        # value of the output was changed by replacement
        unless out_value == value_after_transform
          # for all downstream dependant components
          propagated_update_index = index + 1
          while propagated_update_index < dependency_sorted_subcomponents.size
            pc_name = dependency_sorted_subcomponents[propagated_update_index].component_loaded.name
            component_replacements[pc_name].each do |replacement|
              # replacements for dependant component needs to be updated as well
              replace = replacement[:replace]

              if out_value['Value'] == replace
                replacement[:replace] = value_after_transform['Value']
              else
                node_replace(replacement[:replace], out_value['Value'], value_after_transform['Value'])
              end

            end if component_replacements.include? pc_name
            propagated_update_index += 1
          end
          Debug.debug_dump(component_replacements, "replacements.#{iteration_index}")
          iteration_index += 1
        end
      end
    end
  end

  # process replacements on component itself
  component_replacements[component.name].each do |replacement|
    node_replace(component.cfn_model_raw, replacement[:search], replacement[:replace])
  end
end

.remove_inlined_component_stacks(component, template) ⇒ Object



44
45
46
47
48
49
50
# File 'lib/util/cloudformation.util.rb', line 44

def self.remove_inlined_component_stacks(component, template)
  model = component.cfn_model_raw
  template.subcomponents.each do |sub_component|
    next unless sub_component.inlined
    model['Resources'].delete(sub_component.cfn_name)
  end
end

.rename_condition(tree, search, replacement) ⇒ Object

rename cloudformation condition in cfn model



409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/util/cloudformation.util.rb', line 409

def self.rename_condition(tree, search, replacement)
  # conditions can be referenced by Fn::If and Condition => cond
  tree.keys.each do |k|
    v = tree[k]
    if k == 'Fn::If' and v[0] == search
      tree[k] = [replacement, v[1], v[2]]
    end

    if k == 'Condition' and v == search
      tree[k] = replacement
    end

    if v.is_a? Array or v.is_a? Hash
      rename_condition(v, search, replacement)
    end
  end if tree.is_a? Hash

  tree.each do |element|
    rename_condition(element, search, replacement)
  end if tree.is_a? Array

end

.rename_mapping(tree, search, replacement) ⇒ Object

rename cloudformation mapping in cfn model



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/util/cloudformation.util.rb', line 390

def self.rename_mapping(tree, search, replacement)
  tree.keys.each do |k|
    v = tree[k]
    if k == 'Fn::FindInMap' and v[0] == search
      tree[k] = [replacement, v[1], v[2]]
    end

    if v.is_a? Array or v.is_a? Hash
      rename_mapping(v, search, replacement)
    end
  end if tree.is_a? Hash

  tree.each do |element|
    rename_mapping(element, search, replacement)
  end if tree.is_a? Array

end

.rename_resource(tree, search, replacement) ⇒ Object

rename cloudformation resource in model



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/util/cloudformation.util.rb', line 367

def self.rename_resource(tree, search, replacement)
  tree.keys.each do |k|
    v = tree[k]
    if k == 'Ref' and v == search
      tree[k] = replacement
    end

    if k == 'Fn::GetAtt' and v[0] == search
      tree[k] = [replacement, v[1]]
    end

    if v.is_a? Array or v.is_a? Hash
      rename_resource(v, search, replacement)
    end
  end if tree.is_a? Hash

  tree.each do |element|
    rename_resource(element, search, replacement)
  end if tree.is_a? Array

end

.value_replace(tree, search, replacement) ⇒ Object

Replace single value in tree structure (represented) either as Hash or Array with another value



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# File 'lib/util/cloudformation.util.rb', line 434

def self.value_replace(tree, search, replacement)
  if tree.is_a? Hash
    tree.keys.each do |root|
      subtree = tree[root]
      if root == search
        tree[replacement] = subtree
        tree.delete(root)
      end
      if subtree == search
        tree[root] = replacement
      end
      if subtree.is_a? Hash or subtree.is_a? Array
        value_replace(subtree, search, replacement)
      end
    end
  elsif tree.is_a? Array
    tree.each do |element|
      if element == search
        tree[tree.index element] = replacement
      elsif element.is_a? Hash or element.is_a? Array
        value_replace(element, search, replacement)
      end
    end
  end
end