cfndsl 0.15.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +6 -6
  4. data/.travis.yml +10 -5
  5. data/CHANGELOG.md +721 -400
  6. data/Gemfile +2 -0
  7. data/README.md +85 -23
  8. data/Rakefile +28 -6
  9. data/TODO.md +18 -0
  10. data/UPGRADING.md +22 -0
  11. data/cfndsl.gemspec +17 -16
  12. data/exe/cfndsl +5 -0
  13. data/lib/cfndsl.rb +3 -113
  14. data/lib/cfndsl/aws/cloud_formation_template.rb +10 -1
  15. data/lib/cfndsl/aws/patches/000_CloudFormationResourceSpecification.json +51726 -0
  16. data/lib/cfndsl/aws/patches/000_sam.spec.json +1242 -0
  17. data/lib/cfndsl/aws/patches/100_sam.spec_DeploymentPreference_patch.json +64 -0
  18. data/lib/cfndsl/aws/patches/200_Scrutinies_patch.json +86 -0
  19. data/lib/cfndsl/aws/patches/500_Cognito_IdentityPoolRoleAttachment_patches.json +25 -0
  20. data/lib/cfndsl/aws/patches/500_IoT1Click_patch_PlacementTemplate_DeviceTemplates.json +20 -0
  21. data/lib/cfndsl/aws/patches/500_NetworkAclEntry_patch.json +16 -0
  22. data/lib/cfndsl/aws/patches/500_SAM_Serverless_Function_S3Event_Events_patch.json +16 -0
  23. data/lib/cfndsl/aws/patches/500_SAM_Serverless_Function_S3Location_Version_patch.json +16 -0
  24. data/lib/cfndsl/aws/patches/500_SSM_AssociationName_patch.json +16 -0
  25. data/lib/cfndsl/aws/patches/500_VPCEndpoint_patch.json +17 -0
  26. data/lib/cfndsl/aws/patches/510_ElasticSearch_Domain_patches.json +15 -0
  27. data/lib/cfndsl/aws/patches/520_ServiceDiscovery_InstanceAttributes_patch.json +16 -0
  28. data/lib/cfndsl/aws/patches/600_RefKinds_patch.json +3654 -0
  29. data/lib/cfndsl/aws/patches/700_SAM_Serverless_Function_InlineCode_patch.json +20 -0
  30. data/lib/cfndsl/aws/patches/800_List_types_patch.json +115 -0
  31. data/lib/cfndsl/aws/resource_specification.json +39955 -9293
  32. data/lib/cfndsl/aws/types.rb +5 -3
  33. data/lib/cfndsl/cfnlego.rb +34 -0
  34. data/lib/{cfnlego → cfndsl/cfnlego}/cloudformation.erb +0 -0
  35. data/lib/{cfnlego → cfndsl/cfnlego}/cloudformation.rb +3 -1
  36. data/lib/{cfnlego → cfndsl/cfnlego}/resource.rb +5 -8
  37. data/lib/cfndsl/cloudformation.rb +114 -0
  38. data/lib/cfndsl/conditions.rb +13 -1
  39. data/lib/cfndsl/creation_policy.rb +3 -1
  40. data/lib/cfndsl/deep_merge.rb +4 -0
  41. data/lib/cfndsl/external_parameters.rb +12 -13
  42. data/lib/cfndsl/globals.rb +51 -10
  43. data/lib/cfndsl/json_serialisable_object.rb +4 -2
  44. data/lib/cfndsl/jsonable.rb +51 -68
  45. data/lib/cfndsl/mappings.rb +3 -1
  46. data/lib/cfndsl/module.rb +18 -5
  47. data/lib/cfndsl/names.rb +2 -0
  48. data/lib/cfndsl/orchestration_template.rb +193 -73
  49. data/lib/cfndsl/outputs.rb +7 -1
  50. data/lib/cfndsl/parameters.rb +3 -1
  51. data/lib/cfndsl/plurals.rb +23 -10
  52. data/lib/cfndsl/properties.rb +3 -1
  53. data/lib/cfndsl/rake_task.rb +217 -15
  54. data/lib/cfndsl/ref_check.rb +21 -11
  55. data/lib/cfndsl/resources.rb +8 -19
  56. data/lib/cfndsl/rules.rb +46 -0
  57. data/lib/cfndsl/runner.rb +143 -0
  58. data/lib/cfndsl/specification.rb +82 -84
  59. data/lib/cfndsl/types.rb +212 -95
  60. data/lib/cfndsl/update_policy.rb +3 -1
  61. data/lib/cfndsl/version.rb +3 -1
  62. data/lib/deep_merge/core.rb +10 -6
  63. data/lib/deep_merge/deep_merge.rb +3 -1
  64. data/sample/autoscale.rb +1 -1
  65. data/sample/autoscale2.rb +4 -3
  66. data/sample/circular.rb +2 -0
  67. data/sample/codedeploy.rb +3 -1
  68. data/sample/config_service.rb +5 -3
  69. data/sample/ecs.rb +3 -1
  70. data/sample/export.rb +5 -3
  71. data/sample/iam_policies.rb +2 -0
  72. data/sample/import.rb +4 -2
  73. data/sample/lambda.rb +3 -1
  74. data/sample/s3.rb +2 -0
  75. data/sample/t1.rb +3 -1
  76. data/sample/vpc_example.rb +3 -1
  77. data/sample/vpc_with_vpn_example.rb +3 -1
  78. data/spec/aws/ec2_security_group_spec.rb +2 -0
  79. data/spec/aws/ecs_task_definition_spec.rb +2 -0
  80. data/spec/aws/iam_managed_policy_spec.rb +2 -0
  81. data/spec/aws/kms_alias_spec.rb +2 -0
  82. data/spec/aws/list_type_patches_spec.rb +35 -0
  83. data/spec/aws/logs_log_group_spec.rb +2 -0
  84. data/spec/aws/nested_arrays_spec.rb +194 -0
  85. data/spec/aws/rds_db_instance_spec.rb +2 -0
  86. data/spec/aws/serverless_spec.rb +2 -2
  87. data/spec/cfndsl_spec.rb +102 -63
  88. data/spec/cli_spec.rb +52 -49
  89. data/spec/cloud_formation_template_spec.rb +235 -0
  90. data/spec/condition_spec.rb +24 -0
  91. data/spec/deep_merge_spec.rb +2 -0
  92. data/spec/direct_ruby_spec.rb +19 -0
  93. data/spec/external_parameters_spec.rb +25 -20
  94. data/spec/fixtures/condition-assertion.json +1 -0
  95. data/spec/fixtures/params.json +1 -0
  96. data/spec/fixtures/params.yaml +2 -0
  97. data/spec/fixtures/params_struct1.yaml +4 -0
  98. data/spec/fixtures/params_struct2.yaml +4 -0
  99. data/spec/fixtures/rule-assertion.json +1 -0
  100. data/spec/fixtures/test.rb +12 -4
  101. data/spec/generate_spec.rb +4 -0
  102. data/spec/jsonable_spec.rb +2 -0
  103. data/spec/metadata_spec.rb +2 -0
  104. data/spec/names_spec.rb +2 -0
  105. data/spec/output_spec.rb +2 -0
  106. data/spec/plurals_spec.rb +2 -0
  107. data/spec/resource_name_spec.rb +21 -0
  108. data/spec/resources_spec.rb +2 -7
  109. data/spec/rule_spec.rb +17 -0
  110. data/spec/spec_helper.rb +4 -7
  111. data/spec/support/shared_examples/orchestration_template.rb +17 -2
  112. data/spec/transform_spec.rb +2 -0
  113. data/spec/types_definition_spec.rb +6 -7
  114. metadata +79 -25
  115. data/bin/cfndsl +0 -143
  116. data/lib/cfndsl/errors.rb +0 -29
  117. data/lib/cfndsl/os/heat_template.rb +0 -16
  118. data/lib/cfndsl/os/types.rb +0 -12
  119. data/lib/cfndsl/os/types.yaml +0 -2423
  120. data/lib/cfndsl/patches.rb +0 -98
  121. data/lib/cfnlego.rb +0 -42
  122. data/spec/fixtures/heattest.rb +0 -22
  123. data/spec/heat_template_spec.rb +0 -5
