cfndsl 0.17.5 → 1.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +4 -4
  4. data/.travis.yml +6 -1
  5. data/CHANGELOG.md +32 -34
  6. data/README.md +68 -21
  7. data/Rakefile +25 -3
  8. data/TODO.md +18 -0
  9. data/UPGRADING.md +22 -0
  10. data/cfndsl.gemspec +3 -3
  11. data/exe/cfndsl +5 -0
  12. data/lib/cfndsl.rb +2 -116
  13. data/lib/cfndsl/aws/cloud_formation_template.rb +8 -1
  14. data/lib/cfndsl/aws/patches/000_sam.spec.json +574 -0
  15. data/lib/cfndsl/aws/patches/100_sam.spec_DeploymentPreference_patch.json +64 -0
  16. data/lib/cfndsl/aws/patches/500_Cognito_IdentityPoolRoleAttachment_patches.json +25 -0
  17. data/lib/cfndsl/aws/patches/500_IoT1Click_patch_PlacementTemplate_DeviceTemplates.json +20 -0
  18. data/lib/cfndsl/aws/patches/500_SAM_Serverless_Function_S3Event_Events_patch.json +16 -0
  19. data/lib/cfndsl/aws/patches/500_SAM_Serverless_Function_S3Location_Version_patch.json +16 -0
  20. data/lib/cfndsl/aws/patches/500_SSM_AssociationName_patch.json +16 -0
  21. data/lib/cfndsl/aws/patches/500_VPCEndpoint_patch.json +17 -0
  22. data/lib/cfndsl/aws/patches/510_ElasticSearch_Domain_patches.json +15 -0
  23. data/lib/cfndsl/aws/patches/600_RefKinds_patch.json +3654 -0
  24. data/lib/cfndsl/aws/patches/700_SAM_Serverless_Function_InlineCode_patch.json +20 -0
  25. data/lib/cfndsl/aws/patches/800_List_types_patch.json +115 -0
  26. data/lib/cfndsl/aws/resource_specification.json +35809 -11627
  27. data/lib/cfndsl/aws/types.rb +3 -3
  28. data/lib/cfndsl/cfnlego.rb +34 -0
  29. data/lib/{cfnlego → cfndsl/cfnlego}/cloudformation.erb +0 -0
  30. data/lib/{cfnlego → cfndsl/cfnlego}/cloudformation.rb +0 -0
  31. data/lib/{cfnlego → cfndsl/cfnlego}/resource.rb +3 -8
  32. data/lib/cfndsl/cloudformation.rb +107 -0
  33. data/lib/cfndsl/conditions.rb +11 -1
  34. data/lib/cfndsl/creation_policy.rb +1 -1
  35. data/lib/cfndsl/deep_merge.rb +4 -0
  36. data/lib/cfndsl/external_parameters.rb +4 -13
  37. data/lib/cfndsl/globals.rb +48 -9
  38. data/lib/cfndsl/jsonable.rb +22 -60
  39. data/lib/cfndsl/mappings.rb +1 -1
  40. data/lib/cfndsl/module.rb +16 -5
  41. data/lib/cfndsl/orchestration_template.rb +185 -83
  42. data/lib/cfndsl/outputs.rb +5 -1
  43. data/lib/cfndsl/parameters.rb +1 -1
  44. data/lib/cfndsl/plurals.rb +12 -1
  45. data/lib/cfndsl/properties.rb +1 -1
  46. data/lib/cfndsl/rake_task.rb +206 -12
  47. data/lib/cfndsl/ref_check.rb +19 -11
  48. data/lib/cfndsl/resources.rb +6 -19
  49. data/lib/cfndsl/rules.rb +1 -1
  50. data/lib/cfndsl/runner.rb +143 -0
  51. data/lib/cfndsl/specification.rb +80 -95
  52. data/lib/cfndsl/types.rb +205 -91
  53. data/lib/cfndsl/update_policy.rb +1 -1
  54. data/lib/cfndsl/version.rb +1 -1
  55. data/sample/autoscale.rb +0 -1
  56. data/sample/autoscale2.rb +0 -1
  57. data/sample/config_service.rb +2 -2
  58. data/sample/t1.rb +1 -1
  59. data/sample/vpc_example.rb +1 -1
  60. data/sample/vpc_with_vpn_example.rb +1 -1
  61. data/spec/aws/list_type_patches_spec.rb +35 -0
  62. data/spec/aws/nested_arrays_spec.rb +155 -3
  63. data/spec/aws/serverless_spec.rb +0 -2
  64. data/spec/cfndsl_spec.rb +94 -78
  65. data/spec/cli_spec.rb +16 -54
  66. data/spec/cloud_formation_template_spec.rb +233 -0
  67. data/spec/condition_spec.rb +24 -0
  68. data/spec/direct_ruby_spec.rb +19 -0
  69. data/spec/external_parameters_spec.rb +2 -15
  70. data/spec/fixtures/condition-assertion.json +1 -0
  71. data/spec/fixtures/test.rb +2 -1
  72. data/spec/generate_spec.rb +4 -2
  73. data/spec/resources_spec.rb +0 -7
  74. data/spec/spec_helper.rb +2 -7
  75. data/spec/support/shared_examples/orchestration_template.rb +15 -2
  76. data/spec/types_definition_spec.rb +3 -6
  77. metadata +52 -23
  78. data/bin/cfndsl +0 -160
  79. data/lib/cfndsl/errors.rb +0 -31
  80. data/lib/cfndsl/os/heat_template.rb +0 -18
  81. data/lib/cfndsl/os/types.rb +0 -14
  82. data/lib/cfndsl/os/types.yaml +0 -2423
  83. data/lib/cfndsl/patches.rb +0 -226
  84. data/lib/cfnlego.rb +0 -44
  85. data/spec/fixtures/heattest.rb +0 -24
  86. data/spec/heat_template_spec.rb +0 -7
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cfndsl/jsonable'
3
+ require_relative 'jsonable'
4
4
 
