cfndsl 1.0.5 → 1.3.0

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.
@@ -14,7 +14,7 @@ module Cfnlego
14
14
  end
15
15
 
16
16
  def render
17
- erb = ERB.new(File.read(TEMPLATE), nil, '-')
17
+ erb = ERB.new(File.read(TEMPLATE), trim_mode: '-')
18
18
  erb.result(binding)
19
19
  end
20
20
  end
@@ -43,9 +43,10 @@ module CfnDsl
43
43
  params.load_file file
44
44
  when :raw
45
45
  file_parts = file.split('=')
46
- if file_parts[1].downcase == 'true'
46
+ case file_parts[1].downcase
47
+ when 'true'
47
48
  params.set_param(file_parts[0], true)
48
- elsif file_parts[1].downcase == 'false'
49
+ when 'false'
49
50
  params.set_param(file_parts[0], false)
50
51
  else
51
52
  params.set_param(*file.split('='))
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'version'
4
+ require 'fileutils'
4
5
 
5
6
  # Global variables to adjust CfnDsl behavior
6
7
  module CfnDsl
@@ -87,7 +87,7 @@ module CfnDsl
87
87
  def FnSub(string, substitutions = nil)
88
88
  raise ArgumentError, 'The first argument passed to Fn::Sub must be a string' unless string.is_a? String
89
89
 
90
- refs = string.scan(FN_SUB_SCANNER).map(&:first)
90
+ refs = string.scan(FN_SUB_SCANNER).map(&:first).map { |r| r.split('.', 2).first }
91
91
 
92
92
  if substitutions
93
93
  raise ArgumentError, 'The second argument passed to Fn::Sub must be a Hash' unless substitutions.is_a? Hash
@@ -136,12 +136,13 @@ module CfnDsl
136
136
  check_names
137
137
  hash = {}
138
138
  instance_variables.each do |var|
139
- name = var[1..-1]
139
+ name = var[1..]
140
140
 
141
- if name =~ /^__/
141
+ case name
142
+ when /^__/
142
143
  # if a variable starts with double underscore, strip one off
143
- name = name[1..-1]
144
- elsif name =~ /^_/
144
+ name = name[1..]
145
+ when /^_/
145
146
  # Hide variables that start with single underscore
146
147
  name = nil
147
148
  end
@@ -169,7 +170,7 @@ module CfnDsl
169
170
  def check_names
170
171
  return if instance_variable_get('@Resources').nil?
171
172
 
172
- instance_variable_get('@Resources').keys.each do |name|
173
+ instance_variable_get('@Resources').each_key do |name|
173
174
  next unless name !~ /\A\p{Alnum}+\z/
174
175
 
175
176
  warn "Resource name: #{name} is invalid"
@@ -22,7 +22,7 @@ module CfnDsl
22
22
  # Handles the overall template object
23
23
  # rubocop:disable Metrics/ClassLength
24
24
  class OrchestrationTemplate < JSONable
25
- dsl_attr_setter :AWSTemplateFormatVersion, :Description, :Metadata, :Transform
25
+ dsl_attr_setter :AWSTemplateFormatVersion, :Description, :Metadata, :Transform, :Hooks
26
26
  dsl_content_object :Condition, :Parameter, :Output, :Resource, :Mapping, :Rule
27
27
 