@@ -1,4 +1,6 @@
1
- require 'cfndsl/jsonable'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'jsonable'
2
4
 
3
5
  module CfnDsl
4
6
  # Handles property objects for Resources
@@ -1,36 +1,240 @@
1
- require 'rake'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'rake/tasklib'
4
+ require_relative 'globals'
5
+ require_relative 'specification'
3
6
 
4
- require 'cfndsl'
7
+ # Monkey patch Rake
8
+ module Rake
9
+ # Default verbosity to false
10
+ def self.verbose?
11
+ verbose && verbose != FileUtilsExt::DEFAULT
12
+ end
13
+ end
5
14
 
6
15
  module CfnDsl
7
- # Rake Task
16
+ # Rake TaskLib
17
+ # rubocop:disable Metrics/ClassLength
8
18
  class RakeTask < Rake::TaskLib
19
+ # @deprecated pre 1.x rake task generator
9
20
  attr_accessor :cfndsl_opts
10
21
 
22
+ # Creates Cloudformation generation tasks
23
+ #
24
+ # @example
25
+ # directory 'tmp'
26
+ #
27
+ # namespace :samples do
28
+ #
29
+ # CfnDsl::RakeTask.new do |t|
30
+ # t.specification(file: 'tmp/cloudformation_resources.json')
31
+ #
32
+ # desc 'Generate CloudFormation Json'
33
+ # t.json(name: :json, files: ["sample/*.rb"], pathmap: 'tmp/%f.json', pretty: true, extras: FileList.new('sample/*.yaml') )
34
+ #
35
+ # t.yaml(name: :yaml, files: 'sample/t1.rb', pathmap: 'tmp/%f.yaml', extras: '%X.yaml')
36
+ # end
37
+ # end
11
38
  def initialize(name = nil)
