cfhighlander 0.6.0.alpha.1538010880 → 0.6.0.alpha.1539120146

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1ca60058a558ba23ed08273fa7ec940794476f1fc9228d6077da3521c6d7b19
4
- data.tar.gz: 83b85f5799b9d48263174b5adc2ec5db6a6fde698a21e1b0698c36c1a4091ff7
3
+ metadata.gz: 4a73647965281a89f8bb2bc78e8282e81604f13b5aa06a48ab2ad0f7c21ec78b
4
+ data.tar.gz: 60a0df940c9a7675ec62de5f636144496bd88dad4ddfcaeae1e5c6f6669dca94
5
5
  SHA512:
6
- metadata.gz: 682f0fb7fe00e2ecb5315ec1d5e599591d8a9a8721d55bc40d96fdafbf0fbfadc227b2272aaf20c19802d27fde22c1215a2ef10987fdefb306d42c8ec65ccfe3
7
- data.tar.gz: 553b3e35fe13a125bf633da11486167f4946a5f7ea5cc51be92b9189ffb95ef4d614446e191fbbd5b7648952739337419435858b467cc54efd68c7e72939288d
6
+ metadata.gz: b743ab97b1cc2051d98f048630ee114a2ebd976b3acb14f14d9b989373bcbd0964b10e726330d12b569aa443504f052111bc0ad78febd61a54c8f26b5eb7ea10
7
+ data.tar.gz: 9a5de349f486e6c8cea90a1c7f7672ca178179815f489c16795714684da4dd8d0862a54bee672c083fc0a3d106d3bb5161443e23fb9bc3f06cb0cf8053f61b3c
data/README.md CHANGED
@@ -699,6 +699,46 @@ CfhighlanderTemplate do
699
699
  end
700
700
 
701
701
  ```
702
+ ## Render mode for components
703
+
704
+ Rendering component resources in resulting cloudformation stack is available in 2 modes. These modes
705
+ are controlled using `render` keyword of `Component` DSL statement
706
+
707
+ `Substack` - creates additional substack for cfhighlander component and points to it using [CloudFormation
708
+ Stack](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stack.html) resource type
709
+ This is also default render mode - if no render mode is specified `Substack` will be used
710
+
711
+ `Inline` - places all defined resources from inner component in outer component cloudformation template. Resources,
712
+ Outputs, Conditions, Parameters and Mappings are all inlined - please note that some of the template elements may be renamed in this
713
+ process in order to assure unique names.
714
+
715
+ There are some limitations when using inline components - Inlined component parameters, having values as outputs from another component (inlined or not)
716
+ can't be referenced in component conditions. However, conditions referencing mapping values or parameters passed as mapping values,
717
+ are allowed.
718
+
719
+ **`SIDE EFFECTS`** Side effect of moving from substack based to fully inlined stack may be revealing some of the implicit dependencies within an environment
720
+
721
+ *Example:* Component A defines Hosted Zone, while component B defines Record Set for given hosted zone. Record set is defined
722
+ by referencing Zone Name (rather than ZoneId), meaning there is no explicit dependency between the resources. When both components
723
+ are rendered as substack, implicit dependency is created if there is at least one output from component A passed as parameter
724
+ to component B. Rendering components inlined removes this implicitly defined dependency, as a consequence stack deletion or creation
725
+ may be halted, as record set is being created/deleted before prior the record set.
726
+
727
+ **`WARNING`** Be aware of [resource, condition, parameter, output and mapping limits](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html) on a single template
728
+ when rendering inner components inlined.
729
+
730
+ **`EXAMPLE`** All of the VPC resources will be rendered in outer component template, while bastion
731
+ will be referenced as substack in example below.
732
+
733
+ ```ruby
734
+ CfhighlanderTemplate do
735
+
736
+ Component template:'vpc@1.5.0', name: 'vpc', render: Inline
737
+ Component template:'bastion@1.2.0', name: 'bastion', render: Substack
738
+
739
+ end
740
+ ```
741
+
702
742
 
703
743
 
704
744
  ## Rendering CloudFormation templates
data/bin/cfhighlander.rb CHANGED
@@ -125,6 +125,7 @@ class HighlanderCli < Thor
125
125
  publisher.publishFiles(compiler.cfn_template_paths + compiler.lambda_src_paths)
126
126
 
127
127
  puts "\n\nUse following url to launch CloudFormation stack\n\n#{publisher.getLaunchStackUrl}\n\n"
128
+ puts "\n\nUse following template url to update the stack\n\n#{publisher.getTemplateUrl}\n\n"
128
129
 
129
130
  end
130
131
 
@@ -5,11 +5,11 @@ def mappings_provider(provider_name, is_legacy = false)
5
5
  return nil if provider_name.nil?
6
6
  provider = nil
7
7
  module_name = is_legacy ? 'Highlander': 'Cfhighlander'
8
- providers = Object.const_get(module_name).const_get('MapProviders')
9
8
  begin
9
+ providers = Object.const_get(module_name).const_get('MapProviders')
10
10
  providers.const_get(provider_name)
11
11
  rescue NameError => e
12
- if e.to_s.include? "uninitialized constant #{module_name}::MapProviders::"
12
+ if e.to_s.include? "uninitialized constant"
13
13
  return mappings_provider(provider_name, true) unless is_legacy
14
14
  return nil
15
15
  end
@@ -11,6 +11,7 @@ require 'net/https'
11
11
  require 'highline/import'
12
12
  require 'zip'
13
13
  require_relative './util/zip.util'
14
+ require_relative './util/cloudformation.util'
14
15
 
15
16
  module Cfhighlander
16
17
 
@@ -140,7 +141,22 @@ module Cfhighlander
140
141
 
141
142
 
142
143
  # grab cfndsl model
144
+
145
+ # 1st pass of cloudformation compiling - does the substacks normally
143
146
  model = evaluateCloudFormation
147
+ @component.set_cfndsl_model model
148
+
149
+ # compile sub-component templates
150
+ @sub_components.each do |sub_component|
151
+ sub_component.compileCloudFormation format
152
+ @cfn_template_paths += sub_component.cfn_template_paths
153
+ @lambda_src_paths += sub_component.lambda_src_paths
154
+ end
155
+
156
+ # 2nd pass will flatten any inlined components
157
+ model = Cfhighlander::Util::CloudFormation.flattenCloudformation(
158
+ component: @component
159
+ )
144
160
 
145
161
  # write resulting cloud formation template
146
162
  if format == 'json'
@@ -155,12 +171,8 @@ module Cfhighlander
155
171
  # `cfndsl #{@cfndsl_compiled_path} -p -f #{format} -o #{output_path} --disable-binding`
