diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 8b4d17fb5..8024e604e 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -1249,7 +1249,7 @@ - [ ] describe_vpc_endpoint_connections - [ ] describe_vpc_endpoint_service_configurations - [ ] describe_vpc_endpoint_service_permissions -- [ ] describe_vpc_endpoint_services +- [X] describe_vpc_endpoint_services - [X] describe_vpc_endpoints - [X] describe_vpc_peering_connections - [X] describe_vpcs @@ -4619,4 +4619,4 @@ - workmailmessageflow - workspaces - xray - \ No newline at end of file + diff --git a/moto/acm/models.py b/moto/acm/models.py index 7991dffa7..9dba350c9 100644 --- a/moto/acm/models.py +++ b/moto/acm/models.py @@ -416,6 +416,13 @@ class AWSCertificateManagerBackend(BaseBackend): self.__dict__ = {} self.__init__(region) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "acm-pca" + ) + @staticmethod def _arn_not_found(arn): msg = "Certificate with arn {0} not found in account {1}".format( diff --git a/moto/applicationautoscaling/models.py b/moto/applicationautoscaling/models.py index 92f2a2b80..637de8108 100644 --- a/moto/applicationautoscaling/models.py +++ b/moto/applicationautoscaling/models.py @@ -72,6 +72,13 @@ class ApplicationAutoscalingBackend(BaseBackend): self.__dict__ = {} self.__init__(region, ecs) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "application-autoscaling" + ) + @property def applicationautoscaling_backend(self): return applicationautoscaling_backends[self.region] diff --git a/moto/athena/models.py b/moto/athena/models.py index 24ad73ab9..64587729d 100644 --- a/moto/athena/models.py +++ b/moto/athena/models.py @@ -80,6 +80,13 @@ class AthenaBackend(BaseBackend): self.executions = {} self.named_queries = {} + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "athena" + ) + def create_work_group(self, name, configuration, description, tags): if name in self.work_groups: return None diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 0ba3f7a52..f416a5621 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -622,6 +622,15 @@ class AutoScalingBackend(BaseBackend): self.__dict__ = {} self.__init__(ec2_backend, elb_backend, elbv2_backend) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "autoscaling" + ) + BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "autoscaling-plans" + ) + def create_launch_configuration( self, name, diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 13af21fec..3227d00bb 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -1095,6 +1095,13 @@ class LambdaBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "lambda" + ) + def create_function(self, spec): function_name = spec.get("FunctionName", None) if function_name is None: diff --git a/moto/backends.py b/moto/backends.py index 5847ad85e..74f8dcea2 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -27,6 +27,11 @@ def backends(): yield _import_backend(module_name, backends_name) +def unique_backends(): + for module_name, backends_name in sorted(set(BACKENDS.values())): + yield _import_backend(module_name, backends_name) + + def loaded_backends(): loaded_modules = sys.modules.keys() loaded_modules = [m for m in loaded_modules if m.startswith("moto.")] diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 83328a5f1..76ef3de97 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -499,6 +499,13 @@ class CloudFormationBackend(BaseBackend): self.exports = OrderedDict() self.change_sets = OrderedDict() + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "cloudformation", policy_supported=False + ) + def _resolve_update_parameters(self, instance, incoming_params): parameters = dict( [ diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py index d5be127d1..635d86d40 100644 --- a/moto/cloudwatch/models.py +++ b/moto/cloudwatch/models.py @@ -312,6 +312,13 @@ class CloudWatchBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "monitoring" + ) + @property # Retrieve a list of all OOTB metrics that are provided by metrics providers # Computed on the fly diff --git a/moto/codecommit/models.py b/moto/codecommit/models.py index ad99e8f3d..b56c1583b 100644 --- a/moto/codecommit/models.py +++ b/moto/codecommit/models.py @@ -36,6 +36,13 @@ class CodeCommitBackend(BaseBackend): def __init__(self): self.repositories = {} + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "codecommit" + ) + def create_repository(self, region, repository_name, repository_description): repository = self.repositories.get(repository_name) if repository: diff --git a/moto/codepipeline/models.py b/moto/codepipeline/models.py index 4d2b9c0f9..4d1d8fb6b 100644 --- a/moto/codepipeline/models.py +++ b/moto/codepipeline/models.py @@ -71,6 +71,13 @@ class CodePipelineBackend(BaseBackend): def __init__(self): self.pipelines = {} + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "codepipeline", policy_supported=False + ) + @property def iam_backend(self): return iam_backends["global"] diff --git a/moto/config/models.py b/moto/config/models.py index 571dcf14f..35ee8b02c 100644 --- a/moto/config/models.py +++ b/moto/config/models.py @@ -854,6 +854,13 @@ class ConfigBackend(BaseBackend): self.config_rules = {} self.config_schema = None + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """List of dicts representing default VPC endpoints for this service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "config" + ) + def _validate_resource_types(self, resource_list): if not self.config_schema: self.config_schema = AWSServiceSpec( diff --git a/moto/core/models.py b/moto/core/models.py index 3a487f34e..d22cda2bb 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals -from __future__ import absolute_import - import functools import inspect import os +import random import re +import string import types from abc import abstractmethod from io import BytesIO @@ -698,6 +697,63 @@ class BaseBackend: return paths + @staticmethod + def default_vpc_endpoint_service( + service_region, zones, + ): # pylint: disable=unused-argument + """Invoke the factory method for any VPC endpoint(s) services.""" + return None + + @staticmethod + def vpce_random_number(): + """Return random number for a VPC endpoint service ID.""" + return "".join([random.choice(string.hexdigits.lower()) for i in range(17)]) + + @staticmethod + def default_vpc_endpoint_service_factory( + service_region, + zones, + service="", + service_type="Interface", + private_dns_names=True, + special_service_name="", + policy_supported=True, + base_endpoint_dns_names=None, + ): # pylint: disable=too-many-arguments + """List of dicts representing default VPC endpoints for this service.""" + if special_service_name: + service_name = f"com.amazonaws.{service_region}.{special_service_name}" + else: + service_name = f"com.amazonaws.{service_region}.{service}" + + if not base_endpoint_dns_names: + base_endpoint_dns_names = [f"{service}.{service_region}.vpce.amazonaws.com"] + + endpoint_service = { + "AcceptanceRequired": False, + "AvailabilityZones": zones, + "BaseEndpointDnsNames": base_endpoint_dns_names, + "ManagesVpcEndpoints": False, + "Owner": "amazon", + "ServiceId": f"vpce-svc-{BaseBackend.vpce_random_number()}", + "ServiceName": service_name, + "ServiceType": [{"ServiceType": service_type}], + "Tags": [], + "VpcEndpointPolicySupported": policy_supported, + } + + # Don't know how private DNS names are different, so for now just + # one will be added. + if private_dns_names: + endpoint_service[ + "PrivateDnsName" + ] = f"{service}.{service_region}.amazonaws.com" + endpoint_service["PrivateDnsNameVerificationState"] = "verified" + endpoint_service["PrivateDnsNames"] = [ + {"PrivateDnsName": f"{service}.{service_region}.amazonaws.com"} + ] + return [endpoint_service] + def decorator(self, func=None): if settings.TEST_SERVER_MODE: mocked_backend = ServerModeMockAWS({"global": self}) diff --git a/moto/datasync/models.py b/moto/datasync/models.py index ed60a5946..aebc2f8e4 100644 --- a/moto/datasync/models.py +++ b/moto/datasync/models.py @@ -112,6 +112,13 @@ class DataSyncBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "datasync" + ) + def create_location(self, location_uri, typ=None, metadata=None): """ # AWS DataSync allows for duplicate LocationUris diff --git a/moto/dms/models.py b/moto/dms/models.py index de857a464..063c6590d 100644 --- a/moto/dms/models.py +++ b/moto/dms/models.py @@ -25,6 +25,13 @@ class DatabaseMigrationServiceBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "dms" + ) + def create_replication_task( self, replication_task_identifier, diff --git a/moto/dynamodb2/models/__init__.py b/moto/dynamodb2/models/__init__.py index c72cee52f..fd348549f 100644 --- a/moto/dynamodb2/models/__init__.py +++ b/moto/dynamodb2/models/__init__.py @@ -711,7 +711,7 @@ class Table(CloudFormationModel): projection_expression, index_name=None, filter_expression=None, - **filter_kwargs + **filter_kwargs, ): results = [] @@ -1075,6 +1075,19 @@ class DynamoDBBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + # No 'vpce' in the base endpoint DNS name + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, + zones, + "dynamodb", + "Gateway", + private_dns_names=False, + base_endpoint_dns_names=[f"dynamodb.{service_region}.amazonaws.com"], + ) + def create_table(self, name, **params): if name in self.tables: return None @@ -1288,7 +1301,7 @@ class DynamoDBBackend(BaseBackend): expr_names=None, expr_values=None, filter_expression=None, - **filter_kwargs + **filter_kwargs, ): table = self.tables.get(table_name) if not table: @@ -1311,7 +1324,7 @@ class DynamoDBBackend(BaseBackend): projection_expression, index_name, filter_expression, - **filter_kwargs + **filter_kwargs, ) def scan( diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index bec3ffc41..a5cacdb23 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -24,21 +24,19 @@ class EC2ClientError(RESTError): request_id_tag_name = "RequestID" def __init__(self, *args, **kwargs): - kwargs.setdefault("template", "custom_response") self.templates["custom_response"] = EC2_ERROR_RESPONSE - - super(EC2ClientError, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) class DependencyViolationError(EC2ClientError): def __init__(self, message): - super(DependencyViolationError, self).__init__("DependencyViolation", message) + super().__init__("DependencyViolation", message) class MissingParameterError(EC2ClientError): def __init__(self, parameter): - super(MissingParameterError, self).__init__( + super().__init__( "MissingParameter", "The request must contain the parameter {0}".format(parameter), ) @@ -46,7 +44,7 @@ class MissingParameterError(EC2ClientError): class InvalidDHCPOptionsIdError(EC2ClientError): def __init__(self, dhcp_options_id): - super(InvalidDHCPOptionsIdError, self).__init__( + super().__init__( "InvalidDhcpOptionID.NotFound", "DhcpOptionID {0} does not exist.".format(dhcp_options_id), ) @@ -54,7 +52,7 @@ class InvalidDHCPOptionsIdError(EC2ClientError): class MalformedDHCPOptionsIdError(EC2ClientError): def __init__(self, dhcp_options_id): - super(MalformedDHCPOptionsIdError, self).__init__( + super().__init__( "InvalidDhcpOptionsId.Malformed", 'Invalid id: "{0}" (expecting "dopt-...")'.format(dhcp_options_id), ) @@ -62,21 +60,21 @@ class MalformedDHCPOptionsIdError(EC2ClientError): class InvalidKeyPairNameError(EC2ClientError): def __init__(self, key): - super(InvalidKeyPairNameError, self).__init__( + super().__init__( "InvalidKeyPair.NotFound", "The keypair '{0}' does not exist.".format(key) ) class InvalidKeyPairDuplicateError(EC2ClientError): def __init__(self, key): - super(InvalidKeyPairDuplicateError, self).__init__( + super().__init__( "InvalidKeyPair.Duplicate", "The keypair '{0}' already exists.".format(key) ) class InvalidKeyPairFormatError(EC2ClientError): def __init__(self): - super(InvalidKeyPairFormatError, self).__init__( + super().__init__( "InvalidKeyPair.Format", "Key is not in valid OpenSSH public key format" ) @@ -84,14 +82,14 @@ class InvalidKeyPairFormatError(EC2ClientError): class InvalidVPCIdError(EC2ClientError): def __init__(self, vpc_id): - super(InvalidVPCIdError, self).__init__( + super().__init__( "InvalidVpcID.NotFound", "VpcID {0} does not exist.".format(vpc_id), ) class InvalidSubnetIdError(EC2ClientError): def __init__(self, subnet_id): - super(InvalidSubnetIdError, self).__init__( + super().__init__( "InvalidSubnetID.NotFound", "The subnet ID '{}' does not exist".format(subnet_id), ) @@ -99,7 +97,7 @@ class InvalidSubnetIdError(EC2ClientError): class InvalidFlowLogIdError(EC2ClientError): def __init__(self, count, flow_log_ids): - super(InvalidFlowLogIdError, self).__init__( + super().__init__( "InvalidFlowLogId.NotFound", "These flow log ids in the input list are not found: [TotalCount: {0}] {1}".format( count, flow_log_ids @@ -109,7 +107,7 @@ class InvalidFlowLogIdError(EC2ClientError): class FlowLogAlreadyExists(EC2ClientError): def __init__(self): - super(FlowLogAlreadyExists, self).__init__( + super().__init__( "FlowLogAlreadyExists", "Error. There is an existing Flow Log with the same configuration and log destination.", ) @@ -117,7 +115,7 @@ class FlowLogAlreadyExists(EC2ClientError): class InvalidNetworkAclIdError(EC2ClientError): def __init__(self, network_acl_id): - super(InvalidNetworkAclIdError, self).__init__( + super().__init__( "InvalidNetworkAclID.NotFound", "The network acl ID '{0}' does not exist".format(network_acl_id), ) @@ -125,7 +123,7 @@ class InvalidNetworkAclIdError(EC2ClientError): class InvalidVpnGatewayIdError(EC2ClientError): def __init__(self, vpn_gw): - super(InvalidVpnGatewayIdError, self).__init__( + super().__init__( "InvalidVpnGatewayID.NotFound", "The virtual private gateway ID '{0}' does not exist".format(vpn_gw), ) @@ -133,7 +131,7 @@ class InvalidVpnGatewayIdError(EC2ClientError): class InvalidVpnGatewayAttachmentError(EC2ClientError): def __init__(self, vpn_gw, vpc_id): - super(InvalidVpnGatewayAttachmentError, self).__init__( + super().__init__( "InvalidVpnGatewayAttachment.NotFound", "The attachment with vpn gateway ID '{}' and vpc ID '{}' does not exist".format( vpn_gw, vpc_id @@ -143,7 +141,7 @@ class InvalidVpnGatewayAttachmentError(EC2ClientError): class InvalidVpnConnectionIdError(EC2ClientError): def __init__(self, network_acl_id): - super(InvalidVpnConnectionIdError, self).__init__( + super().__init__( "InvalidVpnConnectionID.NotFound", "The vpnConnection ID '{0}' does not exist".format(network_acl_id), ) @@ -151,7 +149,7 @@ class InvalidVpnConnectionIdError(EC2ClientError): class InvalidCustomerGatewayIdError(EC2ClientError): def __init__(self, customer_gateway_id): - super(InvalidCustomerGatewayIdError, self).__init__( + super().__init__( "InvalidCustomerGatewayID.NotFound", "The customer gateway ID '{0}' does not exist".format(customer_gateway_id), ) @@ -159,7 +157,7 @@ class InvalidCustomerGatewayIdError(EC2ClientError): class InvalidNetworkInterfaceIdError(EC2ClientError): def __init__(self, eni_id): - super(InvalidNetworkInterfaceIdError, self).__init__( + super().__init__( "InvalidNetworkInterfaceID.NotFound", "The network interface ID '{0}' does not exist".format(eni_id), ) @@ -167,7 +165,7 @@ class InvalidNetworkInterfaceIdError(EC2ClientError): class InvalidNetworkAttachmentIdError(EC2ClientError): def __init__(self, attachment_id): - super(InvalidNetworkAttachmentIdError, self).__init__( + super().__init__( "InvalidAttachmentID.NotFound", "The network interface attachment ID '{0}' does not exist".format( attachment_id @@ -177,7 +175,7 @@ class InvalidNetworkAttachmentIdError(EC2ClientError): class InvalidSecurityGroupDuplicateError(EC2ClientError): def __init__(self, name): - super(InvalidSecurityGroupDuplicateError, self).__init__( + super().__init__( "InvalidGroup.Duplicate", "The security group '{0}' already exists".format(name), ) @@ -185,7 +183,7 @@ class InvalidSecurityGroupDuplicateError(EC2ClientError): class InvalidSecurityGroupNotFoundError(EC2ClientError): def __init__(self, name): - super(InvalidSecurityGroupNotFoundError, self).__init__( + super().__init__( "InvalidGroup.NotFound", "The security group '{0}' does not exist".format(name), ) @@ -193,7 +191,7 @@ class InvalidSecurityGroupNotFoundError(EC2ClientError): class InvalidPermissionNotFoundError(EC2ClientError): def __init__(self): - super(InvalidPermissionNotFoundError, self).__init__( + super().__init__( "InvalidPermission.NotFound", "The specified rule does not exist in this security group", ) @@ -201,15 +199,14 @@ class InvalidPermissionNotFoundError(EC2ClientError): class InvalidPermissionDuplicateError(EC2ClientError): def __init__(self): - super(InvalidPermissionDuplicateError, self).__init__( + super().__init__( "InvalidPermission.Duplicate", "The specified rule already exists" ) class InvalidRouteTableIdError(EC2ClientError): def __init__(self, route_table_id): - - super(InvalidRouteTableIdError, self).__init__( + super().__init__( "InvalidRouteTableID.NotFound", "The routeTable ID '{0}' does not exist".format(route_table_id), ) @@ -217,7 +214,7 @@ class InvalidRouteTableIdError(EC2ClientError): class InvalidRouteError(EC2ClientError): def __init__(self, route_table_id, cidr): - super(InvalidRouteError, self).__init__( + super().__init__( "InvalidRoute.NotFound", "no route with destination-cidr-block {0} in route table {1}".format( cidr, route_table_id @@ -227,7 +224,7 @@ class InvalidRouteError(EC2ClientError): class InvalidInstanceIdError(EC2ClientError): def __init__(self, instance_id): - super(InvalidInstanceIdError, self).__init__( + super().__init__( "InvalidInstanceID.NotFound", "The instance ID '{0}' does not exist".format(instance_id), ) @@ -235,7 +232,7 @@ class InvalidInstanceIdError(EC2ClientError): class InvalidInstanceTypeError(EC2ClientError): def __init__(self, instance_type): - super(InvalidInstanceTypeError, self).__init__( + super().__init__( "InvalidInstanceType.NotFound", "The instance type '{0}' does not exist".format(instance_type), ) @@ -243,7 +240,7 @@ class InvalidInstanceTypeError(EC2ClientError): class InvalidAMIIdError(EC2ClientError): def __init__(self, ami_id): - super(InvalidAMIIdError, self).__init__( + super().__init__( "InvalidAMIID.NotFound", "The image id '[{0}]' does not exist".format(ami_id), ) @@ -251,7 +248,7 @@ class InvalidAMIIdError(EC2ClientError): class InvalidAMIAttributeItemValueError(EC2ClientError): def __init__(self, attribute, value): - super(InvalidAMIAttributeItemValueError, self).__init__( + super().__init__( "InvalidAMIAttributeItemValue", 'Invalid attribute item value "{0}" for {1} item type.'.format( value, attribute @@ -261,7 +258,7 @@ class InvalidAMIAttributeItemValueError(EC2ClientError): class MalformedAMIIdError(EC2ClientError): def __init__(self, ami_id): - super(MalformedAMIIdError, self).__init__( + super().__init__( "InvalidAMIID.Malformed", 'Invalid id: "{0}" (expecting "ami-...")'.format(ami_id), ) @@ -269,14 +266,14 @@ class MalformedAMIIdError(EC2ClientError): class InvalidSnapshotIdError(EC2ClientError): def __init__(self, snapshot_id): - super(InvalidSnapshotIdError, self).__init__( + super().__init__( "InvalidSnapshot.NotFound", "" ) # Note: AWS returns empty message for this, as of 2014.08.22. class InvalidVolumeIdError(EC2ClientError): def __init__(self, volume_id): - super(InvalidVolumeIdError, self).__init__( + super().__init__( "InvalidVolume.NotFound", "The volume '{0}' does not exist.".format(volume_id), ) @@ -284,7 +281,7 @@ class InvalidVolumeIdError(EC2ClientError): class InvalidVolumeAttachmentError(EC2ClientError): def __init__(self, volume_id, instance_id): - super(InvalidVolumeAttachmentError, self).__init__( + super().__init__( "InvalidAttachment.NotFound", "Volume {0} can not be detached from {1} because it is not attached".format( volume_id, instance_id @@ -294,7 +291,7 @@ class InvalidVolumeAttachmentError(EC2ClientError): class InvalidVolumeDetachmentError(EC2ClientError): def __init__(self, volume_id, instance_id, device): - super(InvalidVolumeDetachmentError, self).__init__( + super().__init__( "InvalidAttachment.NotFound", "The volume {0} is not attached to instance {1} as device {2}".format( volume_id, instance_id, device @@ -304,7 +301,7 @@ class InvalidVolumeDetachmentError(EC2ClientError): class VolumeInUseError(EC2ClientError): def __init__(self, volume_id, instance_id): - super(VolumeInUseError, self).__init__( + super().__init__( "VolumeInUse", "Volume {0} is currently attached to {1}".format(volume_id, instance_id), ) @@ -312,21 +309,21 @@ class VolumeInUseError(EC2ClientError): class InvalidDomainError(EC2ClientError): def __init__(self, domain): - super(InvalidDomainError, self).__init__( + super().__init__( "InvalidParameterValue", "Invalid value '{0}' for domain.".format(domain) ) class InvalidAddressError(EC2ClientError): def __init__(self, ip): - super(InvalidAddressError, self).__init__( + super().__init__( "InvalidAddress.NotFound", "Address '{0}' not found.".format(ip) ) class LogDestinationNotFoundError(EC2ClientError): def __init__(self, bucket_name): - super(LogDestinationNotFoundError, self).__init__( + super().__init__( "LogDestinationNotFoundException", "LogDestination: '{0}' does not exist.".format(bucket_name), ) @@ -334,7 +331,7 @@ class LogDestinationNotFoundError(EC2ClientError): class InvalidAllocationIdError(EC2ClientError): def __init__(self, allocation_id): - super(InvalidAllocationIdError, self).__init__( + super().__init__( "InvalidAllocationID.NotFound", "Allocation ID '{0}' not found.".format(allocation_id), ) @@ -342,7 +339,7 @@ class InvalidAllocationIdError(EC2ClientError): class InvalidAssociationIdError(EC2ClientError): def __init__(self, association_id): - super(InvalidAssociationIdError, self).__init__( + super().__init__( "InvalidAssociationID.NotFound", "Association ID '{0}' not found.".format(association_id), ) @@ -350,7 +347,7 @@ class InvalidAssociationIdError(EC2ClientError): class InvalidVpcCidrBlockAssociationIdError(EC2ClientError): def __init__(self, association_id): - super(InvalidVpcCidrBlockAssociationIdError, self).__init__( + super().__init__( "InvalidVpcCidrBlockAssociationIdError.NotFound", "The vpc CIDR block association ID '{0}' does not exist".format( association_id @@ -360,7 +357,7 @@ class InvalidVpcCidrBlockAssociationIdError(EC2ClientError): class InvalidVPCPeeringConnectionIdError(EC2ClientError): def __init__(self, vpc_peering_connection_id): - super(InvalidVPCPeeringConnectionIdError, self).__init__( + super().__init__( "InvalidVpcPeeringConnectionId.NotFound", "VpcPeeringConnectionID {0} does not exist.".format( vpc_peering_connection_id @@ -370,7 +367,7 @@ class InvalidVPCPeeringConnectionIdError(EC2ClientError): class InvalidVPCPeeringConnectionStateTransitionError(EC2ClientError): def __init__(self, vpc_peering_connection_id): - super(InvalidVPCPeeringConnectionStateTransitionError, self).__init__( + super().__init__( "InvalidStateTransition", "VpcPeeringConnectionID {0} is not in the correct state for the request.".format( vpc_peering_connection_id @@ -378,9 +375,27 @@ class InvalidVPCPeeringConnectionStateTransitionError(EC2ClientError): ) +class InvalidServiceName(EC2ClientError): + def __init__(self, service_name): + super().__init__( + "InvalidServiceName", + f"The Vpc Endpoint Service '{service_name}' does not exist", + ) + + +class InvalidFilter(EC2ClientError): + def __init__(self, filter_name): + super().__init__("InvalidFilter", f"The filter '{filter_name}' is invalid") + + +class InvalidNextToken(EC2ClientError): + def __init__(self, next_token): + super().__init__("InvalidNextToken", f"The token '{next_token}' is invalid") + + class InvalidDependantParameterError(EC2ClientError): def __init__(self, dependant_parameter, parameter, parameter_value): - super(InvalidDependantParameterError, self).__init__( + super().__init__( "InvalidParameter", "{0} can't be empty if {1} is {2}.".format( dependant_parameter, parameter, parameter_value, @@ -390,7 +405,7 @@ class InvalidDependantParameterError(EC2ClientError): class InvalidDependantParameterTypeError(EC2ClientError): def __init__(self, dependant_parameter, parameter_value, parameter): - super(InvalidDependantParameterTypeError, self).__init__( + super().__init__( "InvalidParameter", "{0} type must be {1} if {2} is provided.".format( dependant_parameter, parameter_value, parameter, @@ -400,14 +415,14 @@ class InvalidDependantParameterTypeError(EC2ClientError): class InvalidAggregationIntervalParameterError(EC2ClientError): def __init__(self, parameter): - super(InvalidAggregationIntervalParameterError, self).__init__( + super().__init__( "InvalidParameter", "Invalid {0}".format(parameter), ) class InvalidParameterValueError(EC2ClientError): def __init__(self, parameter_value): - super(InvalidParameterValueError, self).__init__( + super().__init__( "InvalidParameterValue", "Value {0} is invalid for parameter.".format(parameter_value), ) @@ -415,7 +430,7 @@ class InvalidParameterValueError(EC2ClientError): class InvalidParameterValueErrorTagNull(EC2ClientError): def __init__(self): - super(InvalidParameterValueErrorTagNull, self).__init__( + super().__init__( "InvalidParameterValue", "Tag value cannot be null. Use empty string instead.", ) @@ -423,7 +438,7 @@ class InvalidParameterValueErrorTagNull(EC2ClientError): class InvalidParameterValueErrorUnknownAttribute(EC2ClientError): def __init__(self, parameter_value): - super(InvalidParameterValueErrorUnknownAttribute, self).__init__( + super().__init__( "InvalidParameterValue", "Value ({0}) for parameter attribute is invalid. Unknown attribute.".format( parameter_value @@ -433,7 +448,7 @@ class InvalidParameterValueErrorUnknownAttribute(EC2ClientError): class InvalidGatewayIDError(EC2ClientError): def __init__(self, gateway_id): - super(InvalidGatewayIDError, self).__init__( + super().__init__( "InvalidGatewayID.NotFound", "The eigw ID '{0}' does not exist".format(gateway_id), ) @@ -441,7 +456,7 @@ class InvalidGatewayIDError(EC2ClientError): class InvalidInternetGatewayIdError(EC2ClientError): def __init__(self, internet_gateway_id): - super(InvalidInternetGatewayIdError, self).__init__( + super().__init__( "InvalidInternetGatewayID.NotFound", "InternetGatewayID {0} does not exist.".format(internet_gateway_id), ) @@ -449,7 +464,7 @@ class InvalidInternetGatewayIdError(EC2ClientError): class GatewayNotAttachedError(EC2ClientError): def __init__(self, internet_gateway_id, vpc_id): - super(GatewayNotAttachedError, self).__init__( + super().__init__( "Gateway.NotAttached", "InternetGatewayID {0} is not attached to a VPC {1}.".format( internet_gateway_id, vpc_id @@ -459,7 +474,7 @@ class GatewayNotAttachedError(EC2ClientError): class ResourceAlreadyAssociatedError(EC2ClientError): def __init__(self, resource_id): - super(ResourceAlreadyAssociatedError, self).__init__( + super().__init__( "Resource.AlreadyAssociated", "Resource {0} is already associated.".format(resource_id), ) @@ -467,7 +482,7 @@ class ResourceAlreadyAssociatedError(EC2ClientError): class TagLimitExceeded(EC2ClientError): def __init__(self): - super(TagLimitExceeded, self).__init__( + super().__init__( "TagLimitExceeded", "The maximum number of Tags for a resource has been reached.", ) @@ -475,14 +490,12 @@ class TagLimitExceeded(EC2ClientError): class InvalidID(EC2ClientError): def __init__(self, resource_id): - super(InvalidID, self).__init__( - "InvalidID", "The ID '{0}' is not valid".format(resource_id) - ) + super().__init__("InvalidID", "The ID '{0}' is not valid".format(resource_id)) class InvalidCIDRSubnetError(EC2ClientError): def __init__(self, cidr): - super(InvalidCIDRSubnetError, self).__init__( + super().__init__( "InvalidParameterValue", "invalid CIDR subnet specification: {0}".format(cidr), ) @@ -490,7 +503,7 @@ class InvalidCIDRSubnetError(EC2ClientError): class RulesPerSecurityGroupLimitExceededError(EC2ClientError): def __init__(self): - super(RulesPerSecurityGroupLimitExceededError, self).__init__( + super().__init__( "RulesPerSecurityGroupLimitExceeded", "The maximum number of rules per security group " "has been reached.", ) @@ -498,7 +511,7 @@ class RulesPerSecurityGroupLimitExceededError(EC2ClientError): class MotoNotImplementedError(NotImplementedError): def __init__(self, blurb): - super(MotoNotImplementedError, self).__init__( + super().__init__( "{0} has not been implemented in Moto yet." " Feel free to open an issue at" " https://github.com/spulec/moto/issues".format(blurb) @@ -507,14 +520,12 @@ class MotoNotImplementedError(NotImplementedError): class FilterNotImplementedError(MotoNotImplementedError): def __init__(self, filter_name, method_name): - super(FilterNotImplementedError, self).__init__( - "The filter '{0}' for {1}".format(filter_name, method_name) - ) + super().__init__("The filter '{0}' for {1}".format(filter_name, method_name)) class CidrLimitExceeded(EC2ClientError): def __init__(self, vpc_id, max_cidr_limit): - super(CidrLimitExceeded, self).__init__( + super().__init__( "CidrLimitExceeded", "This network '{0}' has met its maximum number of allowed CIDRs: {1}".format( vpc_id, max_cidr_limit @@ -524,7 +535,7 @@ class CidrLimitExceeded(EC2ClientError): class UnsupportedTenancy(EC2ClientError): def __init__(self, tenancy): - super(UnsupportedTenancy, self).__init__( + super().__init__( "UnsupportedTenancy", "The tenancy value {0} is not supported.".format(tenancy), ) @@ -532,7 +543,7 @@ class UnsupportedTenancy(EC2ClientError): class OperationNotPermitted(EC2ClientError): def __init__(self, association_id): - super(OperationNotPermitted, self).__init__( + super().__init__( "OperationNotPermitted", "The vpc CIDR block with association ID {} may not be disassociated. " "It is the primary IPv4 CIDR block of the VPC".format(association_id), @@ -541,7 +552,7 @@ class OperationNotPermitted(EC2ClientError): class InvalidAvailabilityZoneError(EC2ClientError): def __init__(self, availability_zone_value, valid_availability_zones): - super(InvalidAvailabilityZoneError, self).__init__( + super().__init__( "InvalidParameterValue", "Value ({0}) for parameter availabilityZone is invalid. " "Subnets can currently only be created in the following availability zones: {1}.".format( @@ -552,7 +563,7 @@ class InvalidAvailabilityZoneError(EC2ClientError): class NetworkAclEntryAlreadyExistsError(EC2ClientError): def __init__(self, rule_number): - super(NetworkAclEntryAlreadyExistsError, self).__init__( + super().__init__( "NetworkAclEntryAlreadyExists", "The network acl entry identified by {} already exists.".format( rule_number @@ -562,14 +573,14 @@ class NetworkAclEntryAlreadyExistsError(EC2ClientError): class InvalidSubnetRangeError(EC2ClientError): def __init__(self, cidr_block): - super(InvalidSubnetRangeError, self).__init__( + super().__init__( "InvalidSubnet.Range", "The CIDR '{}' is invalid.".format(cidr_block) ) class InvalidCIDRBlockParameterError(EC2ClientError): def __init__(self, cidr_block): - super(InvalidCIDRBlockParameterError, self).__init__( + super().__init__( "InvalidParameterValue", "Value ({}) for parameter cidrBlock is invalid. This is not a valid CIDR block.".format( cidr_block @@ -579,7 +590,7 @@ class InvalidCIDRBlockParameterError(EC2ClientError): class InvalidDestinationCIDRBlockParameterError(EC2ClientError): def __init__(self, cidr_block): - super(InvalidDestinationCIDRBlockParameterError, self).__init__( + super().__init__( "InvalidParameterValue", "Value ({}) for parameter destinationCidrBlock is invalid. This is not a valid CIDR block.".format( cidr_block @@ -589,7 +600,7 @@ class InvalidDestinationCIDRBlockParameterError(EC2ClientError): class InvalidSubnetConflictError(EC2ClientError): def __init__(self, cidr_block): - super(InvalidSubnetConflictError, self).__init__( + super().__init__( "InvalidSubnet.Conflict", "The CIDR '{}' conflicts with another subnet".format(cidr_block), ) @@ -597,7 +608,7 @@ class InvalidSubnetConflictError(EC2ClientError): class InvalidVPCRangeError(EC2ClientError): def __init__(self, cidr_block): - super(InvalidVPCRangeError, self).__init__( + super().__init__( "InvalidVpc.Range", "The CIDR '{}' is invalid.".format(cidr_block) ) @@ -605,7 +616,7 @@ class InvalidVPCRangeError(EC2ClientError): # accept exception class OperationNotPermitted2(EC2ClientError): def __init__(self, client_region, pcx_id, acceptor_region): - super(OperationNotPermitted2, self).__init__( + super().__init__( "OperationNotPermitted", "Incorrect region ({0}) specified for this request." "VPC peering connection {1} must be accepted in region {2}".format( @@ -617,7 +628,7 @@ class OperationNotPermitted2(EC2ClientError): # reject exception class OperationNotPermitted3(EC2ClientError): def __init__(self, client_region, pcx_id, acceptor_region): - super(OperationNotPermitted3, self).__init__( + super().__init__( "OperationNotPermitted", "Incorrect region ({0}) specified for this request." "VPC peering connection {1} must be accepted or rejected in region {2}".format( @@ -628,7 +639,7 @@ class OperationNotPermitted3(EC2ClientError): class OperationNotPermitted4(EC2ClientError): def __init__(self, instance_id): - super(OperationNotPermitted4, self).__init__( + super().__init__( "OperationNotPermitted", "The instance '{0}' may not be terminated. Modify its 'disableApiTermination' " "instance attribute and try again.".format(instance_id), @@ -637,7 +648,7 @@ class OperationNotPermitted4(EC2ClientError): class InvalidLaunchTemplateNameError(EC2ClientError): def __init__(self): - super(InvalidLaunchTemplateNameError, self).__init__( + super().__init__( "InvalidLaunchTemplateName.AlreadyExistsException", "Launch template name already in use.", ) @@ -645,7 +656,7 @@ class InvalidLaunchTemplateNameError(EC2ClientError): class InvalidParameterDependency(EC2ClientError): def __init__(self, param, param_needed): - super(InvalidParameterDependency, self).__init__( + super().__init__( "InvalidParameterDependency", "The parameter [{0}] requires the parameter {1} to be set.".format( param, param_needed @@ -655,7 +666,7 @@ class InvalidParameterDependency(EC2ClientError): class IncorrectStateIamProfileAssociationError(EC2ClientError): def __init__(self, instance_id): - super(IncorrectStateIamProfileAssociationError, self).__init__( + super().__init__( "IncorrectState", "There is an existing association for instance {0}".format(instance_id), ) @@ -663,7 +674,7 @@ class IncorrectStateIamProfileAssociationError(EC2ClientError): class InvalidAssociationIDIamProfileAssociationError(EC2ClientError): def __init__(self, association_id): - super(InvalidAssociationIDIamProfileAssociationError, self).__init__( + super().__init__( "InvalidAssociationID.NotFound", "An invalid association-id of '{0}' was given".format(association_id), ) @@ -671,7 +682,7 @@ class InvalidAssociationIDIamProfileAssociationError(EC2ClientError): class InvalidVpcEndPointIdError(EC2ClientError): def __init__(self, vpc_end_point_id): - super(InvalidVpcEndPointIdError, self).__init__( + super().__init__( "InvalidVpcEndpointId.NotFound", "The VpcEndPoint ID '{0}' does not exist".format(vpc_end_point_id), ) @@ -679,7 +690,7 @@ class InvalidVpcEndPointIdError(EC2ClientError): class InvalidTaggableResourceType(EC2ClientError): def __init__(self, resource_type): - super(InvalidTaggableResourceType, self).__init__( + super().__init__( "InvalidParameterValue", "'{}' is not a valid taggable resource type for this operation.".format( resource_type @@ -689,7 +700,7 @@ class InvalidTaggableResourceType(EC2ClientError): class GenericInvalidParameterValueError(EC2ClientError): def __init__(self, attribute, value): - super(GenericInvalidParameterValueError, self).__init__( + super().__init__( "InvalidParameterValue", "invalid value for parameter {0}: {1}".format(attribute, value), ) @@ -697,7 +708,7 @@ class GenericInvalidParameterValueError(EC2ClientError): class InvalidSubnetCidrBlockAssociationID(EC2ClientError): def __init__(self, association_id): - super(InvalidSubnetCidrBlockAssociationID, self).__init__( + super().__init__( "InvalidSubnetCidrBlockAssociationID.NotFound", "The subnet CIDR block with association ID '{0}' does not exist".format( association_id @@ -707,7 +718,7 @@ class InvalidSubnetCidrBlockAssociationID(EC2ClientError): class InvalidCarrierGatewayID(EC2ClientError): def __init__(self, carrier_gateway_id): - super(InvalidCarrierGatewayID, self).__init__( + super().__init__( "InvalidCarrierGatewayID.NotFound", "The CarrierGateway ID '{0}' does not exist".format(carrier_gateway_id), ) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 3b3d3cad4..ef512d80f 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1,19 +1,23 @@ from __future__ import unicode_literals import copy +from datetime import datetime import itertools import ipaddress import json -import os +from operator import itemgetter +from os import listdir +from os import environ import pathlib import re import warnings +import weakref + +from collections import defaultdict +from collections import OrderedDict from boto3 import Session -from collections import defaultdict -import weakref -from datetime import datetime from moto.packages.boto.ec2.instance import Instance as BotoInstance, Reservation from moto.packages.boto.ec2.blockdevicemapping import ( BlockDeviceMapping, @@ -24,7 +28,6 @@ from moto.packages.boto.ec2.spotinstancerequest import ( ) from moto.packages.boto.ec2.launchspecification import LaunchSpecification -from collections import OrderedDict from moto.core import BaseBackend from moto.core.models import Model, BaseModel, CloudFormationModel from moto.core.utils import ( @@ -34,7 +37,6 @@ from moto.core.utils import ( from moto.core import ACCOUNT_ID from moto.kms import kms_backends from moto.utilities.utils import load_resource, merge_multiple_dicts, filter_resources -from os import listdir from .exceptions import ( CidrLimitExceeded, @@ -65,6 +67,9 @@ from .exceptions import ( InvalidKeyPairFormatError, InvalidKeyPairNameError, InvalidAggregationIntervalParameterError, + InvalidServiceName, + InvalidFilter, + InvalidNextToken, InvalidDependantParameterError, InvalidDependantParameterTypeError, InvalidFlowLogIdError, @@ -192,20 +197,22 @@ offerings_path = "resources/instance_type_offerings" INSTANCE_TYPE_OFFERINGS = {} for location_type in listdir(root / offerings_path): INSTANCE_TYPE_OFFERINGS[location_type] = {} - for region in listdir(root / offerings_path / location_type): - full_path = offerings_path + "/" + location_type + "/" + region + for _region in listdir(root / offerings_path / location_type): + full_path = offerings_path + "/" + location_type + "/" + _region INSTANCE_TYPE_OFFERINGS[location_type][ - region.replace(".json", "") + _region.replace(".json", "") ] = load_resource(__name__, full_path) -if "MOTO_AMIS_PATH" in os.environ: - with open(os.environ.get("MOTO_AMIS_PATH"), "r", encoding="utf-8") as f: +if "MOTO_AMIS_PATH" in environ: + with open(environ.get("MOTO_AMIS_PATH"), "r", encoding="utf-8") as f: AMIS = json.load(f) else: AMIS = load_resource(__name__, "resources/amis.json") OWNER_ID = ACCOUNT_ID +MAX_NUMBER_OF_ENDPOINT_SERVICES_RESULTS = 1000 +DEFAULT_VPC_ENDPOINT_SERVICES = [] def utc_date_and_time(): @@ -441,15 +448,13 @@ class NetworkInterface(TaggedEC2Resource, CloudFormationModel): elif filter_name == "description": return self.description else: - return super(NetworkInterface, self).get_filter_value( - filter_name, "DescribeNetworkInterfaces" - ) + return super().get_filter_value(filter_name, "DescribeNetworkInterfaces") class NetworkInterfaceBackend(object): def __init__(self): self.enis = {} - super(NetworkInterfaceBackend, self).__init__() + super().__init__() def create_network_interface( self, @@ -459,7 +464,7 @@ class NetworkInterfaceBackend(object): group_ids=None, description=None, tags=None, - **kwargs + **kwargs, ): eni = NetworkInterface( self, @@ -469,7 +474,7 @@ class NetworkInterfaceBackend(object): group_ids=group_ids, description=description, tags=tags, - **kwargs + **kwargs, ) self.enis[eni.id] = eni return eni @@ -563,7 +568,7 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): } def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs): - super(Instance, self).__init__() + super().__init__() self.ec2_backend = ec2_backend self.id = random_instance_id() self.lifecycle = kwargs.get("lifecycle") @@ -990,7 +995,7 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): class InstanceBackend(object): def __init__(self): self.reservations = OrderedDict() - super(InstanceBackend, self).__init__() + super().__init__() def get_instance(self, instance_id): for instance in self.all_instances(): @@ -1220,7 +1225,7 @@ class InstanceBackend(object): class InstanceTypeBackend(object): def __init__(self): - super(InstanceTypeBackend, self).__init__() + super().__init__() def describe_instance_types(self, instance_types=None): matches = INSTANCE_TYPES.values() @@ -1236,7 +1241,7 @@ class InstanceTypeBackend(object): class InstanceTypeOfferingBackend(object): def __init__(self): - super(InstanceTypeOfferingBackend, self).__init__() + super().__init__() def describe_instance_type_offerings(self, location_type=None, filters=None): location_type = location_type or "region" @@ -1283,7 +1288,7 @@ class KeyPair(object): class KeyPairBackend(object): def __init__(self): self.keypairs = {} - super(KeyPairBackend, self).__init__() + super().__init__() def create_key_pair(self, name): if name in self.keypairs: @@ -1359,7 +1364,7 @@ class TagBackend(object): def __init__(self): self.tags = defaultdict(dict) - super(TagBackend, self).__init__() + super().__init__() def create_tags(self, resource_ids, tags): if None in set([tags[tag] for tag in tags]): @@ -1581,7 +1586,7 @@ class Ami(TaggedEC2Resource): elif filter_name == "owner-alias": return self.owner_alias else: - return super(Ami, self).get_filter_value(filter_name, "DescribeImages") + return super().get_filter_value(filter_name, "DescribeImages") class AmiBackend(object): @@ -1589,10 +1594,8 @@ class AmiBackend(object): def __init__(self): self.amis = {} - self._load_amis() - - super(AmiBackend, self).__init__() + super().__init__() def _load_amis(self): for ami in AMIS: @@ -2335,7 +2338,7 @@ class SecurityGroupBackend(object): self.sg_old_ingress_ruls = {} self.sg_old_egress_ruls = {} - super(SecurityGroupBackend, self).__init__() + super().__init__() def create_security_group( self, name, description, vpc_id=None, tags=None, force=False, is_default=None @@ -3102,7 +3105,7 @@ class Volume(TaggedEC2Resource, CloudFormationModel): elif filter_name == "availability-zone": return self.zone.name else: - return super(Volume, self).get_filter_value(filter_name, "DescribeVolumes") + return super().get_filter_value(filter_name, "DescribeVolumes") class Snapshot(TaggedEC2Resource): @@ -3144,9 +3147,7 @@ class Snapshot(TaggedEC2Resource): elif filter_name == "owner-id": return self.owner_id else: - return super(Snapshot, self).get_filter_value( - filter_name, "DescribeSnapshots" - ) + return super().get_filter_value(filter_name, "DescribeSnapshots") class EBSBackend(object): @@ -3154,7 +3155,7 @@ class EBSBackend(object): self.volumes = {} self.attachments = {} self.snapshots = {} - super(EBSBackend, self).__init__() + super().__init__() def create_volume( self, size, zone_name, snapshot_id=None, encrypted=False, kms_key_id=None @@ -3444,7 +3445,7 @@ class VPC(TaggedEC2Resource, CloudFormationModel): return None return self.dhcp_options.id else: - return super(VPC, self).get_filter_value(filter_name, "DescribeVpcs") + return super().get_filter_value(filter_name, "DescribeVpcs") def modify_vpc_tenancy(self, tenancy): if tenancy != "default": @@ -3539,7 +3540,7 @@ class VPCBackend(object): self.vpcs = {} self.vpc_end_points = {} self.vpc_refs[self.__class__].add(weakref.ref(self)) - super(VPCBackend, self).__init__() + super().__init__() @classmethod def get_vpc_refs(cls): @@ -3831,19 +3832,172 @@ class VPCBackend(object): return generic_filter(filters, vpc_end_points) - def get_vpc_end_point_services(self): - vpc_end_point_services = self.vpc_end_points.values() + @staticmethod + def _collect_default_endpoint_services(region): + """Return list of default services using list of backends.""" + if DEFAULT_VPC_ENDPOINT_SERVICES: + return DEFAULT_VPC_ENDPOINT_SERVICES - services = [] - for value in vpc_end_point_services: - services.append(value.service_name) + zones = [ + zone.name + for zones in RegionsAndZonesBackend.zones.values() + for zone in zones + if zone.name.startswith(region) + ] - availability_zones = EC2Backend.describe_availability_zones(self) + from moto import backends # pylint: disable=import-outside-toplevel + + for _backends in backends.unique_backends(): + if region in _backends: + service = _backends[region].default_vpc_endpoint_service(region, zones) + if service: + DEFAULT_VPC_ENDPOINT_SERVICES.extend(service) + + if "global" in _backends: + service = _backends["global"].default_vpc_endpoint_service( + region, zones + ) + if service: + DEFAULT_VPC_ENDPOINT_SERVICES.extend(service) + return DEFAULT_VPC_ENDPOINT_SERVICES + + @staticmethod + def _matches_service_by_tags(service, filter_item): + """Return True if service tags are not filtered by their tags. + + Note that the API specifies a key of "Values" for a filter, but + the botocore library returns "Value" instead. + """ + # For convenience, collect the tags for this service. + service_tag_keys = {x["Key"] for x in service["Tags"]} + if not service_tag_keys: + return False + + matched = True # assume the best + if filter_item["Name"] == "tag-key": + # Filters=[{"Name":"tag-key", "Values":["Name"]}], + # Any tag with this name, regardless of the tag value. + if not service_tag_keys & set(filter_item["Value"]): + matched = False + + elif filter_item["Name"].startswith("tag:"): + # Filters=[{"Name":"tag:Name", "Values":["my-load-balancer"]}], + tag_name = filter_item["Name"].split(":")[1] + if not service_tag_keys & {tag_name}: + matched = False + else: + for tag in service["Tags"]: + if tag["Key"] == tag_name and tag["Value"] in filter_item["Value"]: + break + else: + matched = False + return matched + + @staticmethod + def _filter_endpoint_services(service_names_filters, filters, services): + """Return filtered list of VPC endpoint services.""" + if not service_names_filters and not filters: + return services + + # Verify the filters are valid. + for filter_item in filters: + if filter_item["Name"] not in [ + "service-name", + "service-type", + "tag-key", + ] and not filter_item["Name"].startswith("tag:"): + raise InvalidFilter(filter_item["Name"]) + + # Apply both the service_names filter and the filters themselves. + filtered_services = [] + for service in services: + if ( + service_names_filters + and service["ServiceName"] not in service_names_filters + ): + continue + + # Note that the API specifies a key of "Values" for a filter, but + # the botocore library returns "Value" instead. + matched = True + for filter_item in filters: + if filter_item["Name"] == "service-name": + if service["ServiceName"] not in filter_item["Value"]: + matched = False + + elif filter_item["Name"] == "service-type": + service_types = {x["ServiceType"] for x in service["ServiceType"]} + if not service_types & set(filter_item["Value"]): + matched = False + + elif filter_item["Name"] == "tag-key" or filter_item["Name"].startswith( + "tag:" + ): + if not VPCBackend._matches_service_by_tags(service, filter_item): + matched = False + + # Exit early -- don't bother checking the remaining filters + # as a non-match was found. + if not matched: + break + + # Does the service have a matching service name or does it match + # a filter? + if matched: + filtered_services.append(service) + + return filtered_services + + def describe_vpc_endpoint_services( + self, dry_run, service_names, filters, max_results, next_token, region + ): # pylint: disable=unused-argument,too-many-arguments + """Return info on services to which you can create a VPC endpoint. + + Currently only the default endpoing services are returned. When + create_vpc_endpoint_service_configuration() is implemented, a + list of those private endpoints would be kept and when this API + is invoked, those private endpoints would be added to the list of + default endpoint services. + + The DryRun parameter is ignored. + """ + default_services = self._collect_default_endpoint_services(region) + for service_name in service_names: + if service_name not in [x["ServiceName"] for x in default_services]: + raise InvalidServiceName(service_name) + + # Apply filters specified in the service_names and filters arguments. + filtered_services = sorted( + self._filter_endpoint_services(service_names, filters, default_services), + key=itemgetter("ServiceName"), + ) + + # Determine the start index into list of services based on the + # next_token argument. + start = 0 + vpce_ids = [x["ServiceId"] for x in filtered_services] + if next_token: + if next_token not in vpce_ids: + raise InvalidNextToken(next_token) + start = vpce_ids.index(next_token) + + # Determine the stop index into the list of services based on the + # max_results argument. + if not max_results or max_results > MAX_NUMBER_OF_ENDPOINT_SERVICES_RESULTS: + max_results = MAX_NUMBER_OF_ENDPOINT_SERVICES_RESULTS + + # If necessary, set the value of the next_token. + next_token = "" + if len(filtered_services) > (start + max_results): + service = filtered_services[start + max_results] + next_token = service["ServiceId"] return { - "servicesDetails": vpc_end_point_services, - "services": services, - "availability_zones": availability_zones, + "servicesDetails": filtered_services[start : start + max_results], + "serviceNames": [ + x["ServiceName"] for x in filtered_services[start : start + max_results] + ], + "nextToken": next_token, } def get_vpc_end_point(self, vpc_end_point_id): @@ -3931,7 +4085,7 @@ class VPCPeeringConnectionBackend(object): def __init__(self): self.vpc_pcxs = {} self.vpc_pcx_refs[self.__class__].add(weakref.ref(self)) - super(VPCPeeringConnectionBackend, self).__init__() + super().__init__() @classmethod def get_vpc_pcx_refs(cls): @@ -4130,7 +4284,7 @@ class Subnet(TaggedEC2Resource, CloudFormationModel): elif filter_name == "state": return self.state else: - return super(Subnet, self).get_filter_value(filter_name, "DescribeSubnets") + return super().get_filter_value(filter_name, "DescribeSubnets") def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException @@ -4202,7 +4356,7 @@ class SubnetBackend(object): def __init__(self): # maps availability zone to dict of (subnet_id, subnet) self.subnets = defaultdict(dict) - super(SubnetBackend, self).__init__() + super().__init__() def get_subnet(self, subnet_id): for subnets in self.subnets.values(): @@ -4467,15 +4621,13 @@ class FlowLogs(TaggedEC2Resource, CloudFormationModel): elif filter_name == "deliver-log-status": return "SUCCESS" else: - return super(FlowLogs, self).get_filter_value( - filter_name, "DescribeFlowLogs" - ) + return super().get_filter_value(filter_name, "DescribeFlowLogs") class FlowLogsBackend(object): def __init__(self): self.flow_logs = defaultdict(dict) - super(FlowLogsBackend, self).__init__() + super().__init__() def _validate_request( self, @@ -4683,7 +4835,7 @@ class SubnetRouteTableAssociation(CloudFormationModel): class SubnetRouteTableAssociationBackend(object): def __init__(self): self.subnet_associations = {} - super(SubnetRouteTableAssociationBackend, self).__init__() + super().__init__() def create_subnet_association(self, route_table_id, subnet_id): subnet_association = SubnetRouteTableAssociation(route_table_id, subnet_id) @@ -4749,15 +4901,13 @@ class RouteTable(TaggedEC2Resource, CloudFormationModel): elif filter_name == "association.subnet-id": return self.associations.values() else: - return super(RouteTable, self).get_filter_value( - filter_name, "DescribeRouteTables" - ) + return super().get_filter_value(filter_name, "DescribeRouteTables") class RouteTableBackend(object): def __init__(self): self.route_tables = {} - super(RouteTableBackend, self).__init__() + super().__init__() def create_route_table(self, vpc_id, tags=[], main=False): route_table_id = random_route_table_id() @@ -5035,7 +5185,7 @@ class ManagedPrefixListBackend(object): def __init__(self): self.managed_prefix_lists = {} self.create_default_pls() - super(ManagedPrefixListBackend, self).__init__() + super().__init__() def create_managed_prefix_list( self, @@ -5161,7 +5311,7 @@ class ManagedPrefixListBackend(object): class RouteBackend(object): def __init__(self): - super(RouteBackend, self).__init__() + super().__init__() def create_route( self, @@ -5362,7 +5512,7 @@ class InternetGateway(TaggedEC2Resource, CloudFormationModel): class InternetGatewayBackend(object): def __init__(self): self.internet_gateways = {} - super(InternetGatewayBackend, self).__init__() + super().__init__() def create_internet_gateway(self, tags=[]): igw = InternetGateway(self) @@ -5434,7 +5584,7 @@ class CarrierGateway(TaggedEC2Resource): class CarrierGatewayBackend(object): def __init__(self): self.carrier_gateways = {} - super(CarrierGatewayBackend, self).__init__() + super().__init__() def create_carrier_gateway(self, vpc_id, tags=None): vpc = self.get_vpc(vpc_id) @@ -5490,7 +5640,7 @@ class EgressOnlyInternetGateway(TaggedEC2Resource): class EgressOnlyInternetGatewayBackend(object): def __init__(self): self.egress_only_internet_gateway_backend = {} - super(EgressOnlyInternetGatewayBackend, self).__init__() + super().__init__() def create_egress_only_internet_gateway(self, vpc_id, tags=None): vpc = self.get_vpc(vpc_id) @@ -5563,7 +5713,7 @@ class VPCGatewayAttachment(CloudFormationModel): class VPCGatewayAttachmentBackend(object): def __init__(self): self.gateway_attachments = {} - super(VPCGatewayAttachmentBackend, self).__init__() + super().__init__() def create_vpc_gateway_attachment(self, vpc_id, gateway_id): attachment = VPCGatewayAttachment(vpc_id, gateway_id) @@ -5594,9 +5744,9 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource): subnet_id, tags, spot_fleet_id, - **kwargs + **kwargs, ): - super(SpotInstanceRequest, self).__init__(**kwargs) + super().__init__(**kwargs) ls = LaunchSpecification() self.ec2_backend = ec2_backend self.launch_specification = ls @@ -5638,9 +5788,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource): elif filter_name == "spot-instance-request-id": return self.id else: - return super(SpotInstanceRequest, self).get_filter_value( - filter_name, "DescribeSpotInstanceRequests" - ) + return super().get_filter_value(filter_name, "DescribeSpotInstanceRequests") def launch_instance(self): reservation = self.ec2_backend.add_instances( @@ -5663,7 +5811,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource): class SpotRequestBackend(object, metaclass=Model): def __init__(self): self.spot_instance_requests = {} - super(SpotRequestBackend, self).__init__() + super().__init__() def request_spot_instances( self, @@ -5974,7 +6122,7 @@ class SpotFleetRequest(TaggedEC2Resource, CloudFormationModel): class SpotFleetBackend(object): def __init__(self): self.spot_fleet_requests = {} - super(SpotFleetBackend, self).__init__() + super().__init__() def request_spot_fleet( self, @@ -6141,15 +6289,13 @@ class ElasticAddress(TaggedEC2Resource, CloudFormationModel): # TODO: implement network-interface-owner-id raise FilterNotImplementedError(filter_name, "DescribeAddresses") else: - return super(ElasticAddress, self).get_filter_value( - filter_name, "DescribeAddresses" - ) + return super().get_filter_value(filter_name, "DescribeAddresses") class ElasticAddressBackend(object): def __init__(self): self.addresses = [] - super(ElasticAddressBackend, self).__init__() + super().__init__() def allocate_address(self, domain, address=None, tags=None): if domain not in ["standard", "vpc"]: @@ -6322,9 +6468,7 @@ class DHCPOptionsSet(TaggedEC2Resource): values = [item for item in list(self._options.values()) if item] return itertools.chain(*values) else: - return super(DHCPOptionsSet, self).get_filter_value( - filter_name, "DescribeDhcpOptions" - ) + return super().get_filter_value(filter_name, "DescribeDhcpOptions") @property def options(self): @@ -6334,7 +6478,7 @@ class DHCPOptionsSet(TaggedEC2Resource): class DHCPOptionsSetBackend(object): def __init__(self): self.dhcp_options_sets = {} - super(DHCPOptionsSetBackend, self).__init__() + super().__init__() def associate_dhcp_options(self, dhcp_options, vpc): dhcp_options.vpc = vpc @@ -6440,15 +6584,13 @@ class VPNConnection(TaggedEC2Resource): self.add_tags(tags or {}) def get_filter_value(self, filter_name): - return super(VPNConnection, self).get_filter_value( - filter_name, "DescribeVpnConnections" - ) + return super().get_filter_value(filter_name, "DescribeVpnConnections") class VPNConnectionBackend(object): def __init__(self): self.vpn_connections = {} - super(VPNConnectionBackend, self).__init__() + super().__init__() def create_vpn_connection( self, @@ -6514,7 +6656,7 @@ class VPNConnectionBackend(object): class NetworkAclBackend(object): def __init__(self): self.network_acls = {} - super(NetworkAclBackend, self).__init__() + super().__init__() def get_network_acl(self, network_acl_id): network_acl = self.network_acls.get(network_acl_id, None) @@ -6702,7 +6844,7 @@ class NetworkAclAssociation(object): self.new_association_id = new_association_id self.subnet_id = subnet_id self.network_acl_id = network_acl_id - super(NetworkAclAssociation, self).__init__() + super().__init__() class NetworkAcl(TaggedEC2Resource): @@ -6729,9 +6871,7 @@ class NetworkAcl(TaggedEC2Resource): elif filter_name == "owner-id": return self.owner_id else: - return super(NetworkAcl, self).get_filter_value( - filter_name, "DescribeNetworkAcls" - ) + return super().get_filter_value(filter_name, "DescribeNetworkAcls") class NetworkAclEntry(TaggedEC2Resource): @@ -6781,7 +6921,7 @@ class VpnGateway(TaggedEC2Resource): self.state = state self.add_tags(tags or {}) self.attachments = {} - super(VpnGateway, self).__init__() + super().__init__() def get_filter_value(self, filter_name): if filter_name == "attachment.vpc-id": @@ -6792,22 +6932,20 @@ class VpnGateway(TaggedEC2Resource): return self.id elif filter_name == "type": return self.type - return super(VpnGateway, self).get_filter_value( - filter_name, "DescribeVpnGateways" - ) + return super().get_filter_value(filter_name, "DescribeVpnGateways") class VpnGatewayAttachment(object): def __init__(self, vpc_id, state): self.vpc_id = vpc_id self.state = state - super(VpnGatewayAttachment, self).__init__() + super().__init__() class VpnGatewayBackend(object): def __init__(self): self.vpn_gateways = {} - super(VpnGatewayBackend, self).__init__() + super().__init__() def create_vpn_gateway( self, type="ipsec.1", amazon_side_asn=None, availability_zone=None, tags=None @@ -6870,18 +7008,16 @@ class CustomerGateway(TaggedEC2Resource): self.attachments = {} self.state = state self.add_tags(tags or {}) - super(CustomerGateway, self).__init__() + super().__init__() def get_filter_value(self, filter_name): - return super(CustomerGateway, self).get_filter_value( - filter_name, "DescribeCustomerGateways" - ) + return super().get_filter_value(filter_name, "DescribeCustomerGateways") class CustomerGatewayBackend(object): def __init__(self): self.customer_gateways = {} - super(CustomerGatewayBackend, self).__init__() + super().__init__() def create_customer_gateway( self, type="ipsec.1", ip_address=None, bgp_asn=None, tags=None @@ -7006,7 +7142,7 @@ class TransitGateway(TaggedEC2Resource, CloudFormationModel): class TransitGatewayBackend(object): def __init__(self): self.transit_gateways = {} - super(TransitGatewayBackend, self).__init__() + super().__init__() def create_transit_gateway(self, description=None, options=None, tags=[]): transit_gateway = TransitGateway(self, description, options) @@ -7081,7 +7217,7 @@ class TransitGatewayRouteTable(TaggedEC2Resource): class TransitGatewayRouteTableBackend(object): def __init__(self): self.transit_gateways_route_tables = {} - super(TransitGatewayRouteTableBackend, self).__init__() + super().__init__() def create_transit_gateway_route_table( self, @@ -7400,7 +7536,7 @@ class TransitGatewayPeeringAttachment(TransitGatewayAttachment): class TransitGatewayAttachmentBackend(object): def __init__(self): self.transit_gateway_attachments = {} - super(TransitGatewayAttachmentBackend, self).__init__() + super().__init__() def create_transit_gateway_vpn_attachment( self, vpn_id, transit_gateway_id, tags=[] @@ -7645,7 +7781,7 @@ class TransitGatewayRelationsBackend(object): def __init__(self): self.transit_gateway_associations = {} self.transit_gateway_propagations = {} - super(TransitGatewayRelationsBackend, self).__init__() + super().__init__() def associate_transit_gateway_route_table( self, transit_gateway_attachment_id=None, transit_gateway_route_table_id=None @@ -7793,7 +7929,7 @@ class NatGateway(CloudFormationModel, TaggedEC2Resource): class NatGatewayBackend(object): def __init__(self): self.nat_gateways = {} - super(NatGatewayBackend, self).__init__() + super().__init__() def describe_nat_gateways(self, filters, nat_gateway_ids): nat_gateways = list(self.nat_gateways.values()) @@ -7916,9 +8052,7 @@ class LaunchTemplate(TaggedEC2Resource): if filter_name == "launch-template-name": return self.name else: - return super(LaunchTemplate, self).get_filter_value( - filter_name, "DescribeLaunchTemplates" - ) + return super().get_filter_value(filter_name, "DescribeLaunchTemplates") class LaunchTemplateBackend(object): @@ -7926,7 +8060,7 @@ class LaunchTemplateBackend(object): self.launch_template_name_to_ids = {} self.launch_templates = OrderedDict() self.launch_template_insert_order = [] - super(LaunchTemplateBackend, self).__init__() + super().__init__() def create_launch_template(self, name, description, template_data): if name in self.launch_template_name_to_ids: @@ -7971,7 +8105,7 @@ class IamInstanceProfileAssociation(CloudFormationModel): class IamInstanceProfileAssociationBackend(object): def __init__(self): self.iam_instance_profile_associations = {} - super(IamInstanceProfileAssociationBackend, self).__init__() + super().__init__() def associate_iam_instance_profile( self, @@ -8117,7 +8251,7 @@ class EC2Backend( ): def __init__(self, region_name): self.region_name = region_name - super(EC2Backend, self).__init__() + super().__init__() # Default VPC exists by default, which is the current behavior # of EC2-VPC. See for detail: @@ -8149,6 +8283,15 @@ class EC2Backend( self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "ec2" + ) + BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "ec2messages" + ) + # Use this to generate a proper error template response when in a response # handler. def raise_error(self, code, message): diff --git a/moto/ec2/responses/vpcs.py b/moto/ec2/responses/vpcs.py index ca9985b47..ccbf63676 100644 --- a/moto/ec2/responses/vpcs.py +++ b/moto/ec2/responses/vpcs.py @@ -133,6 +133,7 @@ class VPCs(BaseResponse): attr_value = self.querystring.get("%s.Value" % attribute)[0] self.ec2_backend.modify_vpc_attribute(vpc_id, attr_name, attr_value) return MODIFY_VPC_ATTRIBUTE_RESPONSE + return None def associate_vpc_cidr_block(self): vpc_id = self._get_param("VpcId") @@ -181,7 +182,7 @@ class VPCs(BaseResponse): service_name = self._get_param("ServiceName") route_table_ids = self._get_multi_param("RouteTableId") subnet_ids = self._get_multi_param("SubnetId") - type = self._get_param("VpcEndpointType") + endpoint_type = self._get_param("VpcEndpointType") policy_document = self._get_param("PolicyDocument") client_token = self._get_param("ClientToken") tags = self._get_multi_param("TagSpecification") @@ -192,7 +193,7 @@ class VPCs(BaseResponse): vpc_end_point = self.ec2_backend.create_vpc_endpoint( vpc_id=vpc_id, service_name=service_name, - type=type, + type=endpoint_type, policy_document=policy_document, route_table_ids=route_table_ids, subnet_ids=subnet_ids, @@ -205,7 +206,14 @@ class VPCs(BaseResponse): return template.render(vpc_end_point=vpc_end_point) def describe_vpc_endpoint_services(self): - vpc_end_point_services = self.ec2_backend.get_vpc_end_point_services() + vpc_end_point_services = self.ec2_backend.describe_vpc_endpoint_services( + dry_run=self._get_bool_param("DryRun"), + service_names=self._get_multi_param("ServiceName"), + filters=self._get_multi_param("Filter"), + max_results=self._get_int_param("MaxResults"), + next_token=self._get_param("NextToken"), + region=self.region, + ) template = self.response_template(DESCRIBE_VPC_ENDPOINT_SERVICES_RESPONSE) return template.render(vpc_end_points=vpc_end_point_services) @@ -589,33 +597,63 @@ CREATE_VPC_END_POINT = """ 19a9ff46-7df6-49b8-9726-3df27527089d - {% for serviceName in vpc_end_points.services %} + {% for serviceName in vpc_end_points.serviceNames %} {{ serviceName }} {% endfor %} {% for service in vpc_end_points.servicesDetails %} - - amazon - - - {{ service.type }} - - - - {{ ".".join((service.service_name.split(".")[::-1])) }} - - false + + {{ 'true' if service.AcceptanceRequired else 'false' }} - {% for zone in vpc_end_points.availability_zones %} - {{ zone.name }} + {% for zone in service.AvailabilityZones %} + {{ zone }} {% endfor %} - {{ service.service_name }} - true - + + {% for endpoint in service.BaseEndpointDnsNames %} + {{ endpoint }} + {% endfor %} + + {{ 'true' if service.ManagesVpcEndpoints else 'false' }} + {{ service.Owner }} + {% if service.PrivateDnsName is defined %} + {{ service.PrivateDnsName }} + + {% for dns_name in service.PrivateDnsNames %} + + {{ dns_name.PrivateDnsName }} + + {% endfor %} + + {{ service.PrivateDnsNameVerificationState }} + {% endif %} + {{ service.ServiceId }} + {{ service.ServiceName }} + + {% for service_type in service.ServiceType %} + + {{ service_type.ServiceType }} + + {% endfor %} + + + {% for tag in service.Tags %} + {% for key, value in tag.items() %} + + {{ key }} + {{ value }} + + {% endfor %} + {% endfor %} + + {{ 'true' if service.VpcEndpointPolicySupported else 'false' }} + {% endfor %} + {% if vpc_end_points.nextToken|length %} + {{ vpc_end_points.nextToken }} + {% endif %} """ DESCRIBE_VPC_ENDPOINT_RESPONSE = """ diff --git a/moto/ecr/models.py b/moto/ecr/models.py index ed6bb2e57..2a0e87064 100644 --- a/moto/ecr/models.py +++ b/moto/ecr/models.py @@ -335,6 +335,30 @@ class ECRBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + docker_endpoint = { + "AcceptanceRequired": False, + "AvailabilityZones": zones, + "BaseEndpointDnsNames": [f"dkr.ecr.{service_region}.vpce.amazonaws.com"], + "ManagesVpcEndpoints": False, + "Owner": "amazon", + "PrivateDnsName": f"*.dkr.ecr.{service_region}.amazonaws.com", + "PrivateDnsNameVerificationState": "verified", + "PrivateDnsNames": [ + {"PrivateDnsName": f"*.dkr.ecr.{service_region}.amazonaws.com"} + ], + "ServiceId": f"vpce-svc-{BaseBackend.vpce_random_number()}", + "ServiceName": f"com.amazonaws.{service_region}.ecr.dkr", + "ServiceType": [{"ServiceType": "Interface"}], + "Tags": [], + "VpcEndpointPolicySupported": True, + } + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "api.ecr", special_service_name="ecr.api", + ) + [docker_endpoint] + def _get_repository(self, name, registry_id=None) -> Repository: repo = self.repositories.get(name) reg_id = registry_id or DEFAULT_REGISTRY_ID diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 850c6f3a7..704f7457f 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -675,6 +675,13 @@ class EC2ContainerServiceBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "ecs" + ) + def _get_cluster(self, name): # short name or full ARN of the cluster cluster_name = name.split("/")[-1] diff --git a/moto/elasticbeanstalk/models.py b/moto/elasticbeanstalk/models.py index 13650bb24..5449fe9b6 100644 --- a/moto/elasticbeanstalk/models.py +++ b/moto/elasticbeanstalk/models.py @@ -79,6 +79,15 @@ class EBBackend(BaseBackend): self.__dict__ = {} self.__init__(region) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "elasticbeanstalk" + ) + BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "elasticbeanstalk-health" + ) + def create_application(self, application_name): if application_name in self.applications: raise InvalidParameterValueError( diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index a3c60d40f..585e85013 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -553,6 +553,13 @@ class ELBv2Backend(BaseBackend): self.target_groups = OrderedDict() self.load_balancers = OrderedDict() + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "elasticloadbalancing" + ) + @property def ec2_backend(self): """ diff --git a/moto/emr/models.py b/moto/emr/models.py index 93651db90..39883467c 100644 --- a/moto/emr/models.py +++ b/moto/emr/models.py @@ -397,6 +397,13 @@ class ElasticMapReduceBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "elasticmapreduce" + ) + @property def ec2_backend(self): """ diff --git a/moto/events/models.py b/moto/events/models.py index 0c3fa52c2..7a0b800ad 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -915,6 +915,13 @@ class EventsBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "events" + ) + def _add_default_event_bus(self): self.event_buses["default"] = EventBus(self.region_name, "default") diff --git a/moto/firehose/models.py b/moto/firehose/models.py index fc56b377d..2cba721cf 100644 --- a/moto/firehose/models.py +++ b/moto/firehose/models.py @@ -174,6 +174,13 @@ class FirehoseBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "firehose", special_service_name="kinesis-firehose" + ) + def create_delivery_stream( self, region, diff --git a/moto/glue/models.py b/moto/glue/models.py index d11056115..0c416653e 100644 --- a/moto/glue/models.py +++ b/moto/glue/models.py @@ -24,6 +24,13 @@ class GlueBackend(BaseBackend): self.databases = OrderedDict() self.crawlers = OrderedDict() + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "glue" + ) + def create_database(self, database_name, database_input): if database_name in self.databases: raise DatabaseAlreadyExistsException() diff --git a/moto/iot/models.py b/moto/iot/models.py index 0e86f93d2..35efa6905 100644 --- a/moto/iot/models.py +++ b/moto/iot/models.py @@ -505,6 +505,20 @@ class IoTBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "iot" + ) + BaseBackend.default_vpc_endpoint_service_factory( + service_region, + zones, + "data.iot", + private_dns_names=False, + special_service_name="iot.data", + policy_supported=False, + ) + def create_thing(self, thing_name, thing_type_name, attribute_payload): thing_types = self.list_thing_types() thing_type = None diff --git a/moto/kinesis/models.py b/moto/kinesis/models.py index 93764feb2..8783e4464 100644 --- a/moto/kinesis/models.py +++ b/moto/kinesis/models.py @@ -325,6 +325,13 @@ class KinesisBackend(BaseBackend): def __init__(self): self.streams = OrderedDict() + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "kinesis", special_service_name="kinesis-streams" + ) + def create_stream( self, stream_name, shard_count, retention_period_hours, region_name ): diff --git a/moto/kms/models.py b/moto/kms/models.py index 7ea40d8b4..b9c52bf48 100644 --- a/moto/kms/models.py +++ b/moto/kms/models.py @@ -159,6 +159,13 @@ class KmsBackend(BaseBackend): self.key_to_aliases = defaultdict(set) self.tagger = TaggingService(key_name="TagKey", value_name="TagValue") + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "kms" + ) + def create_key( self, policy, key_usage, customer_master_key_spec, description, tags, region ): diff --git a/moto/logs/models.py b/moto/logs/models.py index ee0fb912c..5571dfad8 100644 --- a/moto/logs/models.py +++ b/moto/logs/models.py @@ -551,6 +551,13 @@ class LogsBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "logs" + ) + def create_log_group(self, log_group_name, tags, **kwargs): if log_group_name in self.groups: raise ResourceAlreadyExistsException() diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 04f9f72eb..d4ffaca26 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -847,6 +847,15 @@ class RDS2Backend(BaseBackend): self.__dict__ = {} self.__init__(region) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "rds" + ) + BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "rds-data" + ) + def create_database(self, db_kwargs): database_id = db_kwargs["db_instance_identifier"] database = Database(**db_kwargs) diff --git a/moto/redshift/models.py b/moto/redshift/models.py index df8fcab44..51cec80d4 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -574,6 +574,15 @@ class RedshiftBackend(BaseBackend): self.__dict__ = {} self.__init__(ec2_backend, region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "redshift" + ) + BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "redshift-data", policy_supported=False + ) + def enable_snapshot_copy(self, **kwargs): cluster_identifier = kwargs["cluster_identifier"] cluster = self.clusters[cluster_identifier] diff --git a/moto/s3/models.py b/moto/s3/models.py index 3c8a70fca..de7045279 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -1,11 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - import json import os import base64 import datetime -import pytz import hashlib import copy import itertools @@ -18,8 +15,9 @@ import sys import time import uuid - from bisect import insort +import pytz + from moto.core import ACCOUNT_ID, BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import iso_8601_datetime_without_milliseconds_s3, rfc_1123_datetime from moto.cloudwatch.models import MetricDatum @@ -1301,6 +1299,38 @@ class S3Backend(BaseBackend): self.account_public_access_block = None self.tagger = TaggingService() + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """List of dicts representing default VPC endpoints for this service.""" + accesspoint = { + "AcceptanceRequired": False, + "AvailabilityZones": zones, + "BaseEndpointDnsNames": [ + f"accesspoint.s3-global.{service_region}.vpce.amazonaws.com", + ], + "ManagesVpcEndpoints": False, + "Owner": "amazon", + "PrivateDnsName": "*.accesspoint.s3-global.amazonaws.com", + "PrivateDnsNameVerificationState": "verified", + "PrivateDnsNames": [ + {"PrivateDnsName": "*.accesspoint.s3-global.amazonaws.com"} + ], + "ServiceId": f"vpce-svc-{BaseBackend.vpce_random_number()}", + "ServiceName": "com.amazonaws.s3-global.accesspoint", + "ServiceType": [{"ServiceType": "Interface"}], + "Tags": [], + "VpcEndpointPolicySupported": True, + } + return ( + BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "s3", "Interface" + ) + + BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "s3", "Gateway" + ) + + [accesspoint] + ) + # TODO: This is broken! DO NOT IMPORT MUTABLE DATA TYPES FROM OTHER AREAS -- THIS BREAKS UNMOCKING! # WRAP WITH A GETTER/SETTER FUNCTION # Register this class as a CloudWatch Metric Provider diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py index b6d513b25..47fce08d2 100644 --- a/moto/sagemaker/models.py +++ b/moto/sagemaker/models.py @@ -877,6 +877,58 @@ class SageMakerModelBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint services.""" + api_service = BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "api.sagemaker", special_service_name="sagemaker.api" + ) + + notebook_service_id = f"vpce-svc-{BaseBackend.vpce_random_number()}" + studio_service_id = f"vpce-svc-{BaseBackend.vpce_random_number()}" + + notebook_service = { + "AcceptanceRequired": False, + "AvailabilityZones": zones, + "BaseEndpointDnsNames": [ + f"{notebook_service_id}.{service_region}.vpce.amazonaws.com", + f"notebook.{service_region}.vpce.sagemaker.aws", + ], + "ManagesVpcEndpoints": False, + "Owner": "amazon", + "PrivateDnsName": f"*.notebook.{service_region}.sagemaker.aws", + "PrivateDnsNameVerificationState": "verified", + "PrivateDnsNames": [ + {"PrivateDnsName": f"*.notebook.{service_region}.sagemaker.aws"} + ], + "ServiceId": notebook_service_id, + "ServiceName": f"aws.sagemaker.{service_region}.notebook", + "ServiceType": [{"ServiceType": "Interface"}], + "Tags": [], + "VpcEndpointPolicySupported": True, + } + studio_service = { + "AcceptanceRequired": False, + "AvailabilityZones": zones, + "BaseEndpointDnsNames": [ + f"{studio_service_id}.{service_region}.vpce.amazonaws.com", + f"studio.{service_region}.vpce.sagemaker.aws", + ], + "ManagesVpcEndpoints": False, + "Owner": "amazon", + "PrivateDnsName": f"*.studio.{service_region}.sagemaker.aws", + "PrivateDnsNameVerificationState": "verified", + "PrivateDnsNames": [ + {"PrivateDnsName": f"*.studio.{service_region}.sagemaker.aws"} + ], + "ServiceId": studio_service_id, + "ServiceName": f"aws.sagemaker.{service_region}.studio", + "ServiceType": [{"ServiceType": "Interface"}], + "Tags": [], + "VpcEndpointPolicySupported": True, + } + return api_service + [notebook_service, studio_service] + def create_model(self, **kwargs): model_obj = Model( region_name=self.region_name, diff --git a/moto/secretsmanager/models.py b/moto/secretsmanager/models.py index dc596774c..ebb4340e4 100644 --- a/moto/secretsmanager/models.py +++ b/moto/secretsmanager/models.py @@ -182,6 +182,13 @@ class SecretsManagerBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint services.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "secretsmanager" + ) + def _is_valid_identifier(self, identifier): return identifier in self.secrets diff --git a/moto/sns/models.py b/moto/sns/models.py index ad6794dd9..351f51bd2 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -387,6 +387,13 @@ class SNSBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """List of dicts representing default VPC endpoints for this service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "sns" + ) + def update_sms_attributes(self, attrs): self.sms_attributes.update(attrs) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 4434df5ba..6c74f1ece 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -584,6 +584,13 @@ class SQSBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "sqs" + ) + def create_queue(self, name, tags=None, **kwargs): queue = self.queues.get(name) if queue: diff --git a/moto/ssm/models.py b/moto/ssm/models.py index 017e7f26b..d30fd1ee4 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -688,6 +688,15 @@ class SimpleSystemManagerBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint services.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "ssm" + ) + BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "ssmmessages" + ) + def _generate_document_information(self, ssm_document, document_format): content = self._get_document_content(document_format, ssm_document) base = { diff --git a/moto/sts/models.py b/moto/sts/models.py index 4891e97ce..730a21d0d 100644 --- a/moto/sts/models.py +++ b/moto/sts/models.py @@ -60,6 +60,13 @@ class STSBackend(BaseBackend): def __init__(self): self.assumed_roles = [] + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "sts" + ) + def get_session_token(self, duration): token = Token(duration=duration) return token diff --git a/moto/transcribe/models.py b/moto/transcribe/models.py index b5abc9431..156142691 100644 --- a/moto/transcribe/models.py +++ b/moto/transcribe/models.py @@ -450,6 +450,15 @@ class TranscribeBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint services.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "transcribe" + ) + BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "transcribestreaming" + ) + def start_transcription_job(self, **kwargs): name = kwargs.get("transcription_job_name") diff --git a/moto/xray/models.py b/moto/xray/models.py index 6352fa37c..3f071f6ba 100644 --- a/moto/xray/models.py +++ b/moto/xray/models.py @@ -236,6 +236,13 @@ class XRayBackend(BaseBackend): self._telemetry_records = [] self._segment_collection = SegmentCollection() + @staticmethod + def default_vpc_endpoint_service(service_region, zones): + """Default VPC endpoint service.""" + return BaseBackend.default_vpc_endpoint_service_factory( + service_region, zones, "xray" + ) + def add_telemetry_records(self, json): self._telemetry_records.append(TelemetryRecords.from_json(json)) diff --git a/tests/test_ec2/test_vpc_endpoint_services.py b/tests/test_ec2/test_vpc_endpoint_services.py new file mode 100644 index 000000000..bd0422278 --- /dev/null +++ b/tests/test_ec2/test_vpc_endpoint_services.py @@ -0,0 +1,259 @@ +"""Unit tests specific to VPC endpoint services.""" +import pytest + +import boto3 +from botocore.exceptions import ClientError + +from moto import mock_ec2 + + +@mock_ec2 +def test_describe_vpc_endpoint_services_bad_args(): + """Verify exceptions are raised for bad arguments.""" + ec2 = boto3.client("ec2", region_name="us-west-1") + + # Bad service name -- a default service would typically be of the format: + # 'com.amazonaws..'. + with pytest.raises(ClientError) as exc: + ec2.describe_vpc_endpoint_services(ServiceNames=["s3"]) + err = exc.value.response["Error"] + assert err["Code"] == "InvalidServiceName" + assert "The Vpc Endpoint Service 's3' does not exist" in err["Message"] + + # Bad filter specification -- the filter name should be "service-type" + # not "ServiceType". + with pytest.raises(ClientError) as exc: + ec2.describe_vpc_endpoint_services( + ServiceNames=["com.amazonaws.us-west-1.s3"], + Filters=[{"Name": "ServiceType", "Values": ["Gateway"]}], + ) + err = exc.value.response["Error"] + assert err["Code"] == "InvalidFilter" + assert "The filter 'ServiceType' is invalid" in err["Message"] + + # Bad token -- a token of "foo" has no correlation with this data. + with pytest.raises(ClientError) as exc: + ec2.describe_vpc_endpoint_services( + ServiceNames=["com.amazonaws.us-west-1.s3"], + Filters=[{"Name": "service-type", "Values": ["Gateway"]}], + NextToken="foo", + ) + err = exc.value.response["Error"] + assert err["Code"] == "InvalidNextToken" + assert "The token 'foo' is invalid" in err["Message"] + + +def fake_endpoint_services(): + """Return a dummy list of default VPC endpoint services.""" + return [ + { + "AcceptanceRequired": False, + "AvailabilityZones": ["us-west-1a", "us-west-1b"], + "BaseEndpointDnsNames": ["access-analyzer.us-west-1.vpce.amazonaws.com"], + "ManagesVpcEndpoints": False, + "Owner": "amazon", + "PrivateDnsName": "access-analyzer.us-west-1.amazonaws.com", + "PrivateDnsNameVerificationState": "verified", + "PrivateDnsNames": [ + {"PrivateDnsName": "access-analyzer.us-west-1.amazonaws.com"}, + ], + "ServiceId": "vpce-svc-1", + "ServiceName": "com.amazonaws.us-west-1.access-analyzer", + "ServiceType": [{"ServiceType": "Interface"}], + "Tags": [], + "VpcEndpointPolicySupported": True, + }, + { + "AcceptanceRequired": False, + "AvailabilityZones": ["us-west-1a", "us-west-1b"], + "BaseEndpointDnsNames": ["config.us-west-1.vpce.amazonaws.com"], + "ManagesVpcEndpoints": False, + "Owner": "amazon", + "PrivateDnsName": "config.us-west-1.amazonaws.com", + "PrivateDnsNameVerificationState": "verified", + "PrivateDnsNames": [{"PrivateDnsName": "config.us-west-1.amazonaws.com"}], + "ServiceId": "vpce-svc-2", + "ServiceName": "com.amazonaws.us-west-1.config", + "ServiceType": [{"ServiceType": "Interface"}], + "Tags": [], + "VpcEndpointPolicySupported": True, + }, + { + "AcceptanceRequired": True, + "AvailabilityZones": ["us-west-1a", "us-west-1b"], + "BaseEndpointDnsNames": ["s3.us-west-1.amazonaws.com"], + "ManagesVpcEndpoints": True, + "Owner": "amazon", + "ServiceId": "vpce-svc-3", + "ServiceName": "com.amazonaws.us-west-1.s3", + "ServiceType": [{"ServiceType": "Gateway"}], + "Tags": [{"Key": "Name", "Value": "s3_gw"}], + "VpcEndpointPolicySupported": False, + }, + { + "AcceptanceRequired": False, + "AvailabilityZones": ["us-west-1a", "us-west-1b"], + "BaseEndpointDnsNames": ["s3.us-west-1.vpce.amazonaws.com"], + "ManagesVpcEndpoints": False, + "Owner": "amazon", + "ServiceId": "vpce-svc-4", + "ServiceName": "com.amazonaws.us-west-1.s3", + "ServiceType": [{"ServiceType": "Interface"}], + "Tags": [ + {"Key": "Name", "Value": "s3_if"}, + {"Key": "Environ", "Value": "test"}, + ], + "VpcEndpointPolicySupported": True, + }, + ] + + +def validate_s3_service_endpoint_gateway(details): + """Validate response contains appropriate s3 Gateway service details.""" + assert details["AcceptanceRequired"] is True + assert details["AvailabilityZones"] == ["us-west-1a", "us-west-1b"] + assert details["BaseEndpointDnsNames"] == ["s3.us-west-1.amazonaws.com"] + assert details["ManagesVpcEndpoints"] is True + assert details["Owner"] == "amazon" + assert details["ServiceId"] == "vpce-svc-3" + assert details["ServiceName"] == "com.amazonaws.us-west-1.s3" + assert details["ServiceType"] == [{"ServiceType": "Gateway"}] + assert details["VpcEndpointPolicySupported"] is False + assert details["Tags"][0] == {"Key": "Name", "Value": "s3_gw"} + + +def validate_s3_service_endpoint_interface(details): + """Validate response contains appropriate s3 Gateway service details.""" + assert details["AcceptanceRequired"] is False + assert details["AvailabilityZones"] == ["us-west-1a", "us-west-1b"] + assert details["BaseEndpointDnsNames"] == ["s3.us-west-1.vpce.amazonaws.com"] + assert details["ManagesVpcEndpoints"] is False + assert details["Owner"] == "amazon" + assert details["ServiceId"] == "vpce-svc-4" + assert details["ServiceName"] == "com.amazonaws.us-west-1.s3" + assert details["ServiceType"] == [{"ServiceType": "Interface"}] + assert details["VpcEndpointPolicySupported"] is True + assert details["Tags"][0] == {"Key": "Name", "Value": "s3_if"} + assert details["Tags"][1] == {"Key": "Environ", "Value": "test"} + + +@mock_ec2 +def test_describe_vpc_endpoint_services_filters(): + """Verify that different type of filters return the expected results.""" + from moto.ec2.models import ec2_backends # pylint: disable=import-outside-toplevel + + ec2_backend = ec2_backends["us-west-1"] + test_data = fake_endpoint_services() + + # Allow access to _filter_endpoint_services as it provides the best + # means of testing this logic. + # pylint: disable=protected-access + + # Test a service name filter, using s3 as the service name. + filtered_services = ec2_backend._filter_endpoint_services( + ["com.amazonaws.us-west-1.s3"], [], test_data, + ) + assert len(filtered_services) == 2 + validate_s3_service_endpoint_gateway(filtered_services[0]) + validate_s3_service_endpoint_interface(filtered_services[1]) + + # Test a service type filter. + filtered_services = ec2_backend._filter_endpoint_services( + [], [{"Name": "service-type", "Value": ["Gateway"]}], test_data, + ) + assert len(filtered_services) == 1 + validate_s3_service_endpoint_gateway(filtered_services[0]) + + # Test a tag key/value filter. + filtered_services = ec2_backend._filter_endpoint_services( + [], [{"Name": "tag-key", "Value": ["Name"]}], test_data, + ) + assert len(filtered_services) == 2 + validate_s3_service_endpoint_gateway(filtered_services[0]) + validate_s3_service_endpoint_interface(filtered_services[1]) + + # Test a tag key filter. + filtered_services = ec2_backend._filter_endpoint_services( + [], [{"Name": "tag:Environ", "Value": ["test"]}], test_data, + ) + assert len(filtered_services) == 1 + validate_s3_service_endpoint_interface(filtered_services[0]) + + # Test when there are no filters. + filtered_services = ec2_backend._filter_endpoint_services([], [], test_data) + assert len(filtered_services) == 4 + + # Test a combo of service name and multiple filters. + filtered_services = ec2_backend._filter_endpoint_services( + ["com.amazonaws.us-west-1.s3"], + [{"Name": "tag:Environ", "Value": ["test"]}], + test_data, + ) + assert len(filtered_services) == 1 + validate_s3_service_endpoint_interface(filtered_services[0]) + + +@mock_ec2 +def test_describe_vpc_default_endpoint_services(): + """Test successfull calls as well as the next_token arg.""" + ec2 = boto3.client("ec2", region_name="us-west-1") + + # Verify the major components of the response. The unit test for filters + # verifies the contents of some of the ServicesDetails entries, so the + # focus of this unit test will be the larger components of the response. + all_services = ec2.describe_vpc_endpoint_services() + assert set(all_services.keys()) == set( + ["ServiceNames", "ServiceDetails", "ResponseMetadata"] + ) + assert len(all_services["ServiceDetails"]) == len(all_services["ServiceNames"]) + all_names = [x["ServiceName"] for x in all_services["ServiceDetails"]] + assert set(all_names) == set(all_services["ServiceNames"]) + + # Verify the handling of the next token. + partial_services = ec2.describe_vpc_endpoint_services(MaxResults=2) + assert len(partial_services["ServiceDetails"]) == 2 + assert len(partial_services["ServiceNames"]) == 2 + assert all_names[0] == partial_services["ServiceNames"][0] + assert all_names[1] == partial_services["ServiceNames"][1] + assert all_names[0] == partial_services["ServiceDetails"][0]["ServiceName"] + assert all_names[1] == partial_services["ServiceDetails"][1]["ServiceName"] + assert partial_services["NextToken"] == ( + all_services["ServiceDetails"][2]["ServiceId"] + ) + + # Use the next token to receive another service. + more_services = ec2.describe_vpc_endpoint_services( + MaxResults=1, NextToken=partial_services["NextToken"] + ) + assert len(more_services["ServiceDetails"]) == 1 + assert len(more_services["ServiceNames"]) == 1 + assert all_names[2] == more_services["ServiceNames"][0] + assert all_names[2] == more_services["ServiceDetails"][0]["ServiceName"] + assert more_services["NextToken"] == all_services["ServiceDetails"][3]["ServiceId"] + + # Use the next token to receive the remaining services. + remaining_services = ec2.describe_vpc_endpoint_services( + NextToken=more_services["NextToken"] + ) + assert len(remaining_services["ServiceDetails"]) == len(all_names) - 3 + assert "NextToken" not in remaining_services + + # Extract one service and verify all the fields. This time the data is + # extracted from the actual response. + config_service = ec2.describe_vpc_endpoint_services( + ServiceNames=["com.amazonaws.us-west-1.config"] + ) + details = config_service["ServiceDetails"][0] + assert details["AcceptanceRequired"] is False + assert details["AvailabilityZones"] == ["us-west-1a", "us-west-1b"] + assert details["BaseEndpointDnsNames"] == ["config.us-west-1.vpce.amazonaws.com"] + assert details["ManagesVpcEndpoints"] is False + assert details["Owner"] == "amazon" + assert details["PrivateDnsName"] == "config.us-west-1.amazonaws.com" + assert details["PrivateDnsNames"] == [ + {"PrivateDnsName": "config.us-west-1.amazonaws.com"} + ] + assert details["PrivateDnsNameVerificationState"] == "verified" + assert details["ServiceName"] == "com.amazonaws.us-west-1.config" + assert details["ServiceType"] == [{"ServiceType": "Interface"}] + assert details["VpcEndpointPolicySupported"] is True diff --git a/tests/test_ec2/test_vpcs.py b/tests/test_ec2/test_vpcs.py index 13151f64d..bd10df56b 100644 --- a/tests/test_ec2/test_vpcs.py +++ b/tests/test_ec2/test_vpcs.py @@ -864,37 +864,6 @@ def test_describe_classic_link_dns_support_multiple(): ) -@mock_ec2 -def test_describe_vpc_end_point_services(): - ec2 = boto3.client("ec2", region_name="us-west-1") - vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") - - route_table = ec2.create_route_table(VpcId=vpc["Vpc"]["VpcId"]) - - ec2.create_vpc_endpoint( - VpcId=vpc["Vpc"]["VpcId"], - ServiceName="com.amazonaws.us-east-1.s3", - RouteTableIds=[route_table["RouteTable"]["RouteTableId"]], - VpcEndpointType="gateway", - ) - - vpc_end_point_services = ec2.describe_vpc_endpoint_services() - - assert vpc_end_point_services.get("ServiceDetails").should.be.true - assert vpc_end_point_services.get("ServiceNames").should.be.true - assert vpc_end_point_services.get("ServiceNames") == ["com.amazonaws.us-east-1.s3"] - assert ( - vpc_end_point_services.get("ServiceDetails")[0] - .get("ServiceType", [])[0] - .get("ServiceType") - == "gateway" - ) - assert vpc_end_point_services.get("ServiceDetails")[0].get("AvailabilityZones") == [ - "us-west-1a", - "us-west-1b", - ] - - @mock_ec2 def test_describe_vpc_end_points(): ec2 = boto3.client("ec2", region_name="us-west-1")