5
5
  module CfnDsl
6
6
  # Handles mapping objects
data/lib/cfndsl/module.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cfndsl/plurals'
4
- require 'cfndsl/names'
3
+ require_relative 'plurals'
4
+ require_relative 'names'
5
5
 
6
6
  # Adds some dsl module helpers
7
7
  class Module
@@ -53,6 +53,7 @@ class Module
53
53
  # on the main object, and the block is then evaluated in the context
54
54
  # of the new object.
55
55
  #
56
+ # rubocop:disable Metrics/MethodLength
56
57
  def dsl_content_object(*symbols)
57
58
  symbols.each do |symbol|
58
59
  plural = CfnDsl::Plurals.pluralize(symbol) # @@plurals[symbol] || "#{symbol}s"
@@ -67,12 +68,22 @@ class Module
67
68
  hash = {}
68
69
  instance_variable_set(pluralvar, hash)
69
70
  end
70
- hash[name] ||= definition_class.new(*values)
71
- hash[name].instance_eval(&block) if block
72
- return hash[name]
71
+ instance = hash[name]
72
+
73
+ if !instance
74
+ instance = definition_class.new(*values)
75
+ hash[name] = instance
76
+ elsif !instance.is_a?(definition_class)
77
+ raise ArgumentError, "#{method}(#{name}) already exists and is not a #{definition_class}"
78
+ elsif !values.empty?
79
+ raise ArgumentError, "wrong number of arguments (given #{values.size + 1}, expected 1) as #{method}(#{name}) already exists"
80
+ end
81
+ instance.instance_eval(&block) if block
82
+ return instance
73
83
  end
74
84
  end
75
85
  end
76
86
  end
87
+ # rubocop:enable Metrics/MethodLength
77
88
  end
78
89
  end