156
172
  puts "CloudFormation #{format.upcase} template for #{dsl.name} written to #{output_path}"
157
173
 
158
- # compile sub-component templates
159
- @sub_components.each do |sub_component|
160
- sub_component.compileCloudFormation format
161
- @cfn_template_paths += sub_component.cfn_template_paths
162
- @lambda_src_paths += sub_component.lambda_src_paths
163
- end
174
+
175
+
164
176
 
165
177
  end
166
178
 
@@ -35,6 +35,10 @@ module Cfhighlander
35
35
  return { 'Fn::Split' => [delimiter, source] }
36
36
  end
37
37
 
38
+ def FnJoin(glue, pieces)
39
+ return { 'Fn::Join' => [glue, pieces]}
40
+ end
41
+
38
42
  def FnSelect(index, list)
39
43
  return { 'Fn::Select' => [index, list] }
40
44
  end
@@ -28,13 +28,13 @@ module Cfhighlander
28
28
  :component_config_override,
29
29
  :export_config
30
30
 
31
-
32
31
  attr_reader :cfn_name,
33
32
  :conditional,
34
33
  :parent,
35
34
  :name,
36
35
  :template,
37
- :template_version
36
+ :template_version,
37
+ :inlined
38
38
 
39
39
  def initialize(parent,
40
40
  name,
@@ -45,6 +45,7 @@ module Cfhighlander
45
45
  export_config = {},
46
46
  conditional = false,
47
47
  enabled = true,
48
+ inline = false,
48
49
  distribution_format = 'yaml')
49
50
 
50
51
  @parent = parent
@@ -52,6 +53,7 @@ module Cfhighlander
52
53
  @export_config = export_config
53
54
  @component_sources = component_sources
54
55
  @conditional = conditional
56
+ @inlined = inline
55
57
 
56
58
  template_name = template
57
59
  template_version = 'latest'
@@ -227,7 +229,12 @@ module Cfhighlander
227
229
  # TODO wire mapping parameters outside of component
228
230
  if param.class == Cfhighlander::Dsl::MappingParam
229
231
  puts " mapping parameter"
230
- return self.resolveMappingParamValue(component, sub_component, param)
232
+ mapping_param_value = self.resolveMappingParamValue(component, sub_component, param)
233
+
234
+ # if mapping param is not resolved, e.g. mapping not provided
235
+ # parameters will bubble to parent component if not matched by outputs from
236
+ # other components
237
+ return mapping_param_value unless mapping_param_value.nil?
231
238
  end
232
239
 
233
240
  # rule #2: match output values from other components
@@ -287,11 +294,6 @@ module Cfhighlander
287
294
  key_name: key_name
288
295
  )
289
296
 
290
- if value.nil?
291
- return "'#{param.default_value}'" unless param.default_value.empty?
292
- return "''"
293
- end
294
-
295
297
  return value