28
28
  GLOBAL_REFS = {
@@ -83,7 +83,6 @@ module CfnDsl
83
83
  resource_name
84
84
  end
85
85
 
86
- # rubocop:disable Metrics/PerceivedComplexity
87
86
  def create_array_property_def(resource, pname, pclass, info)
88
87
  singular_name = CfnDsl::Plurals.singularize pname
89
88
  plural_name = singular_name == pname ? CfnDsl::Plurals.pluralize(pname) : pname
@@ -111,7 +110,6 @@ module CfnDsl
111
110
  # Singular form understands concatenation and Fn::If property
112
111
  create_singular_property_def(resource, pname, pclass, singular_name) if singular_name
113
112
  end
114
- # rubocop:enable Metrics/PerceivedComplexity
115
113
 
116
114
  def create_resource_accessor(accessor, resource, type)
117
115
  class_eval do
@@ -245,7 +243,7 @@ module CfnDsl
245
243
  end
246
244
  end
247
245
 
248
- # rubocop:disable Metrics/PerceivedComplexity
246
+ # rubocop:disable Metrics/CyclomaticComplexity
249
247
  def _check_refs(container_name, method, source_containers)
250
248
  container = instance_variable_get("@#{container_name}s")
251
249
  return [] unless container
@@ -279,7 +277,7 @@ module CfnDsl
279
277
 
280
278
  invalids
281
279
  end
282
- # rubocop:enable Metrics/PerceivedComplexity
280
+ # rubocop:enable Metrics/CyclomaticComplexity
283
281
 
284
282
  def validate
285
283
  errors = check_refs || []
@@ -20,7 +20,7 @@ module CfnDsl
20
20
  @singles = @plurals.invert
21
21
 
22
22
  def pluralize(name)
23
- @plurals.fetch(name.to_s) { |key| key + 's' }
23
+ @plurals.fetch(name.to_s) { |key| "#{key}s" }
24
24
  end
25
25
 
26
26
  def singularize(name)
@@ -29,7 +29,7 @@ module CfnDsl
29
29
  when /List$/
30
30
  key[0..-5]
31
31
  when /ies$/
32
- key[0..-4] + 'y'
32
+ "#{key[0..-4]}y"
33
33
  when /s$/
34
34
  key[0..-2]
35
35
  else
@@ -79,7 +79,7 @@ module CfnDsl
79
79
  # then any existing file is considered sufficient, and 'latest' is the version used for downloading
80
80
  #
81
81
  # @todo Add capability to provide a user spec/patches dir
82
- def specification(name: nil, file:, version: nil)
82
+ def specification(file:, name: nil, version: nil)
83
83
  if name
84
84
  desc 'Update Resource Specification' unless ::Rake.application.last_description
85
85
  task name, [:cfn_spec_version] => file
@@ -218,7 +218,7 @@ module CfnDsl
218
218
  end
219
219
 
220
220
  def verbose
221
- (Rake.verbose? || cfndsl_opts&.fetch(:verbose, false)) && STDERR
221
+ (Rake.verbose? || cfndsl_opts&.fetch(:verbose, false)) && $stderr
222
222
  end
223
223
 
224
224
  def generate(opts)
@@ -238,8 +238,8 @@ module CfnDsl
238
238
  verbose&.puts("Writing to #{type}")
239
239
  end
240
240
 
241
- def outputter(opts)
242
- opts[:output].nil? ? yield(STDOUT) : file_output(opts[:output]) { |f| yield f }
241
+ def outputter(opts, &block)
242
+ opts[:output].nil? ? yield($stdout) : file_output(opts[:output], &block)
243
243
  end
244
244
 
245
245
  def model(filename)
@@ -253,8 +253,8 @@ module CfnDsl
253
253
  cfndsl_opts.fetch(:extras, [])
254
254
  end
255
255
 
256
- def file_output(path)
257
- File.open(File.expand_path(path), 'w') { |f| yield f }
256
+ def file_output(path, &block)
257
+ File.open(File.expand_path(path), 'w', &block)
258
258
  end
259
259
  end
260
260
  # rubocop:enable Metrics/ClassLength
@@ -10,7 +10,7 @@ module RefCheck
10
10
  end
11
11
 
12
12
  # Build up a set of references.
13
- # rubocop:disable Metrics/PerceivedComplexity
13
+ # rubocop:disable Metrics/CyclomaticComplexity
14
14
  def build_references(refs = [], origin = nil, method = :all_refs)
15
15
  if respond_to?(method)
16
16
  send(method).each do |ref|
@@ -30,7 +30,7 @@ module RefCheck
30
30
 
31
31
  refs
32
32
  end
33
- # rubocop:enable Metrics/PerceivedComplexity
33
+ # rubocop:enable Metrics/CyclomaticComplexity
34
34
 
35
35
  def ref_children
36
36
  []
@@ -5,7 +5,7 @@ require_relative 'jsonable'
5
5
  module CfnDsl
6
6
  # Handles Resource objects
7
7
  class ResourceDefinition < JSONable
8
- dsl_attr_setter :Type, :DependsOn, :DeletionPolicy, :Condition, :Metadata
8
+ dsl_attr_setter :Type, :UpdateReplacePolicy, :DeletionPolicy, :Condition, :Metadata
9
9
  dsl_content_object :Property, :UpdatePolicy, :CreationPolicy
10
10
 
11
11
  def add_tag(name, value, propagate = nil)
@@ -16,6 +16,23 @@ module CfnDsl
16
16
  end
17
17
  end
18
18
 
19
+ # DependsOn can be a single value or a list
20
+ def DependsOn(value)
21
+ case @DependsOn
22
+ when nil
23
+ @DependsOn = value
24
+ when Array
25
+ @DependsOn << value
26
+ else
27
+ @DependsOn = [@DependsOn, value]
28
+ end
29
+ if @DependsOn.is_a?(Array)
30
+ @DependsOn.flatten!
31
+ @DependsOn.uniq!
32
+ end
33
+ @DependsOn
34
+ end
35
+
19
36
  def condition_refs
20
37
  [@Condition].flatten.compact.map(&:to_s)
21
38
  end
@@ -7,7 +7,6 @@ require_relative 'globals'
7
7
 
8
8
  module CfnDsl
9
9
  # Runner class to handle commandline invocation
10
- # rubocop:disable Metrics/ClassLength
11
10
  class Runner
12
11
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
13
12
  def self.invoke!
@@ -118,14 +117,14 @@ module CfnDsl
118
117
  end
119
118
 
120
119
  filename = File.expand_path(ARGV[0])
121
- verbose = options[:verbose] && STDERR
120
+ verbose = options[:verbose] && $stderr
122
121
 
123
122
  verbose.puts "Using specification file #{CfnDsl.specification_file}" if verbose
124
123
 
125
124
  require_relative 'cloudformation'
126
125
  model = CfnDsl.eval_file_with_extras(filename, options[:extras], verbose)
127
126
 
128
- output = STDOUT
127
+ output = $stdout
129
128
  if options[:output] != '-'
130
129
  verbose.puts("Writing to #{options[:output]}") if verbose
131
130
  output = File.open(File.expand_path(options[:output]), 'w')
@@ -143,4 +142,3 @@ module CfnDsl
143
142
  end
144
143
  # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
145
144
  end
146
- # rubocop:enable Metrics/ClassLength
@@ -70,20 +70,18 @@ module CfnDsl
70
70
  next unless %w[ResourceTypes PropertyTypes].include?(top_level_type)
71
71
 
72
72
  patches.each_pair do |property_type_name, patch_details|
73
- begin
74
- applies_to = spec[top_level_type]
75
- unless property_type_name == 'patch'
76
- # Patch applies within a specific property type
77
- applies_to = applies_to[property_type_name]
78
- patch_details = patch_details['patch']
79
- end
80
-
81
- Hana::Patch.new(patch_details['operations']).apply(applies_to) if patch_required?(patch_details)
82
- rescue Hana::Patch::MissingTargetException => e
83
- raise "Failed specification patch #{top_level_type} #{property_type_name} from #{from_file}" if fail_patches
84
-
85
- warn "Ignoring failed specification patch #{top_level_type} #{property_type_name} from #{from_file} - #{e.class.name}:#{e.message}"
73
+ applies_to = spec[top_level_type]
74
+ unless property_type_name == 'patch'
75
+ # Patch applies within a specific property type
76
+ applies_to = applies_to[property_type_name]
77
+ patch_details = patch_details['patch']
86
78
  end
79
+
80
+ Hana::Patch.new(patch_details['operations']).apply(applies_to) if patch_required?(patch_details)
81
+ rescue Hana::Patch::MissingTargetException => e
82
+ raise "Failed specification patch #{top_level_type} #{property_type_name} from #{from_file}" if fail_patches
83
+
84
+ warn "Ignoring failed specification patch #{top_level_type} #{property_type_name} from #{from_file} - #{e.class.name}:#{e.message}"
87
85
  end
88
86
  end
89
87
  end
@@ -16,7 +16,7 @@ module CfnDsl
16
16
  { 'Resources' => resources, 'Types' => types, 'Version' => spec.version, 'File' => spec.file }
17
17
  end
18
18
 
19
- # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength
19
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
20
20
  def self.extract_resources(spec)
21
21
  spec.each_with_object({}) do |(resource_name, resource_info), resources|
22
22
  properties = resource_info['Properties'].each_with_object({}) do |(property_name, property_info), extracted|
@@ -43,7 +43,8 @@ module CfnDsl
43
43
  # resource name for uniqueness and connection
44
44
  property_type = resource_name.split('::').join + property_info['Type']
45
45
  else
46
- warn "could not extract resource type from #{resource_name}"
46
+ warn "could not extract resource type for property #{property_name} from #{resource_name}, assuming Json"
47
+ property_type = 'Json'
47
48
  end
48
49
  extracted[property_name] = property_type
49
50
  extracted
@@ -52,9 +53,9 @@ module CfnDsl
52
53
  resources
53
54
  end
54
55
  end
55
- # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength
56
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
56
57
 
57
- # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength
58
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength
58
59
  def self.extract_types(spec)
59
60
  primitive_types = {
60
61
  'String' => 'String',
@@ -99,6 +100,8 @@ module CfnDsl
99
100
  nested_prop_type =
100
101
  if nested_prop_info['ItemType'] == 'Tag'
101
102
  ['Tag']
103
+ elsif (nested_prop_info['ItemType'] == 'Json') && (nested_prop_info['Type'] == 'List')
104
+ ['Json']
102
105
  else
103
106
  Array(root_resource_name + nested_prop_info['ItemType'])
104
107
  end
@@ -106,7 +109,8 @@ module CfnDsl
106
109
  elsif nested_prop_info['Type']
107
110
  nested_prop_type = root_resource_name + nested_prop_info['Type']
108
111
  else
109
- warn "could not extract property type from #{property_name}"
112
+ warn "could not extract property type for #{nested_prop_name} from #{property_name}, assuming Json"
113
+ nested_prop_type = 'Json'
110
114
  p nested_prop_info
111
115
  end
112
116
  extracted[nested_prop_name] = nested_prop_type
@@ -117,9 +121,9 @@ module CfnDsl
117
121
  types
118
122
  end
119
123
  end
120
- # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength
124
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength
121
125
 
122
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
126
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
123
127
  def self.included(type_def)
124
128
  types_list = extract_from_resource_spec
125
129
  type_def.const_set('Types_Internal', types_list)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CfnDsl
4
- VERSION = '1.0.5'
4
+ VERSION = '1.3.0'
5
5
  end
@@ -120,16 +120,16 @@ module DeepMerge
120
120
  puts "#{di} looping: #{src_key.inspect} => #{src_value.inspect} :: #{dest.inspect}" if merge_debug
121
121
  if dest[src_key]
122
122
  puts "#{di} ==>merging: #{src_key.inspect} => #{src_value.inspect} :: #{dest[src_key].inspect}" if merge_debug
123
- dest[src_key] = deep_merge!(src_value, dest[src_key], options.merge(debug_indent: di + ' '))
123
+ dest[src_key] = deep_merge!(src_value, dest[src_key], options.merge(debug_indent: "#{di} "))
124
124
  else # dest[src_key] doesn't exist so we want to create and overwrite it (but we do this via deep_merge!)
125
125
  puts "#{di} ==>merging over: #{src_key.inspect} => #{src_value.inspect}" if merge_debug
126
- # note: we rescue here b/c some classes respond to "dup" but don't implement it (Numeric, TrueClass, FalseClass, NilClass among maybe others)
126
+ # NOTE: we rescue here b/c some classes respond to "dup" but don't implement it (Numeric, TrueClass, FalseClass, NilClass among maybe others)
127
127
  begin
128
128
  src_dup = src_value.dup # we dup src_value if possible because we're going to merge into it (since dest is empty)
129
129
  rescue TypeError
130
130
  src_dup = src_value
131
131
  end
132
- dest[src_key] = deep_merge!(src_value, src_dup, options.merge(debug_indent: di + ' '))
132
+ dest[src_key] = deep_merge!(src_value, src_dup, options.merge(debug_indent: "#{di} "))
133
133
  end
134
134
  elsif dest.is_a?(Array) && extend_existing_arrays
135
135
  dest.push(source)
@@ -181,12 +181,12 @@ module DeepMerge
181
181
  list = []
182
182
  dest.each_index do |i|
183
183
  list[i] = deep_merge!(source[i] || {}, dest[i],
184
- options.merge(debug_indent: di + ' '))
184
+ options.merge(debug_indent: "#{di} "))
185
185
  end
186
- list += source[dest.count..-1] if source.count > dest.count
186
+ list += source[dest.count..] if source.count > dest.count
187
187
  dest = list
188
188
  elsif keep_array_duplicates
189
- dest = dest.concat(source)
189
+ dest.concat(source)
190
190
  else
191
191
  dest |= source
192
192
  end
@@ -215,9 +215,10 @@ module DeepMerge
215
215
  knockout_prefix = options[:knockout_prefix] || false
216
216
  di = options[:debug_indent] || ''
217
217
  if knockout_prefix && overwrite_unmergeable
218
- src_tmp = if source.is_a?(String) # remove knockout string from source before overwriting dest
218
+ src_tmp = case source
219
+ when String # remove knockout string from source before overwriting dest
219
220
  source.gsub(/^#{knockout_prefix}/, '')
220
- elsif source.is_a?(Array) # remove all knockout elements before overwriting dest
221
+ when Array # remove all knockout elements before overwriting dest
221
222
  source.delete_if { |ko_item| ko_item.is_a?(String) && ko_item.match(/^#{knockout_prefix}/) }
222
223
  else
223
224
  source
@@ -23,8 +23,8 @@ CloudFormation do
23
23
 
24
24
  10.times do |i|
25
25
  subnet = "subnet#{i}"
26
- route_table = subnet + 'RouteTable'
27
- route_table_assoc = route_table + 'Assoc'
26
+ route_table = "#{subnet}RouteTable"
27
+ route_table_assoc = "#{route_table}Assoc"
28
28
 
29
29
  Subnet(subnet) do
30
30
  VpcId Ref(:VPC)
@@ -42,7 +42,7 @@ CloudFormation do
42
42
  RouteTableId Ref(route_table)
43
43
  end
44
44
 
45
- EC2_Route(subnet + 'GatewayRoute') do
45
+ EC2_Route("#{subnet}GatewayRoute") do
46
46
  DependsOn :GatewayToInternet
47
47
  RouteTableId Ref(route_table)
48
48
  DestinationCidrBlock '0.0.0.0/0'
@@ -41,8 +41,8 @@ CloudFormation do
41
41
 
42
42
  10.times do |i|
43
43
  subnet = "subnet#{i}"
44
- route_table = subnet + 'RouteTable'
45
- route_table_assoc = route_table + 'Assoc'
44
+ route_table = "#{subnet}RouteTable"
45
+ route_table_assoc = "#{route_table}Assoc"
46
46
 
47
47
  Subnet(subnet) do
48
48
  VpcId Ref(:VPC)
@@ -60,7 +60,7 @@ CloudFormation do
60
60
  RouteTableId Ref(route_table)
61
61
  end
62
62
 
63
- EC2_Route(subnet + 'GatewayRoute') do
63
+ EC2_Route("#{subnet}GatewayRoute") do
64
64
  DependsOn :GatewayToInternet
65
65
  RouteTableId Ref(route_table)
66
66
  DestinationCidrBlock '0.0.0.0/0'
@@ -188,6 +188,26 @@ describe CfnDsl::CloudFormationTemplate do
188
188
  end
189
189
  end
190
190
 
191
+ it 'composes DependsOn' do
192
+ spec = self
193
+ subject.Resource('SomeResource') do
194
+ d = DependsOn('X')
195
+ spec.expect(d).to spec.eq('X') # start with a single value, stays a single value
196
+ d = DependsOn(%w[Y Z])
197
+ spec.expect(d).to spec.eq(%w[X Y Z]) # concatenates values
198
+ d = DependsOn('Y') # uniqeness
199
+ spec.expect(d).to spec.eq(%w[X Y Z])
200
+ end
201
+ expect(subject.to_json).to eq('{"AWSTemplateFormatVersion":"2010-09-09","Resources":{"SomeResource":{"DependsOn":["X","Y","Z"]}}}')
202
+ end
203
+
204
+ it 'supports single value DependsOn' do
205
+ subject.Resource('SomeResource') do
206
+ DependsOn(:ADependency)
207
+ end
208
+ expect(subject.to_json).to eq('{"AWSTemplateFormatVersion":"2010-09-09","Resources":{"SomeResource":{"DependsOn":"ADependency"}}}')
209
+ end
210
+
191
211
  context 'built-in functions' do
192
212
  it 'FnGetAtt' do
193
213
  func = subject.FnGetAtt('A', 'B')