39
+ @tasks = []
40
+ @spec_task = nil
41
+
42
+ last_desc = ::Rake.application.last_description
43
+ desc nil
44
+
12
45
  yield self if block_given?
13
46
 
14
- desc 'Generate Cloudformation' unless ::Rake.application.last_description
15
- task(name || :generate) do |_t, _args|
16
- cfndsl_opts[:files].each do |opts|
17
- generate(opts)
18
- end
47
+ if cfndsl_opts
48
+ desc last_desc if last_desc
49
+ task(name || :generate) { |_t, _args| cfndsl_opts[:files].each { |opts| generate(opts) } }
50
+ else
51
+ define_base_tasks(name || :generate, last_desc)
52
+ end
53
+ end
54
+
55
+ # Use a custom specification file
56
+ #
57
+ # This specification file will be used for any generation tasks
58
+ #
59
+ # Creates a file task to download from upstream source, with an optional additional named task that depends on it.
60
+ #
61
+ # The minimum required version can be specified with the version parameter, which can be overriden by invoking
62
+ # rake with the 'cfn_spec_task' argument
63
+ #
64
+ # @example
65
+ # CfnDsl::RakeTask.new() do |t|
66
+ # t.specification(name: :update_spec, file: 'tmp/cloudformation_resources.json', version: '6.3'0)
67
+ # end
68
+ #
69
+ # # rake update_spec
70
+ # # >> Will ensure 'tmp/cloudformation_resources.json' exists and is at least 6.3.0
71
+ # # rake update_spec[latest]
72
+ # # >> Will always try to download the latest available spec
73
+ #
74
+ #
75
+ # @param [String] file the number of the specification file to use
76
+ # @param [Symbol] name A pretty name for the task to update this file from upstream source
77
+ # @param [String] version The minimum version required, and the default version used when downloading
78
+ # if not specified and not overriden by 'cfn_spec_version' rake task argument
79
+ # then any existing file is considered sufficient, and 'latest' is the version used for downloading
80
+ #
81
+ # @todo Add capability to provide a user spec/patches dir
82
+ def specification(name: nil, file:, version: nil)
83
+ if name
84
+ desc 'Update Resource Specification' unless ::Rake.application.last_description
85
+ task name, [:cfn_spec_version] => file
86
+ end
87
+
88
+ @spec_task = file(file, :cfn_spec_version) do |t, args|
89
+ update_specification(t.name, args.with_defaults(cfn_spec_version: version)[:cfn_spec_version])
90
+ end
91
+ @spec_task.define_singleton_method(:needed?) { true } # We always need to check
92
+ self
93
+ end
94
+
95
+ # Convert DSL sources to json
96
+ #
97
+ # Generates file tasks for each of the matching file in the files FileList. Each task is dependant on the source,
98
+ # the specification file, and the required extras files such that if the timestamp of any of these is earlier
99
+ # the target file will be regenerated.
100
+ #
101
+ # Finally task <name> is generated that depends on all the generated targets.
102
+ #
103
+ # @param [Symbol|String] name the name of a task that is dependant on all the files being converted
104
+ # @param [Rake::FileList | Array<String> ] files source file list
105
+ # @param [String] pathmap expression to map source files to target files
106
+ # @param [Rake::Filelist|Array<String>] extras a list of files to load as external parameters
107
+ # Note String values containing '%' are treated as a pathmap from the source
108
+ # The resulting generated FileList is used as a dependency for generation so any entries not
109
+ # containing '*' MUST exist.
110
+ # @param [Boolean] pretty use JSON.pretty_generate
111
+ # @param [Hash] json_opts, other options to pass to JSON generator
112
+ # rubocop:disable Metrics/ParameterLists
113
+ def json(name:, files:, pathmap:, pretty: false, extras: [], **json_opts)
114
+ json_method = pretty ? :pretty_generate : :generate
115
+ generate_model_tasks(name: name, files: files, pathmap: pathmap, extras: extras) do |model, f|
116
+ f.write(JSON.send(json_method, model, **json_opts))
117
+ end
118
+ self
119
+ end
120
+
121
+ # Convert DSL sources to yaml
122
+ #
123
+ # Generates file tasks for each of the matching file in the files FileList. Each task is dependant on the source,
124
+ # the specification file, and the required extras files such that if the timestamp of any of these is earlier
125
+ # the target file will be regenerated.
126
+ #
127
+ # Finally task <name> is generated that depends on all the generated targets.
128
+ # @param [Symbol|String] name the name of a task that is dependant on all the files being converted
129
+ # @param [Rake::FileList | Array<String> ] files source file list
130
+ # @param [String] pathmap expression to map source files to target files
131
+ # @param [Rake::Filelist|Array<String>] extras a list of files to load as external parameters
132
+ # Note String values containing '%' are treated as a pathmap from the source
133
+ # The resulting generated FileList is used as a dependency for generation so any entries not
134
+ # containing '*' MUST exist.
135
+ # @param [Hash] yaml_opts, other options to pass to YAML generator
136
+ def yaml(name:, files:, pathmap:, extras: [], **yaml_opts)
137
+ generate_model_tasks(name: name, files: files, pathmap: pathmap, extras: extras) do |model, f|
138
+ YAML.dump(model, f, **yaml_opts)
19
139
  end