296
298
  end
297
299
 
@@ -117,6 +117,7 @@ module Cfhighlander
117
117
  export_config: {},
118
118
  conditional: false,
119
119
  enabled: true,
120
+ render: Cfhighlander::Model::Component::Substack,
120
121
  &block)
121
122
  puts "INFO: Requested subcomponent #{name} with template #{template}"
122
123
  if ((not template_arg.nil?) and template.nil?)
@@ -137,7 +138,8 @@ module Cfhighlander
137
138
  config,
138
139
  export_config,
139
140
  conditional,
140
- enabled
141
+ enabled,
142
+ render == Cfhighlander::Model::Component::Inline
141
143
  )
142
144
  # component.instance_eval(&block) unless block.nil?
143
145
  @subcomponents_exec[name] = block unless block.nil?
@@ -482,9 +484,6 @@ def CfhighlanderTemplate(&block)
482
484
  unless @distribution_prefix.nil?
483
485
  instance.DistributionPrefix(@distribution_prefix)
484
486
  end
485
- unless @distribution_format.nil?
486
- instance.Forma
487
- end
488
487
 
489
488
  # process convention over configuration componentname.config.yaml files
490
489
  @potential_subcomponent_overrides.each do |name, config|
@@ -15,8 +15,9 @@ module Cfhighlander
15
15
  default_locations = [
16
16
  LOCAL_CFHIGHLANDER_CACHE_LOCATION,
17
17
  File.expand_path('components'),
18
- File.expand_path('.')
18
+ File.expand_path('.'),
19
19
  ]
20
+ default_locations << ENV['CFHIGHLANDER_WORKDIR'] if ENV.key? 'CFHIGHLANDER_WORKDIR'
20
21
  default_locations.each do |predefined_path|
21
22
  component_sources.unshift(predefined_path)
22
23
  end
@@ -6,6 +6,9 @@ module Cfhighlander
6
6
 
7
7
  class Component
8
8
 
9
+ Inline = 'inline'
10
+ Substack = 'substack'
11
+
9
12
  attr_accessor :component_dir,
10
13
  :config,
11
14
  :highlander_dsl_path,
@@ -26,12 +29,12 @@ module Cfhighlander
26
29
  :factory,
27
30
  :extended_component,
28
31
  :is_parent_component
29
-
30
- attr_reader :cfn_model,
31
- :outputs,
32
+ attr_reader :outputs,
32
33
  :factory,
33
34
  :extended_component,
34
- :potential_subcomponent_overrides
35
+ :potential_subcomponent_overrides,
36
+ :cfn_model,
37
+ :cfn_model_raw
35
38
 
36
39
 
37
40
  def initialize(template_meta, component_name, factory)
@@ -90,7 +93,7 @@ module Cfhighlander
90
93
  def loadDepandantExt()
91
94
  @highlander_dsl.dependson_components.each do |requirement|
92
95
  requirement.component_loaded.cfndsl_ext_files.each do |file|
93
- cfndsl_ext_files << file
96
+ @cfndsl_ext_files << file
94
97
  end
95
98
  end
96
99
  end
@@ -230,12 +233,17 @@ module Cfhighlander
230
233
  end
231
234
 
232
235
  # evaluates cfndsl with current config
236
+ def set_cfndsl_model(value)
237
+ @cfn_model = value.as_json
238
+ @cfn_model_raw = JSON.parse(@cfn_model.to_json)
239
+ end
233
240
  def eval_cfndsl
234
241
  compiler = Cfhighlander::Compiler::ComponentCompiler.new self
235
242
  # there is no need for processing lambda source code during cloudformation evaluation,
236
243
  # this version never gets published
237
244
  compiler.process_lambdas = false
238
245
  @cfn_model = compiler.evaluateCloudFormation().as_json
