cfndsl 1.8.0 → 1.9.5

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.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'version'
4
+ require 'json'
4
5
  require 'fileutils'
5
6
 
6
7
  # Global variables to adjust CfnDsl behavior
@@ -10,8 +11,15 @@ module CfnDsl
10
11
 
11
12
  module_function
12
13
 
13
- AWS_SPECIFICATION_URL = 'https://d201a2mn26r7lk.cloudfront.net/%<version>s/gzip/CloudFormationResourceSpecification.json'
14
14
  LOCAL_SPEC_FILE = File.expand_path('aws/resource_specification.json', __dir__)
15
+ REGION_SPEC_URLS_FILE = File.expand_path('aws/region_spec_urls.json', __dir__)
16
+
17
+ SPEC_PATH_SUFFIX = '%<version>s/gzip/CloudFormationResourceSpecification.json'
18
+ DEFAULT_SPEC_REGION = 'us-east-1'
19
+
20
+ def region_spec_urls
21
+ @region_spec_urls ||= JSON.parse(File.read(REGION_SPEC_URLS_FILE)).tap { |h| h.delete('_source') }.freeze
22
+ end
15
23
 
16
24
  def disable_deep_merge
17
25
  @disable_deep_merge = true
@@ -42,15 +50,35 @@ module CfnDsl
42
50
  File.join(ENV['HOME'], '.cfndsl/resource_specification.json')
43
51
  end
44
52
 
45
- def update_specification_file(file: user_specification_file, version: nil)
53
+ # Build the full specification URL for a given region and version
54
+ #
55
+ # @param region [String] AWS region code
56
+ # @param version [String] specification version or 'latest'
57
+ # @return [String] full URL to the CloudFormation resource specification
58
+ def spec_url_for_region(region, version:)
59
+ base_url = region_spec_urls.fetch(region) do
60
+ raise Error, "Unsupported region '#{region}'. Use CfnDsl.supported_spec_regions for a list of supported regions."
61
+ end
62
+ "#{base_url}/#{format(SPEC_PATH_SUFFIX, version: version)}"
63
+ end
64
+
65
+ # List all supported regions for spec downloads
66
+ #
67
+ # @return [Array<String>] sorted list of supported region codes
68
+ def supported_spec_regions
69
+ region_spec_urls.keys.sort
70
+ end
71
+
72
+ def update_specification_file(file: user_specification_file, version: nil, region: nil)
46
73
  require 'open-uri'
47
74
  version ||= 'latest'
75
+ region ||= DEFAULT_SPEC_REGION
48
76
  FileUtils.mkdir_p File.dirname(file)
49
- url = format(AWS_SPECIFICATION_URL, version: version)
77
+ url = spec_url_for_region(region, version: version)
50
78
  content = URI.parse(url).open.read
51
79
  version = JSON.parse(content)['ResourceSpecificationVersion'] if version == 'latest'
52
80
  File.open(file, 'w') { |f| f.puts content }
53
- { file: file, version: version, url: url }
81
+ { file: file, version: version, url: url, region: region }
54
82
  rescue StandardError
55
83
  raise "Failed updating specification file #{file} from #{url}"
56
84
  end
@@ -79,14 +79,14 @@ 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(file:, name: nil, version: nil)
82
+ def specification(file:, name: nil, version: nil, region: nil)
83
83
  if name
84
84
  desc 'Update Resource Specification' unless ::Rake.application.last_description
85
85
  task name, [:cfn_spec_version] => file
86
86
  end
87
87
 
88
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])
89
+ update_specification(t.name, args.with_defaults(cfn_spec_version: version)[:cfn_spec_version], region: region)
90
90
  end
91
91
  @spec_task.define_singleton_method(:needed?) { true } # We always need to check
92
92
  self
@@ -163,13 +163,13 @@ module CfnDsl
163
163
  tasks.unshift(load_spec)
164
164
  end
165
165
 
166
- def update_specification(file, minimum_version)
167
- if CfnDsl::Specification.update_required?(version: minimum_version, file: file)
166
+ def update_specification(file, minimum_version, region: nil)
167
+ if region || CfnDsl::Specification.update_required?(version: minimum_version, file: file)
168
168
 
169
169
  safe_update_embedded_spec if file == CfnDsl::LOCAL_SPEC_FILE
170
170
 