140
+ self
20
141
  end
142
+ # rubocop:enable Metrics/ParameterLists
21
143
 
22
144
  private
23
145
 
146
+ attr_reader :tasks, :spec_task
147
+
148
+ def define_base_tasks(name, description)
149
+ return if tasks.empty?
150
+
151
+ desc description || 'Generate Cloudformation'
152
+ task(name || :generate, [:cfn_spec_version] => tasks)
153
+
154
+ load_spec = task load_spec_types: [spec_task].compact do |_t, args|
155
+ CfnDsl.specification_file = spec_task&.name || CfnDsl::LOCAL_SPEC_FILE
156
+ require_relative 'cloudformation'
157
+ CfnDsl::CloudFormationTemplate.check_types(file: spec_task&.name, version: @updated_version || args[:cfn_spec_version])
158
+ verbose&.puts "Using Cloudformation types generated from #{CfnDsl::CloudFormationTemplate.template_types.values_at('File', 'Version').join(' ')}"
159
+ end
160
+ load_spec.define_singleton_method(:timestamp) { Rake::EARLY } # Prevent this task from causing generation tasks.
161
+
162
+ tasks.unshift(load_spec)
163
+ end
164
+
165
+ def update_specification(file, minimum_version)
166
+ if CfnDsl::Specification.update_required?(version: minimum_version, file: file)
167
+
168
+ safe_update_embedded_spec if file == CfnDsl::LOCAL_SPEC_FILE
169
+
170
+ result = CfnDsl.update_specification_file(file: file, version: minimum_version)
171
+ puts "Specification #{result[:file]} updated to version #{result[:version]}"
172
+ @updated_version = result[:version]
173
+ elsif minimum_version
174
+ verbose&.puts "Specification #{file} is already >= #{minimum_version}"
175
+ end
176
+ end
177
+
178
+ def safe_update_embedded_spec
179
+ return if File.fnmatch?(File.join(File.dirname(File.expand_path(Rake.application.rakefile)), '*'), CfnDsl::LOCAL_SPEC_FILE)
180
+
181
+ raise Error, 'Refusing to update CloudFormation spec embedded in cfndsl gem'
182
+ end
183
+
184
+ def generate_model_tasks(name:, files:, pathmap:, extras:)
185
+ files = Rake::FileList.new(*files) unless files.is_a?(Rake::FileList)
186
+
187
+ tasks << task(name, [:cfn_spec_version] => files.pathmap(pathmap))
188
+
189
+ files.each do |source|
190
+ matched_extras = build_extras_filelist(source, extras)
191
+
192
+ file source.pathmap(pathmap) => [source, :load_spec_types, matched_extras] do |task|
193
+ eval_extras = matched_extras.map { |e| [:yaml, e] } # eval treats yaml and json same
194
+ puts "Generating Cloudformation for #{source} to #{task.name}"
195
+ model = CfnDsl.eval_file_with_extras(source, eval_extras, verbose)
196
+ File.open(task.name, 'w') { |f| yield model, f }
197
+ end
198
+ end
199
+
200
+ self
201
+ end
202
+
203
+ def build_extras_filelist(source, extras)
204
+ extras = [extras] unless extras.respond_to?(:each)
205
+ extras.each_with_object(Rake::FileList.new) do |extra, result|
206
+ case extra
207
+ when Rake::FileList
208
+ result.add(extra)
209
+ when Array
210
+ result.add(*extra)
211
+ when /%/
212
+ result.add(source.pathmap(extra))
213
+ else
214
+ result.add(extra)
215
+ end
216
+ end
217
+ end
218
+
219
+ def verbose
220
+ (Rake.verbose? || cfndsl_opts&.fetch(:verbose, false)) && STDERR
221
+ end
222
+
24
223
  def generate(opts)