@@ -1,10 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cfndsl/jsonable'
4
- require 'cfndsl/names'
5
- require 'cfndsl/aws/types'
6
- require 'cfndsl/os/types'
7
- require 'cfndsl/globals'
3
+ require_relative 'globals'
4
+ require_relative 'module'
5
+ require_relative 'jsonable'
6
+ require_relative 'names'
7
+ require_relative 'plurals'
8
+ require_relative 'ref_check'
9
+ require_relative 'properties'
10
+ require_relative 'update_policy'
11
+ require_relative 'creation_policy'
12
+ require_relative 'conditions'
13
+ require_relative 'mappings'
14
+ require_relative 'resources'
15
+ require_relative 'rules'
16
+ require_relative 'parameters'
17
+ require_relative 'outputs'
18
+
19
+ require 'tsort'
8
20
 
9
21
  module CfnDsl
10
22
  # Handles the overall template object
@@ -34,7 +46,7 @@ module CfnDsl
34
46
 
35
47
  abreve_name = parts.join('_')
36
48
  if accessors.key? abreve_name
37
- accessors.delete abreve_name # Delete potentially ambiguous names
49
+ accessors[abreve_name] = :duplicate # Delete potentially ambiguous names
38
50
  else
39
51
  accessors[abreve_name] = type_module.const_get resource_name
40
52
  types_mapping[abreve_name] = resource
@@ -42,17 +54,25 @@ module CfnDsl
42
54
  parts.shift
43
55
  end
44
56
  end
45
- accessors.each_pair { |acc, res| create_resource_accessor(acc, res, types_mapping[acc]) }
57
+ accessors.each_pair { |acc, res| create_resource_accessor(acc, res, types_mapping[acc]) unless res == :duplicate }
46
58
  end
47
59
 
48
60
  def create_resource_def(name, info)
49
- resource = Class.new ResourceDefinition
61
+ resource = Class.new ResourceDefinition do
62
+ # do not allow Type to be respecified
63
+ def Type(type = nil)
64
+ return @Type unless type
65
+ raise CfnDsl::Error, "Cannot override previously defined Type #{@Type} with #{type}" unless type == @Type
66
+
67
+ super
68
+ end
69
+ end
50
70
  resource_name = name.gsub(/::/, '_')
51
71
  type_module.const_set(resource_name, resource)
52
72
  info['Properties'].each_pair do |pname, ptype|
53
73
  if ptype.is_a? Array
54
74
  pclass = type_module.const_get ptype.first
55
- create_array_property_def(resource, pname, pclass)
75
+ create_array_property_def(resource, pname, pclass, info)
56
76
  else
57
77
  pclass = type_module.const_get ptype
58
78
  create_property_def(resource, pname, pclass)
@@ -61,9 +81,66 @@ module CfnDsl
61
81
  resource_name
62
82
  end
63
83
 