171
- result = CfnDsl.update_specification_file(file: file, version: minimum_version)
172
- puts "Specification #{result[:file]} updated to version #{result[:version]}"
171
+ result = CfnDsl.update_specification_file(file: file, version: minimum_version, region: region)
172
+ puts "Specification #{result[:file]} updated to version #{result[:version]} (#{result[:region]})"
173
173
  @updated_version = result[:version]
174
174
  elsif minimum_version
175
175
  verbose&.puts "Specification #{file} is already >= #{minimum_version}"
data/lib/cfndsl/runner.rb CHANGED
@@ -64,6 +64,10 @@ module CfnDsl
64
64
  options[:update_spec] = true
65
65
  end
66
66
 
67
+ opts.on('-r', '--region REGION', 'AWS region for specification download (use with -u)') do |region|
68
+ options[:region] = region
69
+ end
70
+
67
71
  opts.on('-g', '--generate RESOURCE_TYPE,RESOURCE_LOGICAL_NAME', 'Add resource type and logical name') do |r|
68
72
  options[:lego] = true
69
73
  options[:resources] = []
@@ -92,8 +96,8 @@ module CfnDsl
92
96
 
93
97
  if options[:update_spec]
94
98
  warn 'Updating specification file'
95
- result = CfnDsl.update_specification_file(version: options[:spec_version])
96
- warn "Specification #{result[:version]} successfully written to #{result[:file]}"
99
+ result = CfnDsl.update_specification_file(version: options[:spec_version], region: options[:region])
100
+ warn "Specification #{result[:version]} (#{result[:region]}) successfully written to #{result[:file]}"
97
101
  end
98
102
 
99
103
  if options[:assetversion]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CfnDsl
4
- VERSION = '1.8.0'
4
+ VERSION = '1.9.5'
5
5
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # rubocop:disable Metrics/AbcSize, Metrics/BlockNesting, Metrics/CyclomaticComplexity, Metrics/MethodLength
4
- # rubocop:disable Metrics/ModuleLength, Metrics/PerceivedComplexity, Style/IfInsideElse, Style/Semicolon
4
+ # rubocop:disable Metrics/ModuleLength, Metrics/PerceivedComplexity, Style/Semicolon
5
5
  #
6
6
  # Totally borrowed from https://github.com/danielsdeleo/deep_merge
7
7
  module DeepMerge
@@ -246,4 +246,4 @@ module DeepMerge
246
246
  end
247
247
  end
248
248
  # rubocop:enable Metrics/AbcSize, Metrics/BlockNesting, Metrics/CyclomaticComplexity, Metrics/MethodLength
249
- # rubocop:enable Metrics/ModuleLength, Metrics/PerceivedComplexity, Style/IfInsideElse, Style/Semicolon
249
+ # rubocop:enable Metrics/ModuleLength, Metrics/PerceivedComplexity, Style/Semicolon
data/spec/cli_spec.rb CHANGED
@@ -17,6 +17,7 @@ describe 'cfndsl', type: :aruba do
17
17
  -s, --specification-file FILE Location of Cloudformation Resource Specification file
18
18
  -u [VERSION], Update the Resource Specification file to latest, or specific version
19
19
  --update-specification
20
+ -r, --region REGION AWS region for specification download (use with -u)
20
21
  -g RESOURCE_TYPE,RESOURCE_LOGICAL_NAME,
21
22
  --generate Add resource type and logical name
22
23
  -a, --assetversion Print out the specification version
@@ -45,7 +46,7 @@ describe 'cfndsl', type: :aruba do
45
46
  run_command "cfndsl -u #{WORKING_SPEC_VERSION}"
46
47
  expect(last_command_started).to have_output_on_stderr(<<-OUTPUT.gsub(/^ {8}/, '').chomp)
47
48
  Updating specification file
48
- Specification #{WORKING_SPEC_VERSION} successfully written to #{ENV['HOME']}/.cfndsl/resource_specification.json
49
+ Specification #{WORKING_SPEC_VERSION} (us-east-1) successfully written to #{ENV['HOME']}/.cfndsl/resource_specification.json
49
50
  OUTPUT
50
51
  expect(last_command_started).to have_exit_status(0)
51
52
  end
@@ -56,7 +57,19 @@ describe 'cfndsl', type: :aruba do
56
57
  run_command 'cfndsl -u'
57
58
 
