2015-10-08 20:15:31 +02:00
|
|
|
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
|
|
|
# may not use this file except in compliance with the License. A copy of
|
|
|
|
# the License is located at
|
|
|
|
#
|
|
|
|
# http://aws.amazon.com/apache2.0/
|
|
|
|
#
|
|
|
|
# or in the "license" file accompanying this file. This file is
|
|
|
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
|
|
|
# ANY KIND, either express or implied. See the License for the specific
|
|
|
|
# language governing permissions and limitations under the License.
|
2016-10-11 02:22:54 +02:00
|
|
|
"""Resolves regions and endpoints.
|
2015-10-08 20:15:31 +02:00
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
This module implements endpoint resolution, including resolving endpoints for a
|
|
|
|
given service and region and resolving the available endpoints for a service
|
|
|
|
in a specific AWS partition.
|
2015-10-08 20:15:31 +02:00
|
|
|
"""
|
2016-10-11 02:22:54 +02:00
|
|
|
import logging
|
|
|
|
import re
|
|
|
|
|
2021-11-03 18:14:15 +01:00
|
|
|
from botocore.exceptions import (
|
2022-01-12 23:38:07 +01:00
|
|
|
NoRegionError, UnknownRegionError, EndpointVariantError
|
2021-11-03 18:14:15 +01:00
|
|
|
)
|
2015-10-08 20:15:31 +02:00
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
LOG = logging.getLogger(__name__)
|
2022-01-12 23:38:07 +01:00
|
|
|
DEFAULT_URI_TEMPLATE = '{service}.{region}.{dnsSuffix}' # noqa
|
2016-10-11 02:22:54 +02:00
|
|
|
DEFAULT_SERVICE_DATA = {'endpoints': {}}
|
2015-10-08 20:15:31 +02:00
|
|
|
|
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
class BaseEndpointResolver(object):
|
|
|
|
"""Resolves regions and endpoints. Must be subclassed."""
|
|
|
|
def construct_endpoint(self, service_name, region_name=None):
|
|
|
|
"""Resolves an endpoint for a service and region combination.
|
2015-10-08 20:15:31 +02:00
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
:type service_name: string
|
|
|
|
:param service_name: Name of the service to resolve an endpoint for
|
|
|
|
(e.g., s3)
|
2015-10-08 20:15:31 +02:00
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
:type region_name: string
|
|
|
|
:param region_name: Region/endpoint name to resolve (e.g., us-east-1)
|
|
|
|
if no region is provided, the first found partition-wide endpoint
|
|
|
|
will be used if available.
|
2015-10-08 20:15:31 +02:00
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
:rtype: dict
|
|
|
|
:return: Returns a dict containing the following keys:
|
|
|
|
- partition: (string, required) Resolved partition name
|
|
|
|
- endpointName: (string, required) Resolved endpoint name
|
|
|
|
- hostname: (string, required) Hostname to use for this endpoint
|
|
|
|
- sslCommonName: (string) sslCommonName to use for this endpoint.
|
|
|
|
- credentialScope: (dict) Signature version 4 credential scope
|
|
|
|
- region: (string) region name override when signing.
|
|
|
|
- service: (string) service name override when signing.
|
|
|
|
- signatureVersions: (list<string>) A list of possible signature
|
|
|
|
versions, including s3, v4, v2, and s3v4
|
|
|
|
- protocols: (list<string>) A list of supported protocols
|
|
|
|
(e.g., http, https)
|
|
|
|
- ...: Other keys may be included as well based on the metadata
|
2015-10-08 20:15:31 +02:00
|
|
|
"""
|
2016-10-11 02:22:54 +02:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def get_available_partitions(self):
|
|
|
|
"""Lists the partitions available to the endpoint resolver.
|
|
|
|
|
|
|
|
:return: Returns a list of partition names (e.g., ["aws", "aws-cn"]).
|
|
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def get_available_endpoints(self, service_name, partition_name='aws',
|
|
|
|
allow_non_regional=False):
|
|
|
|
"""Lists the endpoint names of a particular partition.
|
|
|
|
|
|
|
|
:type service_name: string
|
|
|
|
:param service_name: Name of a service to list endpoint for (e.g., s3)
|
|
|
|
|
|
|
|
:type partition_name: string
|
|
|
|
:param partition_name: Name of the partition to limit endpoints to.
|
|
|
|
(e.g., aws for the public AWS endpoints, aws-cn for AWS China
|
|
|
|
endpoints, aws-us-gov for AWS GovCloud (US) Endpoints, etc.
|
|
|
|
|
|
|
|
:type allow_non_regional: bool
|
|
|
|
:param allow_non_regional: Set to True to include endpoints that are
|
|
|
|
not regional endpoints (e.g., s3-external-1,
|
|
|
|
fips-us-gov-west-1, etc).
|
|
|
|
:return: Returns a list of endpoint names (e.g., ["us-east-1"]).
|
|
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
|
|
class EndpointResolver(BaseEndpointResolver):
|
|
|
|
"""Resolves endpoints based on partition endpoint metadata"""
|
2022-01-12 23:38:07 +01:00
|
|
|
|
|
|
|
_UNSUPPORTED_DUALSTACK_PARTITIONS = ['aws-iso', 'aws-iso-b']
|
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
def __init__(self, endpoint_data):
|
|
|
|
"""
|
|
|
|
:param endpoint_data: A dict of partition data.
|
|
|
|
"""
|
|
|
|
if 'partitions' not in endpoint_data:
|
|
|
|
raise ValueError('Missing "partitions" in endpoint data')
|
|
|
|
self._endpoint_data = endpoint_data
|
|
|
|
|
2022-01-12 23:38:07 +01:00
|
|
|
def get_service_endpoints_data(self, service_name, partition_name='aws'):
|
|
|
|
for partition in self._endpoint_data['partitions']:
|
|
|
|
if partition['partition'] != partition_name:
|
|
|
|
continue
|
|
|
|
services = partition['services']
|
|
|
|
if service_name not in services:
|
|
|
|
continue
|
|
|
|
return services[service_name]['endpoints']
|
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
def get_available_partitions(self):
|
|
|
|
result = []
|
|
|
|
for partition in self._endpoint_data['partitions']:
|
|
|
|
result.append(partition['partition'])
|
|
|
|
return result
|
|
|
|
|
|
|
|
def get_available_endpoints(self, service_name, partition_name='aws',
|
2022-01-12 23:38:07 +01:00
|
|
|
allow_non_regional=False,
|
|
|
|
endpoint_variant_tags=None):
|
2016-10-11 02:22:54 +02:00
|
|
|
result = []
|
|
|
|
for partition in self._endpoint_data['partitions']:
|
|
|
|
if partition['partition'] != partition_name:
|
|
|
|
continue
|
|
|
|
services = partition['services']
|
|
|
|
if service_name not in services:
|
|
|
|
continue
|
2022-01-12 23:38:07 +01:00
|
|
|
service_endpoints = services[service_name]['endpoints']
|
|
|
|
for endpoint_name in service_endpoints:
|
|
|
|
is_regional_endpoint = endpoint_name in partition['regions']
|
|
|
|
# Only regional endpoints can be modeled with variants
|
|
|
|
if endpoint_variant_tags and is_regional_endpoint:
|
|
|
|
variant_data = self._retrieve_variant_data(
|
|
|
|
service_endpoints[endpoint_name],
|
|
|
|
endpoint_variant_tags)
|
|
|
|
if variant_data:
|
|
|
|
result.append(endpoint_name)
|
|
|
|
elif allow_non_regional or is_regional_endpoint:
|
2016-10-11 02:22:54 +02:00
|
|
|
result.append(endpoint_name)
|
|
|
|
return result
|
|
|
|
|
2022-01-12 23:38:07 +01:00
|
|
|
def get_partition_dns_suffix(self, partition_name,
|
|
|
|
endpoint_variant_tags=None):
|
2021-09-22 22:53:42 +02:00
|
|
|
for partition in self._endpoint_data['partitions']:
|
|
|
|
if partition['partition'] == partition_name:
|
2022-01-12 23:38:07 +01:00
|
|
|
if endpoint_variant_tags:
|
|
|
|
variant = self._retrieve_variant_data(
|
|
|
|
partition.get('defaults'), endpoint_variant_tags)
|
|
|
|
if variant and 'dnsSuffix' in variant:
|
|
|
|
return variant['dnsSuffix']
|
|
|
|
else:
|
|
|
|
return partition['dnsSuffix']
|
2021-09-22 22:53:42 +02:00
|
|
|
return None
|
|
|
|
|
2022-01-12 23:38:07 +01:00
|
|
|
def construct_endpoint(self, service_name, region_name=None,
|
|
|
|
partition_name=None, use_dualstack_endpoint=False,
|
|
|
|
use_fips_endpoint=False):
|
|
|
|
if service_name == 's3' and use_dualstack_endpoint and region_name is None:
|
|
|
|
region_name = 'us-east-1'
|
2020-05-30 12:49:33 +02:00
|
|
|
if partition_name is not None:
|
|
|
|
valid_partition = None
|
|
|
|
for partition in self._endpoint_data['partitions']:
|
|
|
|
if partition['partition'] == partition_name:
|
|
|
|
valid_partition = partition
|
|
|
|
|
|
|
|
if valid_partition is not None:
|
2021-11-03 18:14:15 +01:00
|
|
|
result = self._endpoint_for_partition(
|
2022-01-12 23:38:07 +01:00
|
|
|
valid_partition, service_name,
|
|
|
|
region_name, use_dualstack_endpoint, use_fips_endpoint,
|
|
|
|
True
|
2021-11-03 18:14:15 +01:00
|
|
|
)
|
2020-05-30 12:49:33 +02:00
|
|
|
return result
|
|
|
|
return None
|
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
# Iterate over each partition until a match is found.
|
|
|
|
for partition in self._endpoint_data['partitions']:
|
2022-01-12 23:38:07 +01:00
|
|
|
if use_dualstack_endpoint and (
|
|
|
|
partition['partition'] in
|
|
|
|
self._UNSUPPORTED_DUALSTACK_PARTITIONS):
|
|
|
|
continue
|
2016-10-11 02:22:54 +02:00
|
|
|
result = self._endpoint_for_partition(
|
2022-01-12 23:38:07 +01:00
|
|
|
partition, service_name, region_name, use_dualstack_endpoint,
|
|
|
|
use_fips_endpoint
|
|
|
|
)
|
2016-10-11 02:22:54 +02:00
|
|
|
if result:
|
|
|
|
return result
|
|
|
|
|
2021-11-03 18:14:15 +01:00
|
|
|
def get_partition_for_region(self, region_name):
|
|
|
|
for partition in self._endpoint_data['partitions']:
|
|
|
|
if self._region_match(partition, region_name):
|
|
|
|
return partition['partition']
|
|
|
|
raise UnknownRegionError(
|
|
|
|
region_name=region_name,
|
|
|
|
error_msg='No partition found for provided region_name.'
|
|
|
|
)
|
|
|
|
|
2022-01-12 23:38:07 +01:00
|
|
|
def _endpoint_for_partition(self, partition, service_name, region_name,
|
|
|
|
use_dualstack_endpoint, use_fips_endpoint,
|
|
|
|
force_partition=False):
|
|
|
|
partition_name = partition["partition"]
|
|
|
|
if (use_dualstack_endpoint and
|
|
|
|
partition_name in self._UNSUPPORTED_DUALSTACK_PARTITIONS):
|
|
|
|
error_msg = ("Dualstack endpoints are currently not supported"
|
|
|
|
" for %s partition" % partition_name)
|
|
|
|
raise EndpointVariantError(tags=['dualstack'], error_msg=error_msg)
|
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
# Get the service from the partition, or an empty template.
|
|
|
|
service_data = partition['services'].get(
|
|
|
|
service_name, DEFAULT_SERVICE_DATA)
|
|
|
|
# Use the partition endpoint if no region is supplied.
|
|
|
|
if region_name is None:
|
|
|
|
if 'partitionEndpoint' in service_data:
|
|
|
|
region_name = service_data['partitionEndpoint']
|
2015-11-24 12:34:53 +01:00
|
|
|
else:
|
2016-10-11 02:22:54 +02:00
|
|
|
raise NoRegionError()
|
2022-01-12 23:38:07 +01:00
|
|
|
|
|
|
|
resolve_kwargs = {
|
|
|
|
'partition': partition,
|
|
|
|
'service_name': service_name,
|
|
|
|
'service_data': service_data,
|
|
|
|
'endpoint_name': region_name,
|
|
|
|
'use_dualstack_endpoint': use_dualstack_endpoint,
|
|
|
|
'use_fips_endpoint': use_fips_endpoint,
|
|
|
|
}
|
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
# Attempt to resolve the exact region for this partition.
|
|
|
|
if region_name in service_data['endpoints']:
|
2022-01-12 23:38:07 +01:00
|
|
|
return self._resolve(**resolve_kwargs)
|
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
# Check to see if the endpoint provided is valid for the partition.
|
2020-05-30 12:49:33 +02:00
|
|
|
if self._region_match(partition, region_name) or force_partition:
|
2016-10-11 02:22:54 +02:00
|
|
|
# Use the partition endpoint if set and not regionalized.
|
|
|
|
partition_endpoint = service_data.get('partitionEndpoint')
|
|
|
|
is_regionalized = service_data.get('isRegionalized', True)
|
|
|
|
if partition_endpoint and not is_regionalized:
|
|
|
|
LOG.debug('Using partition endpoint for %s, %s: %s',
|
|
|
|
service_name, region_name, partition_endpoint)
|
2022-01-12 23:38:07 +01:00
|
|
|
resolve_kwargs['endpoint_name'] = partition_endpoint
|
|
|
|
return self._resolve(**resolve_kwargs)
|
2016-10-11 02:22:54 +02:00
|
|
|
LOG.debug('Creating a regex based endpoint for %s, %s',
|
|
|
|
service_name, region_name)
|
2022-01-12 23:38:07 +01:00
|
|
|
return self._resolve(**resolve_kwargs)
|
2016-10-11 02:22:54 +02:00
|
|
|
|
|
|
|
def _region_match(self, partition, region_name):
|
|
|
|
if region_name in partition['regions']:
|
|
|
|
return True
|
|
|
|
if 'regionRegex' in partition:
|
|
|
|
return re.compile(partition['regionRegex']).match(region_name)
|
|
|
|
return False
|
|
|
|
|
2022-01-12 23:38:07 +01:00
|
|
|
def _retrieve_variant_data(self, endpoint_data, tags):
|
|
|
|
variants = endpoint_data.get('variants', [])
|
|
|
|
for variant in variants:
|
|
|
|
if set(variant['tags']) == set(tags):
|
|
|
|
result = variant.copy()
|
|
|
|
return result
|
|
|
|
|
|
|
|
def _create_tag_list(self, use_dualstack_endpoint, use_fips_endpoint):
|
|
|
|
tags = []
|
|
|
|
if use_dualstack_endpoint:
|
|
|
|
tags.append('dualstack')
|
|
|
|
if use_fips_endpoint:
|
|
|
|
tags.append('fips')
|
|
|
|
return tags
|
|
|
|
|
|
|
|
def _resolve_variant(self, tags, endpoint_data, service_defaults,
|
|
|
|
partition_defaults):
|
|
|
|
result = {}
|
|
|
|
for variants in [endpoint_data, service_defaults,
|
|
|
|
partition_defaults]:
|
|
|
|
variant = self._retrieve_variant_data(variants, tags)
|
|
|
|
if variant:
|
|
|
|
self._merge_keys(variant, result)
|
|
|
|
return result
|
|
|
|
|
|
|
|
def _resolve(self, partition, service_name, service_data, endpoint_name,
|
|
|
|
use_dualstack_endpoint, use_fips_endpoint):
|
|
|
|
endpoint_data = service_data.get('endpoints', {}).get(endpoint_name, {})
|
|
|
|
|
|
|
|
if endpoint_data.get('deprecated'):
|
|
|
|
LOG.warning(
|
|
|
|
'Client is configured with the deprecated endpoint: %s' % (
|
|
|
|
endpoint_name
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
service_defaults = service_data.get('defaults', {})
|
|
|
|
partition_defaults = partition.get('defaults', {})
|
|
|
|
tags = self._create_tag_list(use_dualstack_endpoint,
|
|
|
|
use_fips_endpoint)
|
|
|
|
|
|
|
|
if tags:
|
|
|
|
result = self._resolve_variant(tags, endpoint_data,
|
|
|
|
service_defaults,
|
|
|
|
partition_defaults)
|
|
|
|
if result == {}:
|
|
|
|
error_msg = ("Endpoint does not exist for %s in region %s" % (
|
|
|
|
service_name, endpoint_name
|
|
|
|
))
|
|
|
|
raise EndpointVariantError(tags=tags, error_msg=error_msg)
|
|
|
|
else:
|
|
|
|
result = endpoint_data
|
|
|
|
|
|
|
|
# If dnsSuffix has not already been consumed from a variant definition
|
|
|
|
if 'dnsSuffix' not in result:
|
|
|
|
result['dnsSuffix'] = partition['dnsSuffix']
|
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
result['partition'] = partition['partition']
|
|
|
|
result['endpointName'] = endpoint_name
|
2022-01-12 23:38:07 +01:00
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
# Merge in the service defaults then the partition defaults.
|
2022-01-12 23:38:07 +01:00
|
|
|
self._merge_keys(service_defaults, result)
|
|
|
|
self._merge_keys(partition_defaults, result)
|
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
result['hostname'] = self._expand_template(
|
2022-01-12 23:38:07 +01:00
|
|
|
partition, result['hostname'], service_name, endpoint_name,
|
|
|
|
result['dnsSuffix']
|
|
|
|
)
|
2016-10-11 02:22:54 +02:00
|
|
|
if 'sslCommonName' in result:
|
|
|
|
result['sslCommonName'] = self._expand_template(
|
|
|
|
partition, result['sslCommonName'], service_name,
|
2022-01-12 23:38:07 +01:00
|
|
|
endpoint_name, result['dnsSuffix'])
|
|
|
|
|
2016-10-11 02:22:54 +02:00
|
|
|
return result
|
|
|
|
|
|
|
|
def _merge_keys(self, from_data, result):
|
|
|
|
for key in from_data:
|
|
|
|
if key not in result:
|
|
|
|
result[key] = from_data[key]
|
|
|
|
|
|
|
|
def _expand_template(self, partition, template, service_name,
|
2022-01-12 23:38:07 +01:00
|
|
|
endpoint_name, dnsSuffix):
|
2016-10-11 02:22:54 +02:00
|
|
|
return template.format(
|
|
|
|
service=service_name, region=endpoint_name,
|
2022-01-12 23:38:07 +01:00
|
|
|
dnsSuffix=dnsSuffix)
|