cfhighlander 0.6.0.alpha.1538010880 → 0.6.0.alpha.1539120146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +40 -0
- data/bin/cfhighlander.rb +1 -0
- data/hl_ext/mapping_helper.rb +2 -2
- data/lib/cfhighlander.compiler.rb +18 -6
- data/lib/cfhighlander.dsl.base.rb +4 -0
- data/lib/cfhighlander.dsl.subcomponent.rb +10 -8
- data/lib/cfhighlander.dsl.template.rb +3 -4
- data/lib/cfhighlander.factory.templatefinder.rb +2 -1
- data/lib/cfhighlander.model.component.rb +13 -5
- data/lib/cfhighlander.publisher.rb +7 -2
- data/lib/cfhighlander.validator.rb +0 -1
- data/lib/util/cloudformation.util.rb +450 -0
- data/lib/util/debug.util.rb +37 -0
- metadata +18 -3
- data/cfndsl_ext/sg.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a73647965281a89f8bb2bc78e8282e81604f13b5aa06a48ab2ad0f7c21ec78b
|
4
|
+
data.tar.gz: 60a0df940c9a7675ec62de5f636144496bd88dad4ddfcaeae1e5c6f6669dca94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/hl_ext/mapping_helper.rb
CHANGED
@@ -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
|
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
|
-
|
159
|
-
|
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
|
|
@@ -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
|
-
|
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
|
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
|
@@ -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.
|
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
|
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
|