64
- def create_property_def(resource, pname, pclass)
84
+ # rubocop:disable Metrics/PerceivedComplexity
85
+ def create_array_property_def(resource, pname, pclass, info)
86
+ singular_name = CfnDsl::Plurals.singularize pname
87
+ plural_name = singular_name == pname ? CfnDsl::Plurals.pluralize(pname) : pname
88
+
89
+ if singular_name == plural_name
90
+ # Generate the extended list concat method
91
+ plural_name = nil
92
+ elsif pname == plural_name && info['Properties'].include?(singular_name)
93
+ # The singlular name is a different property, do not redefine it here but rather use the extended form
94
+ # with the plural name. This allows construction of deep types, but no mechanism to overwrite a previous value
95
+ # (eg CodePipeline::Pipeline ArtifactStores vs ArtifactStore)
96
+ # Note is is also possible (but unlikely) for the spec to change in a way that triggers this condition where it did not
97
+ # before which will result in breaking behaviour for existing apps.
98
+ singular_name = plural_name
99
+ plural_name = nil
100
+ elsif pname == singular_name && info['Properties'].include?(plural_name)
101
+ # The plural name is a different property, do not redefine it here
102
+ # Note it is unlikely that a singular form is going to be a List property if the plural form also exists.
103
+ plural_name = singular_name
104
+ end
105
+
106
+ # Plural form just a normal property definition expecting an Array type
107
+ create_property_def(resource, pname, Array, plural_name) if plural_name
108
+
109
+ # Singular form understands concatenation and Fn::If property
110
+ create_singular_property_def(resource, pname, pclass, singular_name) if singular_name
111
+ end
112
+ # rubocop:enable Metrics/PerceivedComplexity
113
+
114
+ def create_resource_accessor(accessor, resource, type)
115
+ class_eval do
116
+ CfnDsl.method_names(accessor) do |method|
117
+ define_method(method) do |name, *values, &block|
118
+ name = name.to_s
119
+ @Resources ||= {}
120
+ instance = @Resources[name]
121
+ if !instance
122
+ instance = resource.new(*values)
123
+ # Previously the type was set after the block was evaled
124
+ # But now trying to reset Type on a specific subtype will raise exception
125
+ instance.instance_variable_set('@Type', type)
126
+ @Resources[name] = instance
127
+ elsif type != (other_type = instance.instance_variable_get('@Type'))
128
+ raise ArgumentError, "Resource #{name}<#{other_type}> exists, and is not a <#{type}>"
129
+ elsif !values.empty?
130
+ raise ArgumentError, "wrong number of arguments (given #{values.size + 1}, expected 1) as Resource #{name} already exists"
131
+ end
132
+ @Resources[name].instance_eval(&block) if block
133
+ instance
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ def create_property_def(resource, pname, pclass, method_name = pname)
65
142
  resource.class_eval do
66
- CfnDsl.method_names(pname) do |method|
143
+ CfnDsl.method_names(method_name) do |method|
67
144
  define_method(method) do |*values, &block|
68
145
  values.push pclass.new if values.empty?
69
146
  @Properties ||= {}
@@ -75,114 +152,139 @@ module CfnDsl
75
152
  end
76
153
  end
77
154
 
78
- def create_array_property_def(resource, pname, pclass)
79
- create_property_def(resource, pname, Array)
80
-
81
- sname = CfnDsl::Plurals.singularize pname
82
-
83
- return if sname == pname
84
-
155
+ def create_singular_property_def(resource, pname, pclass, singular_name)
85
156
  resource.class_eval do
86
- CfnDsl.method_names(sname) do |method|
87
- define_method(method) do |value = nil, &block|
157
+ CfnDsl.method_names(singular_name) do |method|
158
+ define_method(method) do |value = nil, fn_if: nil, **hash_value, &block|
159
+ value = hash_value unless value || hash_value.empty?
88
160
  @Properties ||= {}
89
161
  @Properties[pname] ||= PropertyDefinition.new([])
90
- value ||= pclass.new
91
- @Properties[pname].value.push value
92
- value.instance_eval(&block) if block
162
+ if value.is_a?(Array)
163
+ @Properties[pname].value.concat(value)
164
+ else
165
+ value ||= pclass.new
166
+ @Properties[pname].value.push fn_if ? FnIf(fn_if, value, Ref('AWS::NoValue')) : value
167
+ value.instance_eval(&block) if block
168
+ end
93
169
  value
94
170
  end
95
171
  end
96
172
  end
97
173
  end
174
+ end
98
175
 
99
- def create_resource_accessor(accessor, resource, type)
100
- class_eval do
101
- CfnDsl.method_names(accessor) do |method|
102
- define_method(method) do |name, *values, &block|
103
- name = name.to_s
104
- @Resources ||= {}
105
- @Resources[name] ||= resource.new(*values)
106
- @Resources[name].instance_eval(&block) if block
107
- @Resources[name].instance_variable_set('@Type', type)
108
- @Resources[name]
109
- end
110
- end
111
- end
176
+ def initialize(description = nil, &block)
177
+ @AWSTemplateFormatVersion = '2010-09-09'
178
+ @Description = description if description
179
+ declare(&block) if block_given?
180
+ end
181
+
182
+ alias _Condition Condition
183
+
184
+ # Condition has two usages at this level
185
+ # @overload Condition(name,expression)
186
+ # @overload Condition(name) - referencing a condition in a condition expression
187
+ def Condition(name, expression = nil)
188
+ if expression
189
+ _Condition(name, expression)
190
+ else
191
+ { Condition: ConditionDefinition.new(name) }
112
192
  end