58
59
  expected = %r{Updating specification file
59
- Specification ([0-9]+\.){2}[0-9]+ successfully written to #{ENV['HOME']}/.cfndsl/resource_specification.json}
60
+ Specification ([0-9]+\.){2}[0-9]+ \(us-east-1\) successfully written to #{ENV['HOME']}/.cfndsl/resource_specification.json}
61
+
62
+ expect(last_command_started).to have_output_on_stderr(expected)
63
+ expect(last_command_started).to have_exit_status(0)
64
+ end
65
+ end
66
+
67
+ context 'cfndsl -u --region us-east-2' do
68
+ it 'updates the specification file from the specified region' do
69
+ run_command 'cfndsl -u --region us-east-2'
70
+
71
+ expected = %r{Updating specification file
72
+ Specification ([0-9]+\.){2}[0-9]+ \(us-east-2\) successfully written to #{ENV['HOME']}/.cfndsl/resource_specification.json}
60
73
 
61
74
  expect(last_command_started).to have_output_on_stderr(expected)
62
75
  expect(last_command_started).to have_exit_status(0)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'net/http'
5
+ require 'uri'
6
+
7
+ describe 'Region specification availability', :network do
8
+ results = {}
9
+
10
+ before(:all) do
11
+ threads = CfnDsl.region_spec_urls.map do |region, base_url|
12
+ Thread.new do
13
+ url = URI.parse("#{base_url}/latest/gzip/CloudFormationResourceSpecification.json")
14
+ http = Net::HTTP.new(url.host, url.port)
15
+ http.use_ssl = (url.scheme == 'https')
16
+ http.open_timeout = 5
17
+ http.read_timeout = 5
18
+
19
+ begin
20
+ response = http.request_head(url.path)
21
+ results[region] = { code: response.code.to_i, url: url.to_s }
22
+ rescue Net::OpenTimeout, Net::ReadTimeout
23
+ results[region] = { code: nil, url: url.to_s, timeout: true }
24
+ end
25
+ end
26
+ end
27
+ threads.each(&:join)
28
+ end
29
+
30
+ CfnDsl.region_spec_urls.each_key do |region|
31
+ it "#{region} has a reachable latest specification" do
32
+ result = results[region]
33
+ skip "Timed out connecting to #{region} (#{result[:url]}) — likely a network route issue" if result[:timeout]
34
+ expect(result[:code]).to eq(200), "Expected 200 for #{region} (#{result[:url]}), got #{result[:code]}"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe CfnDsl do
6
+ describe '.spec_url_for_region' do
7
+ it 'builds the correct URL for a CloudFront-backed region' do
8
+ url = CfnDsl.spec_url_for_region('eu-west-1', version: '255.0.0')
9
+ expect(url).to eq('https://d3teyb21fexa9r.cloudfront.net/255.0.0/gzip/CloudFormationResourceSpecification.json')
10
+ end
11
+
12
+ it 'builds the correct URL for an S3-backed region' do
13
+ url = CfnDsl.spec_url_for_region('af-south-1', version: 'latest')
14
+ expect(url).to eq('https://cfn-resource-specifications-af-south-1-prod.s3.af-south-1.amazonaws.com/latest/gzip/CloudFormationResourceSpecification.json')
15
+ end
16
+
17
+ it 'builds the correct URL for the default region (us-east-1)' do
18
+ url = CfnDsl.spec_url_for_region('us-east-1', version: '7.1.0')
19
+ expect(url).to eq('https://d1uauaxba7bl26.cloudfront.net/7.1.0/gzip/CloudFormationResourceSpecification.json')
20
+ end
21
+
22
+ it 'raises CfnDsl::Error for an unsupported region' do
23
+ expect { CfnDsl.spec_url_for_region('mars-west-1', version: 'latest') }
24
+ .to raise_error(CfnDsl::Error, /Unsupported region 'mars-west-1'/)
25
+ end
26
+ end
27
+
28
+ describe '.supported_spec_regions' do
29
+ it 'returns a sorted array of 35 region codes' do
30
+ regions = CfnDsl.supported_spec_regions
31
+ expect(regions).to be_an(Array)
32
+ expect(regions).to eq(regions.sort)
33
+ expect(regions.size).to eq(35)
34
+ end
35
+
36
+ it 'includes well-known regions' do
37
+ regions = CfnDsl.supported_spec_regions
38
+ expect(regions).to include('us-east-1', 'us-west-2', 'eu-west-1', 'ap-southeast-2')
39
+ end
40
+ end
41
+
42
+ describe '.update_specification_file' do
43
+ let(:tmpdir) { Dir.mktmpdir }
44
+ let(:tmpfile) { File.join(tmpdir, 'spec.json') }
45
+ let(:spec_content) { '{"ResourceSpecificationVersion":"255.0.0","ResourceTypes":{},"PropertyTypes":{}}' }
46
+
47
+ before do
48
+ mock_io = double('io', read: spec_content)
49
+ mock_uri = double('uri', open: mock_io)
50
+ allow(URI).to receive(:parse).and_return(mock_uri)
51
+ end
52
+
53
+ after { FileUtils.rm_rf(tmpdir) }
54
+
55
+ context 'with explicit region' do
56
+ it 'uses the region-specific URL' do
57
+ result = CfnDsl.update_specification_file(file: tmpfile, version: '255.0.0', region: 'eu-west-1')
58
+ expect(result[:url]).to eq('https://d3teyb21fexa9r.cloudfront.net/255.0.0/gzip/CloudFormationResourceSpecification.json')
59
+ expect(result[:region]).to eq('eu-west-1')
60
+ end
61
+
62
+ it 'writes the spec content to the file' do
63
+ CfnDsl.update_specification_file(file: tmpfile, version: '255.0.0', region: 'eu-west-1')
64
+ expect(File.exist?(tmpfile)).to be true
65
+ parsed = JSON.parse(File.read(tmpfile))
66
+ expect(parsed['ResourceSpecificationVersion']).to eq('255.0.0')
67
+ end
68
+
69
+ it 'resolves version from content when version is latest' do
70
+ result = CfnDsl.update_specification_file(file: tmpfile, version: 'latest', region: 'us-east-1')
71
+ expect(result[:version]).to eq('255.0.0')
72
+ end
73
+ end
74
+
75
+ context 'without region (defaults to us-east-1)' do
76
+ it 'uses the us-east-1 URL' do
77
+ result = CfnDsl.update_specification_file(file: tmpfile, version: '255.0.0')
78
+ expect(result[:url]).to eq('https://d1uauaxba7bl26.cloudfront.net/255.0.0/gzip/CloudFormationResourceSpecification.json')
79
+ expect(result[:region]).to eq('us-east-1')
80
+ end
81
+ end
82
+ end
83
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cfndsl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steven Jack
@@ -52,6 +52,7 @@ extensions: []
52
52
  extra_rdoc_files: []
53
53
  files:
54
54
  - ".github/workflows/spec.yml"
55
+ - ".github/workflows/spec_latest.yml"
55
56
  - ".github/workflows/spec_publish.yml"
56
57
  - ".gitignore"
57
58
  - ".rubocop.yml"
@@ -86,6 +87,7 @@ files:
86
87
  - lib/cfndsl/aws/patches/700_SAM_Serverless_Function_InlineCode_patch.json
87
88
  - lib/cfndsl/aws/patches/900_SageMakerTags_patch.json
88
89
  - lib/cfndsl/aws/patches/901_SageMakerTags_patch.json
90
+ - lib/cfndsl/aws/region_spec_urls.json
89
91
  - lib/cfndsl/aws/resource_specification.json
90
92
  - lib/cfndsl/aws/types.rb
91
93
  - lib/cfndsl/aws/types.yaml
@@ -168,6 +170,8 @@ files:
168
170
  - spec/names_spec.rb
169
171
  - spec/output_spec.rb
170
172
  - spec/plurals_spec.rb
173
+ - spec/region_spec_availability_spec.rb
174
+ - spec/region_spec_urls_spec.rb
171
175
  - spec/resource_name_spec.rb
172
176
  - spec/resources_spec.rb
173
177
  - spec/rule_spec.rb
@@ -228,6 +232,8 @@ test_files:
228
232
  - spec/names_spec.rb
229
233
  - spec/output_spec.rb
230
234
  - spec/plurals_spec.rb
235
+ - spec/region_spec_availability_spec.rb
236
+ - spec/region_spec_urls_spec.rb
231
237
  - spec/resource_name_spec.rb
232
238
  - spec/resources_spec.rb
233
239
  - spec/rule_spec.rb