Skip to content

Commit 45214f4

Browse files
authored
Support modeled dualstack and fips variant endpoints; Support S3 Outpost FIPS (#2553)
1 parent 9e16f6d commit 45214f4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1529
-277
lines changed

build_tools/services.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class ServiceEnumerator
1010
MANIFEST_PATH = File.expand_path('../../services.json', __FILE__)
1111

1212
# Minimum `aws-sdk-core` version for new gem builds
13-
MINIMUM_CORE_VERSION = "3.121.2"
13+
MINIMUM_CORE_VERSION = "3.122.0"
1414

1515
EVENTSTREAM_PLUGIN = "Aws::Plugins::EventStreamConfiguration"
1616

gems/aws-partitions/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Unreleased Changes
22
------------------
33

4+
* Feature - Support modeled dualstack and fips endpoints in `Aws::Partitions::EndpointProvider`.
5+
46
1.524.0 (2021-11-03)
57
------------------
68

gems/aws-partitions/lib/aws-partitions/endpoint_provider.rb

Lines changed: 124 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,19 @@ def initialize(rules)
3737
# "monitoring" for cloudwatch.
3838
# @param [String] sts_regional_endpoints [STS only] Whether to use
3939
# `legacy` (global endpoint for legacy regions) or `regional` mode for
40-
# using regional endpoint for supported regions except 'aws-global'
40+
# using regional endpoint for supported regions except 'aws-global'
41+
# @param [Hash] variants Endpoint variants such as 'fips' or 'dualstack'
42+
# @option variants [Boolean] :dualstack When true, resolve a dualstack
43+
# endpoint.
44+
# @option variants [Boolean] :fips When true, resolve a FIPS endpoint.
4145
# @api private Use the static class methods instead.
42-
def resolve(region, service, sts_regional_endpoints)
43-
'https://' + endpoint_for(region, service, sts_regional_endpoints)
46+
def resolve(region, service, sts_regional_endpoints, variants)
47+
'https://' + endpoint_for(region, service, build_is_global_fn(sts_regional_endpoints), variants)
4448
end
4549

4650
# @api private Use the static class methods instead.
4751
def signing_region(region, service, sts_regional_endpoints)
48-
credential_scope(region, service, sts_regional_endpoints)
52+
credential_scope(region, service, build_is_global_fn(sts_regional_endpoints))
4953
.fetch('region', region)
5054
end
5155

@@ -54,47 +58,87 @@ def signing_service(region, service)
5458
# don't default to the service name
5559
# signers should prefer the api metadata's signingName
5660
# if no service is set in the credentialScope
57-
credential_scope(region, service, 'regional')
61+
credential_scope(region, service, build_is_global_fn('regional'))
5862
.fetch('service', nil)
5963
end
6064

65+
# @param [String] region The region used to fetch the partition.
66+
# @param [String] service Used only if dualstack is true. Used to find a
67+
# DNS suffix for a specific service.
68+
# @param [Hash] variants Endpoint variants such as 'fips' or 'dualstack'
69+
# @option variants [Boolean] :dualstack When true, resolve a dualstack
70+
# endpoint.
71+
# @option variants [Boolean] :fips When true, resolve a FIPS endpoint.
6172
# @api private Use the static class methods instead.
62-
def credential_scope(region, service, sts_regional_endpoints)
63-
partition = get_partition(region)
64-
service_cfg = partition.fetch('services', {})
65-
.fetch(service, {})
66-
endpoints = service_cfg.fetch('endpoints', {})
73+
def dns_suffix_for(region, service, variants)
74+
if configured_variants?(variants)
75+
resolve_variant(region, service, variants)['dnsSuffix']
76+
else
77+
get_partition(region)['dnsSuffix']
78+
end
79+
end
80+
81+
private
6782

68-
# Check for sts legacy behavior
69-
sts_legacy = service == 'sts' &&
70-
sts_regional_endpoints == 'legacy' &&
71-
STS_LEGACY_REGIONS.include?(region)
83+
def configured_variants?(variants)
84+
variants.values.any?
85+
end
7286

73-
is_global = !endpoints.key?(region) &&
74-
service_cfg['isRegionalized'] == false
87+
def fetch_variant(cfg, tags)
88+
variants = cfg.fetch('variants', [])
89+
variants.find { |v| tags == Set.new(v['tags']) } || {}
90+
end
7591

76-
# Check for global endpoint.
77-
if sts_legacy || is_global
92+
def resolve_variant(region, service, config_variants)
93+
tags = Set.new(config_variants.select { |_k,v| v == true }.map { |k,_v| k.to_s })
94+
is_global_fn = build_is_global_fn # ignore legacy STS config for variants
95+
96+
partition_cfg = get_partition(region)
97+
service_cfg = partition_cfg.fetch('services', {})
98+
.fetch(service, {})
99+
100+
endpoints_cfg = service_cfg.fetch('endpoints', {})
101+
102+
if is_global_fn.call(service, region, endpoints_cfg, service_cfg)
78103
region = service_cfg.fetch('partitionEndpoint', region)
79104
end
80105

81-
default_credential_scope = service_cfg
82-
.fetch('defaults', {})
83-
.fetch('credentialScope', {})
106+
region_cfg = endpoints_cfg.fetch(region, {})
107+
warn_deprecation(service, region) if region_cfg['deprecated']
84108

85-
endpoints
86-
.fetch(region, {})
87-
.fetch('credentialScope', default_credential_scope)
109+
partition_defaults = fetch_variant(partition_cfg.fetch('defaults', {}), tags)
110+
service_defaults = fetch_variant(service_cfg.fetch('defaults', {}), tags)
111+
endpoint_cfg = fetch_variant(region_cfg, tags)
112+
113+
# merge upwards, preferring values from endpoint > service > partition
114+
partition_defaults.merge(service_defaults.merge(endpoint_cfg))
88115
end
89116

90-
# @api private Use the static class methods instead.
91-
def dns_suffix_for(region)
92-
get_partition(region)['dnsSuffix']
117+
def validate_variant!(config_variants, resolved_variant)
118+
unless resolved_variant['hostname'] && resolved_variant['dnsSuffix']
119+
enabled_variants = config_variants.select { |_k, v| v}.map { |k, _v| k.to_s }.join(', ')
120+
raise ArgumentError,
121+
"#{enabled_variants} not supported for this region and partition."
122+
end
93123
end
94124

95-
private
125+
def endpoint_for(region, service, is_global_fn, variants)
126+
if configured_variants?(variants)
127+
endpoint_with_variants_for(region, service, variants)
128+
else
129+
endpoint_no_variants_for(region, service, is_global_fn)
130+
end
131+
end
132+
133+
def endpoint_with_variants_for(region, service, variants)
134+
variant = resolve_variant(region, service, variants)
135+
validate_variant!(variants, variant)
136+
variant['hostname'].sub('{region}', region)
137+
.sub('{service}', service)
138+
.sub('{dnsSuffix}', variant['dnsSuffix'])
139+
end
96140

97-
def endpoint_for(region, service, sts_regional_endpoints)
141+
def endpoint_no_variants_for(region, service, is_global_fn)
98142
partition = get_partition(region)
99143
service_cfg = partition.fetch('services', {}).fetch(service, {})
100144

@@ -105,30 +149,66 @@ def endpoint_for(region, service, sts_regional_endpoints)
105149

106150
endpoints = service_cfg.fetch('endpoints', {})
107151

108-
# Check for sts legacy behavior
109-
sts_legacy = service == 'sts' &&
110-
sts_regional_endpoints == 'legacy' &&
111-
STS_LEGACY_REGIONS.include?(region)
112-
113-
is_global = !endpoints.key?(region) &&
114-
service_cfg['isRegionalized'] == false
115-
116152
# Check for global endpoint.
117-
if sts_legacy || is_global
153+
if is_global_fn.call(service, region, endpoints, service_cfg)
118154
region = service_cfg.fetch('partitionEndpoint', region)
119155
end
120156

121157
# Check for service/region level endpoint.
122-
endpoint = endpoints
158+
region_cfg = endpoints
123159
.fetch(region, {})
160+
endpoint = region_cfg
124161
.fetch('hostname', default_endpoint)
125162

163+
warn_deprecation(service, region) if region_cfg['deprecated']
164+
126165
# Replace placeholders from the endpoints
127166
endpoint.sub('{region}', region)
128167
.sub('{service}', service)
129168
.sub('{dnsSuffix}', partition['dnsSuffix'])
130169
end
131170

171+
def warn_deprecation(service, region)
172+
warn("The endpoint for service: #{service}, region: #{region}"\
173+
' is deprecated.')
174+
end
175+
176+
# returns a callable that takes a region
177+
# and returns true if the service is global
178+
def build_is_global_fn(sts_regional_endpoints='regional')
179+
lambda do |service, region, endpoints, service_cfg|
180+
# Check for sts legacy behavior
181+
sts_legacy = service == 'sts' &&
182+
sts_regional_endpoints == 'legacy' &&
183+
STS_LEGACY_REGIONS.include?(region)
184+
185+
is_global = !endpoints.key?(region) &&
186+
service_cfg['isRegionalized'] == false
187+
188+
sts_legacy || is_global
189+
end
190+
end
191+
192+
def credential_scope(region, service, is_global_fn)
193+
partition = get_partition(region)
194+
service_cfg = partition.fetch('services', {})
195+
.fetch(service, {})
196+
endpoints = service_cfg.fetch('endpoints', {})
197+
198+
# Check for global endpoint.
199+
if is_global_fn.call(service, region, endpoints, service_cfg)
200+
region = service_cfg.fetch('partitionEndpoint', region)
201+
end
202+
203+
default_credential_scope = service_cfg
204+
.fetch('defaults', {})
205+
.fetch('credentialScope', {})
206+
207+
endpoints
208+
.fetch(region, {})
209+
.fetch('credentialScope', default_credential_scope)
210+
end
211+
132212
def get_partition(region_or_partition)
133213
partition_containing_region(region_or_partition) ||
134214
partition_matching_region(region_or_partition) ||
@@ -144,7 +224,7 @@ def partition_containing_region(region)
144224

145225
def partition_matching_region(region)
146226
@rules['partitions'].find do |p|
147-
region.match(p['regionRegex']) ||
227+
p['regionRegex'] && region.match(p['regionRegex']) ||
148228
p['services'].values.find do |svc|
149229
svc['endpoints'].key?(region) if svc.key?('endpoints')
150230
end
@@ -161,8 +241,8 @@ def default_partition
161241
end
162242

163243
class << self
164-
def resolve(region, service, sts_regional_endpoints = 'regional')
165-
default_provider.resolve(region, service, sts_regional_endpoints)
244+
def resolve(region, service, sts_endpoint = 'regional', variants = {})
245+
default_provider.resolve(region, service, sts_endpoint, variants)
166246
end
167247

168248
def signing_region(region, service, sts_regional_endpoints = 'regional')
@@ -173,8 +253,8 @@ def signing_service(region, service)
173253
default_provider.signing_service(region, service)
174254
end
175255

176-
def dns_suffix_for(region)
177-
default_provider.dns_suffix_for(region)
256+
def dns_suffix_for(region, service = nil, variants = {})
257+
default_provider.dns_suffix_for(region, service, variants)
178258
end
179259

180260
private

gems/aws-partitions/lib/aws-partitions/service.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ def initialize(options = {})
1616
@name = options[:name]
1717
@partition_name = options[:partition_name]
1818
@regions = options[:regions]
19+
@fips_regions = options[:fips_regions]
20+
@dualstack_regions = options[:dualstack_regions]
1921
@regionalized = options[:regionalized]
2022
@partition_region = options[:partition_region]
2123
end
@@ -31,6 +33,14 @@ def initialize(options = {})
3133
# Regions are scoped to the partition.
3234
attr_reader :regions
3335

36+
# @return [Set<String>] The FIPS compatible regions this service is
37+
# available in. Regions are scoped to the partition.
38+
attr_reader :fips_regions
39+
40+
# @return [Set<String>] The Dualstack compatible regions this service is
41+
# available in. Regions are scoped to the partition.
42+
attr_reader :dualstack_regions
43+
3444
# @return [String,nil] The global patition endpoint for this service.
3545
# May be `nil`.
3646
attr_reader :partition_region
@@ -54,6 +64,8 @@ def build(service_name, service, partition)
5464
name: service_name,
5565
partition_name: partition['partition'],
5666
regions: regions(service, partition),
67+
fips_regions: variant_regions('fips', service, partition),
68+
dualstack_regions: variant_regions('dualstack', service, partition),
5769
regionalized: service['isRegionalized'] != false,
5870
partition_region: partition_region(service)
5971
)
@@ -67,6 +79,21 @@ def regions(service, partition)
6779
names - ["#{partition['partition']}-global"]
6880
end
6981

82+
def variant_regions(variant_name, service, partition)
83+
svc_endpoints = service.fetch('endpoints', {})
84+
names = Set.new
85+
svc_endpoints.each do |key, value|
86+
variants = value.fetch('variants', [])
87+
variants.each do |variant|
88+
tags = variant.fetch('tags', [])
89+
if tags.include?(variant_name) && partition['regions'].key?(key)
90+
names << key
91+
end
92+
end
93+
end
94+
names - ["#{partition['partition']}-global"]
95+
end
96+
7097
def partition_region(service)
7198
service['partitionEndpoint']
7299
end

gems/aws-partitions/partitions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17194,4 +17194,4 @@
1719417194
}
1719517195
} ],
1719617196
"version" : 3
17197-
}
17197+
}

0 commit comments

Comments
 (0)