113
193
  end
114
194
 
115
- def initialize
116
- @AWSTemplateFormatVersion = '2010-09-09'
195
+ def check_refs
196
+ invalids = check_condition_refs + check_resource_refs + check_output_refs + check_rule_refs
197
+ invalids unless invalids.empty?
117
198
  end
118
199
 
119
- # rubocop:disable Metrics/PerceivedComplexity
120
- def valid_ref?(ref, origin = nil)
200
+ def valid_ref?(ref, ref_containers = [GLOBAL_REFS, @Resources, @Parameters])
121
201
  ref = ref.to_s
122
- origin = origin.to_s if origin
202
+ ref_containers.any? { |c| c && c.key?(ref) }
203
+ end
123
204
 
124
- return true if GLOBAL_REFS.key?(ref)
205
+ def check_condition_refs
206
+ invalids = []
125
207
 
126
- return true if @Parameters&.key?(ref)
208
+ # Conditions can refer to other conditions in Fn::And, Fn::Or and Fn::Not
209
+ invalids.concat(_check_refs(:Condition, :condition_refs, [@Conditions]))
127
210
 
128
- return !origin || !@_resource_refs || !@_resource_refs[ref] || !@_resource_refs[ref].key?(origin) if @Resources.key?(ref)
211
+ # They can also Ref Globals and Parameters (but not Resources))
212
+ invalids.concat(_check_refs(:Condition, :all_refs, [GLOBAL_REFS, @Parameters]))
213
+ end
129
214
 
130
- false
215
+ def check_resource_refs
216
+ invalids = []
217
+ invalids.concat(_check_refs(:Resource, :all_refs, [@Resources, GLOBAL_REFS, @Parameters]))
218
+
219
+ # DependsOn and conditions in Fn::If expressions
220
+ invalids.concat(_check_refs(:Resource, :condition_refs, [@Conditions]))
131
221
  end
132
- # rubocop:enable Metrics/PerceivedComplexity
133
222
 
134
- def check_refs
135
- invalids = check_resource_refs + check_output_refs + check_rule_refs
136
- invalids unless invalids.empty?
223
+ def check_output_refs
224
+ invalids = []
225
+ invalids.concat(_check_refs(:Output, :all_refs, [@Resources, GLOBAL_REFS, @Parameters]))
226
+ invalids.concat(_check_refs(:Output, :condition_refs, [@Conditions]))
137
227
  end
138
228
 
139
- def check_resource_refs
229
+ def check_rule_refs
140
230
  invalids = []
141
- @_resource_refs = {}
142
- if @Resources
143
- @Resources.each_key do |resource|
144
- @_resource_refs[resource.to_s] = @Resources[resource].build_references({})
145
- end
146
- @_resource_refs.each_key do |origin|
147
- @_resource_refs[origin].each_key do |ref|
148
- invalids.push "Invalid Reference: Resource #{origin} refers to #{ref}" unless valid_ref?(ref, origin)
149
- end
150
- end
151
- end
231
+ invalids.concat(_check_refs(:Rule, :all_refs, [@Resources, GLOBAL_REFS, @Parameters]))
232
+ invalids.concat(_check_refs(:Rule, :condition_refs, [@Conditions]))
152
233
  invalids
153
234
  end
154
235
 
155
- def check_output_refs
156
- invalids = []
157
- output_refs = {}
158
- if @Outputs
159
- @Outputs.each_key do |resource|
160
- output_refs[resource.to_s] = @Outputs[resource].build_references({})
161
- end
162
- output_refs.each_key do |origin|
163
- output_refs[origin].each_key do |ref|
164
- invalids.push "Invalid Reference: Output #{origin} refers to #{ref}" unless valid_ref?(ref)
165
- end
166
- end
236
+ # For testing for cycles
237
+ class RefHash < Hash
238
+ include TSort
239
+
240
+ alias tsort_each_node each_key
241
+ def tsort_each_child(node, &block)
242
+ fetch(node, []).each(&block)
167
243
  end