25
224
  log(opts)
26
225
  outputter(opts) do |output|
27
- output.puts cfndsl_opts[:pretty] ? JSON.pretty_generate(model(opts[:filename])) : model(opts[:filename]).to_json
226
+ if cfndsl_opts[:outformat] == 'yaml'
227
+ data = model(opts[:filename]).to_json
228
+ output.puts JSON.parse(data).to_yaml
229
+ else
230
+ output.puts cfndsl_opts[:pretty] ? JSON.pretty_generate(model(opts[:filename])) : model(opts[:filename]).to_json
231
+ end
28
232
  end
29
233
  end
30
234
 
31
235
  def log(opts)
32
236
  type = opts[:output].nil? ? 'STDOUT' : opts[:output]
33
- verbose.puts("Writing to #{type}") if verbose
237
+ verbose&.puts("Writing to #{type}")
34
238
  end
35
239
 
36
240
  def outputter(opts)
@@ -39,7 +243,8 @@ module CfnDsl
39
243
 
40
244
  def model(filename)
41
245
  raise "#{filename} doesn't exist" unless File.exist?(filename)
42
- verbose.puts("using extras #{extra}") if verbose
246
+
247
+ verbose&.puts("using extras #{extra}")
43
248
  CfnDsl.eval_file_with_extras(filename, extra, verbose)