246
+ @cfn_model_raw = JSON.parse(@cfn_model.to_json)
239
247
  @outputs = (
240
248
  if @cfn_model.key? 'Outputs'
241
249
  then
@@ -40,12 +40,17 @@ module Cfhighlander
40
40
 
41
41
  end
42
42
 
43
- def getLaunchStackUrl
43
+ def getTemplateUrl
44
44
  template_url = "https://#{@component.highlander_dsl.distribution_bucket}.s3.amazonaws.com/"
45
45
  template_url += @component.highlander_dsl.distribution_prefix + "/"
46
46
  template_url += @component.highlander_dsl.version
47
- region = s3_bucket_region(@component.highlander_dsl.distribution_bucket)
48
47
  template_url += "/#{@component.name}.compiled.yaml"
48
+ return template_url
49
+ end
50
+
51
+ def getLaunchStackUrl
52
+ template_url = getTemplateUrl
53
+ region = s3_bucket_region(@component.highlander_dsl.distribution_bucket)
49
54
  return "https://console.aws.amazon.com/cloudformation/home?region=#{region}#/stacks/create/review?filter=active&templateURL=" +
50
55
  "#{URI::encode(template_url)}&stackName=#{@component.name}"
51
56
  end
@@ -7,7 +7,6 @@ module Cfhighlander
7
7
 
8
8
  module Cloudformation
9
9
 
10
-
11
10
  class Validator
12
11
 
13
12
  def initialize(component)
@@ -0,0 +1,450 @@
1
+ require_relative '../cfhighlander.model.component'
2
+ require_relative './debug.util'
3
+ require 'duplicate'
4
+
5
+ module Cfhighlander
6
+
7
+ module Util
8
+
9
+ class CloudFormation
10
+
11
+ def self.flattenCloudformation(args = {})
12
+
13
+ component = args.fetch(:component)
14
+ template = component.highlander_dsl
15
+
16
+ # make sure all mappings, resources and conditions
17
+ # are named uniquely in all of the templates
18
+ flatten_key_names(component, template)
19
+ Debug.debug_dump_cfn(template, 'namespace_flat')
20
+
21
+ # collect output values
22
+ output_values = collect_output_values(template)
23
+ Debug.debug_dump(output_values, 'outputs')
24
+
25
+ # collect referenced parameters and convert to replacements
26
+ component_replacements = collect_replacements(component, template, output_values)
27
+ Debug.debug_dump(component_replacements, 'replacements')
28
+
29
+ # apply replacements in referenced templates
30
+ process_replacements(component, template, component_replacements)
31
+ Debug.debug_dump_cfn(template, 'transformed')
32
+
33
+ # inline all of the resources
34
+ inline_resources(component, template)
35
+
36
+ # remove substacks
37
+ remove_inlined_component_stacks(component, template)
38
+
39
+ # return inlined model
40
+ return component.cfn_model_raw
41
+ end
42
+
43
+ def self.remove_inlined_component_stacks(component, template)
44
+ model = component.cfn_model_raw
45
+ template.subcomponents.each do |sub_component|
46
+ next unless sub_component.inlined
47
+ model['Resources'].delete(sub_component.component_loaded.name)
48
+ end
49
+ end
50
+
51
+ def self.flatten_namespace(element_type, component, template)
52
+ if component.cfn_model_raw.key? element_type
53
+ keys_taken = component.cfn_model_raw[element_type].keys
54
+ else
55
+ keys_taken = []
56
+ end
57
+ template.subcomponents.each do |sub_component|
58
+ next unless sub_component.inlined
59
+ model = sub_component.component_loaded.cfn_model_raw
60
+
61
+ model[element_type].keys.each do |key|
62
+ if keys_taken.include? key
63
+ candidate = "#{sub_component.component_loaded.name}#{key}"
64
+ counter = 1
65
+ while keys_taken.include? candidate
66
+ candidate = "#{sub_component.component_loaded.name}#{key}#{counter}"
67
+ counter = counter + 1
68
+ end
69
+ actual_key = candidate
70
+ # we need to replace all as
71
+ # resources can reference conditions
72
+ # outputs can and will reference resources
73
+ model[element_type][actual_key] = model[element_type][key]
74
+ model[element_type].delete(key)
75
+ case element_type
76
+ when 'Resources'
77
+ rename_resource(model, key, actual_key)
78
+ when 'Mappings'
79
+ rename_mapping(model, key, actual_key)
80
+ when 'Conditions'
81
+ rename_condition(model, key, actual_key)
82
+ when 'Outputs'
83
+ # outputs are not effecting anything within the same template
84
+ end
85
+ keys_taken << actual_key
86
+ else
87
+ keys_taken << key
88
+ end
89
+ end if model.key? element_type
90
+ end
91
+ end
92
+
93
+ def self.inline_resources(component, template)
94
+ inline_elements('Conditions', component, template)
95
+ inline_elements('Mappings', component, template)
96
+ inline_elements('Resources', component, template)
97
+ inline_elements('Outputs', component, template)
98
+ end
99
+
100
+ def self.inline_elements(element_name, component, template)
101
+ parent_model = component.cfn_model_raw
102
+ template.subcomponents.each do |sub_component|
103
+ next unless sub_component.inlined
104
+ model = sub_component.component_loaded.cfn_model_raw
105
+ model[element_name].each do |resource, value|
106
+ # effective extraction of child resource into parent
107
+ parent_model[element_name] = {} unless parent_model.key? element_name
108
+ parent_model[element_name][resource] = value
109
+ end if model.key? element_name
110
+ end
111
+ end
112
+
113
+ def self.process_replacements(component, template, component_replacements)
114
+
115
+ # replacement processing is done from least to most dependant component
116
+ dependency_sorted_subcomponents = template.subcomponents.sort {|sc1, sc2|
117
+ sc1_params = component.cfn_model_raw['Resources'][sc1.cfn_name]['Properties']['Parameters']
118
+ sc2_params = component.cfn_model_raw['Resources'][sc2.cfn_name]['Properties']['Parameters']
119
+ outval_refs_sc1 = find_outval_refs(sc1_params)
120
+ outval_refs_sc2 = find_outval_refs(sc2_params)
121
+
122
+ # if sc1 is dependant on sc2,
123
+ # sc2 param outval refs should have sc1 output
124
+ # and vice versa
125
+ sc1_depends_sc2 = if outval_refs_sc1.find{|oref| oref[:component] == sc2.cfn_name}.nil? then false else true end
126
+ sc2_depends_sc1 = if outval_refs_sc2.find{|oref| oref[:component] == sc1.cfn_name}.nil? then false else true end
127
+
128
+ if (sc1_depends_sc2 and sc2_depends_sc1)
129
+ raise StandardError, "Components #{sc1.cfn_name} and #{sc2.cfn_name} have circular dependency!!"
130
+ end
131
+ if sc1_depends_sc2 then
132
+ +1
133
+ elsif sc2_depends_sc1 then
134
+ -1
135
+ else
136
+ 0
137
+ end
138
+ }
139
+
140
+ # process replacements in order from least dependant to
141
+ # most dependant
142
+ dependency_sorted_subcomponents.each_with_index do |sub_component, index|
143
+ next unless sub_component.inlined
144
+ component_name = sub_component.component_loaded.name
145
+ if component_replacements.key? component_name
146
+ if sub_component.component_loaded.cfn_model_raw.key? 'Outputs'
147
+ outputs_apriori = duplicate(sub_component.component_loaded.cfn_model_raw['Outputs'])
148
+ else
149
+ outputs_apriori = {}
150
+ end
151
+ component_replacements[component_name].each do |replacement|
152
+ node_replace(
153
+ sub_component.component_loaded.cfn_model_raw,
154
+ replacement[:search],
155
+ replacement[:replace]
156
+ ) # some of the component outputs may be changed and thus replacements need be updated
157
+ end
158
+ iteration_index = 2
159
+ outputs_apriori.each do |out_name, out_value|
160
+ value_after_transform = sub_component.component_loaded.cfn_model_raw['Outputs'][out_name]
161
+ # value of the output was changed by replacement
162
+ unless out_value == value_after_transform
163
+ # for all downstream dependant components
164
+ propagated_update_index = index + 1
165
+ while propagated_update_index < dependency_sorted_subcomponents.size
166
+ pc_name = dependency_sorted_subcomponents[propagated_update_index].component_loaded.name
167
+ component_replacements[pc_name].each do |replacement|
168
+ # replacements for dependant component needs to be updated as well
169
+ replace = replacement[:replace]
170
+
171
+ if out_value['Value'] == replace
172
+ replacement[:replace] = value_after_transform['Value']
173
+ else
174
+ node_replace(replacement[:replace], out_value['Value'], value_after_transform['Value'])
175
+ end
176
+
177
+ end if component_replacements.include? pc_name
178
+ propagated_update_index += 1
179
+ end
180
+ Debug.debug_dump(component_replacements, "replacements.#{iteration_index}")
181
+ iteration_index += 1
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ # process replacements on component itself
188
+ component_replacements[component.name].each do |replacement|
189
+ node_replace(component.cfn_model_raw, replacement[:search], replacement[:replace])
190
+ end
191
+ end
192
+
193
+ def self.find_outval_refs(tree, outval_refs = [])
194
+ tree.each do |key, val|
195
+
196
+ # if we have located get att, it may be output value
197
+ if key == 'Fn::GetAtt'
198
+ if val.is_a? Array and val.size == 2
199
+ if val[1].start_with? 'Outputs.'
200
+ component = val[0]
201
+ output = val[1].split('.')[1]
202
+ outval_refs << { component: component, outputName: output }
203
+ end
204
+ end
205
+ elsif val.is_a? Hash or val.is_a? Array
206
+ # however we may also find output deeper in the tree
207
+ # example being FnIf(condition, out1, out2)
208
+ find_outval_refs(val, outval_refs)
209
+ end
210
+
211
+ end if tree.is_a? Hash
212
+
213
+ tree.each do |element|
214
+ find_outval_refs(element, outval_refs)
215
+ end if tree.is_a? Array
216
+ return outval_refs
217
+ end
218
+
219
+ def self.collect_replacements(component, template, output_values)
220
+ replacements = {}
221
+
222
+ # collect replacements for inlined components
223
+ template.subcomponents.each do |sub_component|
224
+ next unless sub_component.inlined
225
+ component_loaded = sub_component.component_loaded
226
+ replacements[component_loaded.name] = []
227
+ sub_stack_def = component.cfn_model_raw['Resources'][sub_component.cfn_name]
228
+ next unless sub_stack_def['Properties'].key? 'Parameters'
229
+ params = sub_stack_def['Properties']['Parameters']
230
+ params.each do |param_name, param_value|
231
+ # if param value is hash, we may find output values
232
+ # these should be replaced with inlined values
233
+ if param_value.is_a? Hash
234
+ outval_refs = find_outval_refs(param_value)
235
+ outval_refs.each do |out_ref|
236
+ # replacement only takes place if
237
+ # source component is inlined as well
238
+ # if source component is not inlined
239
+ # it's output won't be collected
240
+ source_sub_component = template.subcomponents.find {|sc| sc.component_loaded.name == out_ref[:component]}
241
+
242
+ # if source component is not inlined we can replacement as-is
243
+ next unless source_sub_component.inlined
244
+ search = { 'Fn::GetAtt' => [
245
+ out_ref[:component],
246
+ "Outputs.#{out_ref[:outputName]}"
247
+ ] }
248
+ replacement = output_values[out_ref[:component]][out_ref[:outputName]]
249
+ if param_value == search
250
+ param_value = replacement
251
+ else
252
+ # parameter value may be deeper in the structure, e.g.
253
+ # member of Fn::If intrinsic function
254
+ node_replace(
255
+ param_value,
256
+ search,
257
+ replacement
258
+ )
259
+ end if output_values.key? out_ref[:component]
260
+
261
+ end
262
+ end
263
+ replacements[component_loaded.name] << {
264
+ search: { 'Ref' => param_name },
265
+ replace: param_value
266
+ }
267
+ end
268
+
269
+ end
270
+
271
+ # collect replacements to be performed on parameters of non-inlined components
272
+ # that are referencing inlined components
273
+ replacements[component.name] = []
274
+ template.subcomponents.each do |sub_component|
275
+ next if sub_component.inlined
276
+ sub_stack_def = component.cfn_model_raw['Resources'][sub_component.cfn_name]
277
+ next unless sub_stack_def['Properties'].key? 'Parameters'
278
+ params = sub_stack_def['Properties']['Parameters']
279
+ params.each do |param_name, param_value|
280
+ if param_value.is_a? Hash
281
+ outval_refs = find_outval_refs(param_value)
282
+
283
+ # component is NOT inlined and has out references to components that MAY be inlined
284
+ outval_refs.each do |out_ref|
285
+ component_name = out_ref[:component]
286
+ ref_sub_component = template.subcomponents.find {|sc| sc.name == component_name}
287
+ if ref_sub_component.inlined
288
+ # out refs here need to be replaced with actual values
289
+ replacement = output_values[out_ref[:component]][out_ref[:outputName]]
290
+ replacements[component.name] << {
291
+ search: { 'Fn::GetAtt' => [component_name, "Outputs.#{out_ref[:outputName]}"] },
292
+ replace: replacement
293
+ }
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
299
+ return replacements
300
+ end
301
+
302
+ def self.collect_output_values(template)
303
+ output_vals = {}
304
+ template.subcomponents.each do |sub_component|
305
+ # we collect outputs only from inlined components
306
+ model = sub_component.component_loaded.cfn_model_raw
307
+ model['Outputs'].each do |name, value|
308
+ output_vals[sub_component.component_loaded.name] = {} unless output_vals.key? sub_component.component_loaded.name
309
+ output_vals[sub_component.component_loaded.name][name] = value['Value']
310
+ end if model.key? 'Outputs'
311
+ end
312
+ return output_vals
313
+ end
314
+
315
+ ## if hash is treated as
316
+ ## collection of tree structures
317
+ ## where each key in Hash is root of the tree
318
+ ## and value is subtree
319
+ ## replace hash subtree with another subtree
320
+ def self.node_replace(tree, search, replacement)
321
+ if tree.is_a? Hash
322
+ tree.each do |root, subtree|
323
+ if subtree == search
324
+ tree[root] = replacement
325
+ elsif subtree.is_a? Hash or subtree.is_a? Array
326
+ node_replace(subtree, search, replacement)
327
+ end
328
+ end
329
+ elsif tree.is_a? Array
330
+ tree.each do |element|
331
+ if element == search
332
+ tree[tree.index element] = replacement
333
+ elsif element.is_a? Hash or element.is_a? Array
334
+ node_replace(element, search, replacement)
335
+ end
336
+ end
337
+ end
338
+ end
339
+
340
+ # rename cloudformation resource in model
341
+ def self.rename_resource(tree, search, replacement)
342
+ tree.keys.each do |k|
343
+ v = tree[k]
344
+ if k == 'Ref' and v == search
345
+ tree[k] = replacement
346
+ end
347
+
348
+ if k == 'Fn::GetAtt' and v[0] == search
349
+ tree[k] = [replacement, v[1]]
350
+ end
351
+
352
+ if v.is_a? Array or v.is_a? Hash
353
+ rename_resource(v, search, replacement)
354
+ end
355
+ end if tree.is_a? Hash
356
+
357
+ tree.each do |element|
358
+ rename_resource(element, search, replacement)
359
+ end if tree.is_a? Array
360
+
361
+ end
362
+
363
+ # rename cloudformation mapping in cfn model
364
+ def self.rename_mapping(tree, search, replacement)
365
+ tree.keys.each do |k|
366
+ v = tree[k]
367
+ if k == 'Fn::FindInMap' and v[0] = search
368
+ tree[k] = [replacement, v[1], v[2]]
369
+ end
370
+
371
+ if v.is_a? Array or v.is_a? Hash
372
+ rename_mapping(v, search, replacement)
373
+ end
374
+ end if tree.is_a? Hash
375
+
376
+ tree.each do |element|
377
+ rename_mapping(element, search, replacement)
378
+ end if tree.is_a? Array
379
+
380
+ end
381
+
382
+ # rename cloudformation condition in cfn model
383
+ def self.rename_condition(tree, search, replacement)
384
+ # conditions can be referenced by Fn::If and Condition => cond
385
+ tree.keys.each do |k|
386
+ v = tree[k]
387
+ if k == 'Fn::If' and v[0] == search
388
+ tree[k] = [replacement, v[1], v[2]]
389
+ end
390
+
391
+ if k == 'Condition' and v == search
392
+ tree[k] = replacement
393
+ end
394
+
395
+ if v.is_a? Array or v.is_a? Hash
396
+ rename_condition(v, search, replacement)
397
+ end
398
+ end if tree.is_a? Hash
399
+
400
+ tree.each do |element|
401
+ rename_condition(element, search, replacement)
402
+ end if tree.is_a? Array
403
+
404
+ end
405
+
406
+ ## Replace single value in tree structure (represented)
407
+ # either as Hash or Array with another value
408
+ def self.value_replace(tree, search, replacement)
409
+ if tree.is_a? Hash
410
+ tree.keys.each do |root|
411
+ subtree = tree[root]
412
+ if root == search
413
+ tree[replacement] = subtree
414
+ tree.delete(root)
415
+ end
416
+ if subtree == search
417
+ tree[root] = replacement
418
+ end
419
+ if subtree.is_a? Hash or subtree.is_a? Array
420
+ value_replace(subtree, search, replacement)
421
+ end
422
+ end
423
+ elsif tree.is_a? Array
424
+ tree.each do |element|
425
+ if element == search
426
+ tree[tree.index element] = replacement
427
+ elsif element.is_a? Hash or element.is_a? Array
428
+ value_replace(element, search, replacement)
429
+ end
430
+ end
431
+ end
432
+ end
433
+
434
+ def self.flatten_key_names(component, template)
435
+ flatten_namespace('Conditions', component, template)
436
+ Debug.debug_dump_cfn(template, 'flat.conditions')
437
+
438
+ flatten_namespace('Mappings', component, template)
439
+ Debug.debug_dump_cfn(template, 'flat.mappings')
440
+
441
+ flatten_namespace('Resources', component, template)
442
+ Debug.debug_dump_cfn(template, 'flat.resources')
443
+
444
+ flatten_namespace('Outputs', component, template)
445
+ Debug.debug_dump_cfn(template, 'flat.outputs')
446
+ end
447
+
448
+ end
449
+ end
450
+ end
@@ -0,0 +1,37 @@
1
+ module Cfhighlander
2
+
3
+ module Util
4
+
5
+ class Debug
6
+
7
+ @@debug_folder_exists = false
8
+
9
+ def self.create_debug_folder
10
+ if ((ENV.key? 'CFHIGHLANDER_DEBUG') and (ENV['CFHIGHLANDER_DEBUG'] == '1'))
11
+ FileUtils.mkdir_p "#{ENV['CFHIGHLANDER_WORKDIR']}/out/debug/"
12
+ @@debug_folder_exists = true
13
+ end unless @@debug_folder_exists
14
+ end
15
+
16
+ def self.debug_dump_cfn(template, step_name)
17
+ create_debug_folder
18
+ if ((ENV.key? 'CFHIGHLANDER_DEBUG') and (ENV['CFHIGHLANDER_DEBUG'] == '1'))
19
+ template.subcomponents.each do |sub_component|
20
+ path = "#{ENV['CFHIGHLANDER_WORKDIR']}/out/debug/#{sub_component.cfn_name}_#{step_name}.yaml"
21
+ File.write(path, sub_component.component_loaded.cfn_model_raw.to_yaml)
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.debug_dump(model, dump_name)
27
+ create_debug_folder
28
+ if ((ENV.key? 'CFHIGHLANDER_DEBUG') and (ENV['CFHIGHLANDER_DEBUG'] == '1'))
29
+ path = "#{ENV['CFHIGHLANDER_WORKDIR']}/out/debug/#{dump_name}.yaml"
30
+ File.write(path, model.to_yaml)
31
+ end
32
+ end
33
+
34
+
35
+ end
36
+ end
37
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cfhighlander
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0.alpha.1538010880
4
+ version: 0.6.0.alpha.1539120146
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikola Tosic
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2018-09-27 00:00:00.000000000 Z
13
+ date: 2018-10-09 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: highline
@@ -212,6 +212,20 @@ dependencies:
212
212
  - - ">="
213
213
  - !ruby/object:Gem::Version
214
214
  version: 1.5.1
215
+ - !ruby/object:Gem::Dependency
216
+ name: duplicate
217
+ requirement: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - "~>"
220
+ - !ruby/object:Gem::Version
221
+ version: '1.1'
222
+ type: :runtime
223
+ prerelease: false
224
+ version_requirements: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - "~>"
227
+ - !ruby/object:Gem::Version
228
+ version: '1.1'
215
229
  - !ruby/object:Gem::Dependency
216
230
  name: rspec
217
231
  requirement: !ruby/object:Gem::Requirement
@@ -243,7 +257,6 @@ files:
243
257
  - cfndsl_ext/config/managed_policies.yaml
244
258
  - cfndsl_ext/iam_helper.rb
245
259
  - cfndsl_ext/lambda_helper.rb
246
- - cfndsl_ext/sg.rb
247
260
  - hl_ext/aws_helper.rb
248
261
  - hl_ext/common_helper.rb
249
262
  - hl_ext/mapping_helper.rb
@@ -261,6 +274,8 @@ files:
261
274
  - lib/cfhighlander.model.templatemeta.rb
262
275
  - lib/cfhighlander.publisher.rb
263
276
  - lib/cfhighlander.validator.rb
277
+ - lib/util/cloudformation.util.rb
278
+ - lib/util/debug.util.rb
264
279
  - lib/util/zip.util.rb
265
280
  - templates/cfndsl.component.template.erb
266
281
  homepage: https://github.com/theonestack/cfhighlander/blob/master/README.md
data/cfndsl_ext/sg.rb DELETED
@@ -1,53 +0,0 @@
1
- require 'netaddr'
2
-
3
- def sg_create_rules (x, ip_blocks={})
4
- rules = []
5
- x.each do | group |
6
- group['ips'].each do |ip|
7
- group['rules'].each do |rule|
8
- lookup_ips_for_sg(ip_blocks, ip).each do |cidr|
9
- rules << { IpProtocol: "#{rule['IpProtocol']}", FromPort: "#{rule['FromPort']}", ToPort: "#{rule['ToPort']}", CidrIp: cidr }
10
- end
11
- end
12
- end
13
- end
14
- return rules
15
- end
16
-
17
-
18
- def lookup_ips_for_sg (ips, ip_block_name={})
19
- cidr = []
20
- if ip_block_name == 'stack'
21
- cidr = [FnJoin( "", [ "10.", Ref('StackOctet'), ".", "0.0/16" ] )]
22
- elsif ips.has_key? ip_block_name
23
- ips[ip_block_name].each do |ip|
24
- if (ips.include?(ip) || ip_block_name == 'stack')
25
- cidr += lookup_ips_for_sg(ips, ip) unless ip == ip_block_name
26
- else
27
- if ip == 'stack'
28
- cidr << [FnJoin( "", [ "10.", Ref('StackOctet'), ".", "0.0/16" ] )]
29
- elsif(isCidr(ip))
30
- cidr << ip
31
- else
32
- STDERR.puts("WARN: ip #{ip} is not a valid CIDR. Ignoring IP")
33
- end
34
- end
35
- end
36
- else
37
- if isCidr(ip_block_name)
38
- cidr = [ip_block_name]
39
- else
40
- STDERR.puts("WARN: ip #{ip_block_name} is not a valid CIDR. Ignoring IP")
41
- end
42
- end
43
- cidr
44
- end
45
-
46
- def isCidr(block)
47
- begin
48
- NetAddr::CIDR.create(block)
49
- return block.include?('/')
50
- rescue NetAddr::ValidationError
51
- return false
52
- end
53
- end