168
- invalids
169
244
  end
170
245
 
171
- def check_rule_refs
246
+ # rubocop:disable Metrics/PerceivedComplexity
247
+ def _check_refs(container_name, method, source_containers)
248
+ container = instance_variable_get("@#{container_name}s")
249
+ return [] unless container
250
+
172
251
  invalids = []
173
- @_rule_refs = {}
174
- if @Rules
175
- @Rules.each_key do |rule|
176
- @_rule_refs[resource.to_s] = @Rules[rule].build_references({})
252
+ referred_by = RefHash.new { |h, k| h[k] = [] }
253
+ self_check = source_containers.first.eql?(container)
254
+
255
+ container.each_pair do |name, entry|
256
+ name = name.to_s
257
+ begin
258
+ refs = entry.build_references([], self_check && name, method)
259
+ refs.each { |r| referred_by[r.to_s] << name }
260
+ rescue RefCheck::SelfReference, RefCheck::NullReference => e
261
+ # Topological sort will not detect self or null references
262
+ invalids.push("#{container_name} #{e.message}")
177
263
  end
178
- @_rule_refs.each_key do |origin|
179
- @_rule_refs[origin].each_key do |ref|
180
- invalids.push "Invalid Reference: Rule #{origin} refers to #{ref}" unless valid_ref?(ref, origin)
181
- end
264
+ end
265
+
266
+ referred_by.each_pair do |ref, names|
267
+ unless valid_ref?(ref, source_containers)
268
+ invalids.push "Invalid Reference: #{container_name}s #{names} refer to unknown #{method == :condition_refs ? 'Condition' : 'Reference'} #{ref}"
182
269
  end
183
270
  end
271
+
272
+ begin
273
+ referred_by.tsort if self_check && invalids.empty? # Check for cycles
274
+ rescue TSort::Cyclic => e
275
+ invalids.push "Cyclic references found in #{container_name}s #{referred_by} - #{e.message}"
276
+ end
277
+
184
278
  invalids
185
279
  end
280
+ # rubocop:enable Metrics/PerceivedComplexity
281
+
282
+ def validate
283
+ errors = check_refs || []
284
+ raise CfnDsl::Error, "#{errors.size} errors in template\n#{errors.join("\n")}" unless errors.empty?
285
+
286
+ self
287
+ end
186
288
  end
187
289
  # rubocop:enable Metrics/ClassLength
188
290
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cfndsl/jsonable'
3
+ require_relative 'jsonable'
4
4
 
5
5
  module CfnDsl
6
6
  # Handles Output objects
@@ -14,5 +14,9 @@ module CfnDsl
14
14
  def initialize(value = nil)
15
15
  @Value = value if value
16
16
  end
17
+
18
+ def condition_refs
19
+ [@Condition].flatten.compact.map(&:to_s)
20
+ end
17
21
  end
18
22
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cfndsl/jsonable'
3
+ require_relative 'jsonable'
4
4
 
5
5
  module CfnDsl
6
6
  # Handles input parameter objects
@@ -24,7 +24,18 @@ module CfnDsl
24
24
  end
25
25
 
26
26
  def singularize(name)
27
- @singles.fetch(name.to_s) { |key| key[0..-2] }
27
+ @singles.fetch(name.to_s) do |key|
28
+ case key
29
+ when /List$/
30
+ key[0..-5]
31
+ when /ies$/
32
+ key[0..-4] + 'y'
33
+ when /s$/
34
+ key[0..-2]
35
+ else
36
+ key
37
+ end
38
+ end
28
39
  end
29
40
  end
30
41
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cfndsl/jsonable'
3
+ require_relative 'jsonable'
4
4
 
5
5
  module CfnDsl
6
6
  # Handles property objects for Resources