44
249
  end
45
250
 
@@ -47,12 +252,9 @@ module CfnDsl
47
252
  cfndsl_opts.fetch(:extras, [])
48
253
  end
49
254
 
50
- def verbose
51
- cfndsl_opts[:verbose] && STDERR
52
- end
53
-
54
255
  def file_output(path)
55
256
  File.open(File.expand_path(path), 'w') { |f| yield f }
56
257
  end
57
258
  end
259
+ # rubocop:enable Metrics/ClassLength
58
260
  end
@@ -1,26 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This module defines some methods for walking the reference tree
2
4
  # of various objects.
3
5
  module RefCheck
4
- # Build up a set of references.
5
- def build_references(refs)
6
- raise 'Circular reference' if @_visited
6
+ class SelfReference < StandardError
7
+ end
7
8
 
8
- @_visited = true
9
+ class NullReference < StandardError
10
+ end
9
11
 
10
- if respond_to?(:all_refs)
11
- all_refs.each do |ref|
12
- refs[ref.to_s] = 1
12
+ # Build up a set of references.
13
+ # rubocop:disable Metrics/PerceivedComplexity
14
+ def build_references(refs = [], origin = nil, method = :all_refs)
15
+ if respond_to?(method)
16
+ send(method).each do |ref|
17
+ raise SelfReference, "#{origin} references itself at #{to_json}" if origin && ref.to_s == origin
18
+ raise NullReference, "#{origin} contains null value reference at #{to_json}" if origin && ref.nil?
19
+
20
+ refs << ref
13
21
  end
14
22
  end
15
23
 
16
24
  ref_children.each do |elem|
17
- elem.build_references(refs) if elem.respond_to?(:build_references)
18
- end
25
+ # Nulls are not permitted in Cloudformation templates.
26
+ raise NullReference, "#{origin} contains null value reference at #{to_json}" if origin && elem.nil?
19
27
 
20
- @_visited = nil
28
+ elem.build_references(refs, origin, method) if elem.respond_to?(:build_references)
29
+ end
21
30
 
22
31
  refs
23
32
  end
33
+ # rubocop:enable Metrics/PerceivedComplexity
24
34
 
25
35
  def ref_children
26
36
  []
@@ -35,7 +45,7 @@ class Array
35
45
  end
36
46
  end
37
47
 
38
- # Mixin to Array
48
+ # Mixin to Hash
39
49
  class Hash
40
50
  include RefCheck
41
51
  def ref_children
@@ -1,6 +1,6 @@
1
- require 'cfndsl/jsonable'
2
- require 'cfndsl/properties'
3
- require 'cfndsl/update_policy'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'jsonable'
4
4
 
5
5
  module CfnDsl
6
6
  # Handles Resource objects
@@ -8,10 +8,6 @@ module CfnDsl
8
8
  dsl_attr_setter :Type, :DependsOn, :DeletionPolicy, :Condition, :Metadata
9
9
  dsl_content_object :Property, :UpdatePolicy, :CreationPolicy
10
10
 
11
- def addTag(name, value, propagate = nil)
12
- add_tag(name, value, propagate)
13
- end
14
-
15
11
  def add_tag(name, value, propagate = nil)
16
12
  send(:Tag) do
17
13
  Key name
@@ -20,19 +16,12 @@ module CfnDsl
20
16
  end
21
17
  end
22
18
 
23
- def all_refs
24
- refs = []
25
- if @DependsOn
26
- if @DependsOn.respond_to?(:each)
27
- @DependsOn.each do |dep|
28
- refs.push dep
29
- end
30
- end
31
-
32
- refs.push @DependsOn if @DependsOn.instance_of?(String)
33
- end
19
+ def condition_refs
20
+ [@Condition].flatten.compact.map(&:to_s)
21
+ end
34
22
 
35
- refs
23
+ def all_refs
24
+ [@DependsOn].flatten.compact.map(&:to_s)
36
25
  end
37
26
  end
38
27
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'jsonable'
4
+
5
+ module CfnDsl
6
+ # Handles Rule objects
7
+ class RuleDefinition < JSONable
8
+ dsl_attr_setter :RuleCondition, :Assertions
9
+
10
+ def initialize
11
+ @Assertions = []
12
+ end
13
+
14
+ def Assert(desc, struct)
15
+ @Assertions.push('Assert' => struct, 'AssertDescription' => desc)
16
+ end
17
+
18
+ def FnContains(list_of_strings, string)
19
+ Fn.new('Contains', [list_of_strings, string])
20
+ end
21
+
22
+ def FnEachMemberEquals(list_of_strings, string)
23
+ Fn.new('EachMemberEquals', [list_of_strings, string])
24
+ end
25
+
26
+ def FnEachMemberIn(strings_to_check, strings_to_match)
27
+ Fn.new('EachMemberIn', [strings_to_check, strings_to_match])
28
+ end
29
+
30
+ def FnRefAll(parameter_type)
31
+ Fn.new('RefAll', parameter_type)
32
+ end
33
+
34
+ def FnValueOf(parameter_logical_id, attribute)
35
+ raise 'Cannot use functions within FnValueOf' unless parameter_logical_id.is_a?(String) && attribute.is_a?(String)
36
+
37
+ Fn.new('ValueOf', [parameter_logical_id, attribute])
38
+ end
39
+
40
+ def FnValueOfAll(parameter_logical_id, attribute)
41
+ raise 'Cannot use functions within FnValueOfAll' unless parameter_logical_id.is_a?(String) && attribute.is_a?(String)
42
+
43
+ Fn.new('ValueOfAll', [parameter_logical_id, attribute])
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'json'
5
+ require_relative 'globals'
6
+ # Defer require of other capabilities (particularly loading dynamic Types) until required
7
+
8
+ module CfnDsl
9
+ # Runner class to handle commandline invocation
10
+ class Runner
11
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
12
+ def self.invoke!
13
+ options = {}
14
+
15
+ optparse = OptionParser.new do |opts|
16
+ opts.version = CfnDsl::VERSION
17
+ opts.banner = 'Usage: cfndsl [options] FILE'
18
+
19
+ # Define the options, and what they do
20
+ options[:output] = '-'
21
+ opts.on('-o', '--output FILE', 'Write output to file') do |file|
22
+ options[:output] = file
23
+ end
24
+
25
+ options[:extras] = []
26
+ opts.on('-y', '--yaml FILE', 'Import yaml file as local variables') do |file|
27
+ options[:extras].push([:yaml, File.expand_path(file)])
28
+ end
29
+
30
+ opts.on('-j', '--json FILE', 'Import json file as local variables') do |file|
31
+ options[:extras].push([:json, File.expand_path(file)])
32
+ end
33
+
34
+ opts.on('-p', '--pretty', 'Pretty-format output JSON') do
35
+ options[:pretty] = true
36
+ end
37
+
38
+ opts.on('-f', '--format FORMAT', 'Specify the output format (JSON default)') do |format|
39
+ raise "Format #{format} not supported" unless %w[json yaml].include? format
40
+
41
+ options[:outformat] = format
42
+ end
43
+
44
+ opts.on('-D', '--define "VARIABLE=VALUE"', 'Directly set local VARIABLE as VALUE') do |file|
45
+ options[:extras].push([:raw, file])
46
+ end
47
+
48
+ options[:verbose] = false
49
+ opts.on('-v', '--verbose', 'Turn on verbose ouptut') do
50
+ options[:verbose] = true
51
+ end
52
+
53
+ opts.on('-m', '--disable-deep-merge', 'Disable deep merging of yaml') do
54
+ CfnDsl.disable_deep_merge
55
+ end
56
+
57
+ # TODO: Support options to add a spec/patches dir
58
+ opts.on('-s', '--specification-file FILE', 'Location of Cloudformation Resource Specification file') do |file|
59
+ CfnDsl.specification_file File.expand_path(file)
60
+ end
61
+
62
+ opts.on('-u', '--update-specification [VERSION]', 'Update the Resource Specification file to latest, or specific version') do |file|
63
+ options[:spec_version] = file || 'latest'
64
+ options[:update_spec] = true
65
+ end
66
+
67
+ opts.on('-g', '--generate RESOURCE_TYPE,RESOURCE_LOGICAL_NAME', 'Add resource type and logical name') do |r|
68
+ options[:lego] = true
69
+ options[:resources] << r
70
+ end
71
+
72
+ opts.on('-a', '--assetversion', 'Print out the specification version') do
73
+ options[:assetversion] = true
74
+ end
75
+
76
+ opts.on('-l', '--list', 'List supported resources') do
77
+ require_relative 'cfnlego'
78
+ puts Cfnlego.Resources.sort
79
+ exit
80
+ end
81
+
82
+ # This displays the help screen, all programs are
83
+ # assumed to have this option.
84
+ opts.on('-h', '--help', 'Display this screen') do
85
+ puts opts
86
+ exit
87
+ end
88
+ end
89
+
90
+ optparse.parse!
91
+
92
+ if options[:update_spec]
93
+ warn 'Updating specification file'
94
+ result = CfnDsl.update_specification_file(version: options[:spec_version])
95
+ warn "Specification #{result[:version]} successfully written to #{result[:file]}"
96
+ end
97
+
98
+ if options[:assetversion]
99
+ spec_file = JSON.parse File.read(CfnDsl.specification_file)
100
+ warn spec_file['ResourceSpecificationVersion']
101
+ end
102
+
103
+ if options[:lego]
104
+ require 'cfnlego'
105
+ puts Cfnlego.run(options)
106
+ exit
107
+ end
108
+
109
+ if ARGV.empty?
110
+ if options[:update_spec] || options[:assetversion]
111
+ exit 0
112
+ else
113
+ puts optparse.help
114
+ exit 1
115
+ end
116
+ end
117
+
118
+ filename = File.expand_path(ARGV[0])
119
+ verbose = options[:verbose] && STDERR
120
+
121
+ verbose.puts "Using specification file #{CfnDsl.specification_file}" if verbose
122
+
123
+ require_relative 'cloudformation'
124
+ model = CfnDsl.eval_file_with_extras(filename, options[:extras], verbose)
125
+
126
+ output = STDOUT
127
+ if options[:output] != '-'
128
+ verbose.puts("Writing to #{options[:output]}") if verbose
129
+ output = File.open(File.expand_path(options[:output]), 'w')
130
+ elsif verbose
131
+ verbose.puts('Writing to STDOUT')
132
+ end
133
+
134
+ if options[:outformat] == 'yaml'
135
+ data = model.to_json
136
+ output.puts JSON.parse(data).to_yaml
137
+ else
138
+ output.puts options[:pretty] ? JSON.pretty_generate(model) : model.to_json
139
+ end
140
+ end
141
+ end
142
+ end
143
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity