diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 267111b42..e610de325 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -268,6 +268,8 @@ jobs: cd .. - name: "Create report" run: | + ls -la + cp server_output.log moto-terraform-tests/build/server_output.log cd moto-terraform-tests bin/create-report bin/create-report-cli diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 861d9a9cc..8543607b4 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -1280,7 +1280,7 @@ ## ec2
-34% implemented +35% implemented - [ ] accept_reserved_instances_exchange_quote - [ ] accept_transit_gateway_multicast_domain_associations @@ -1394,7 +1394,7 @@ - [X] create_vpc - [X] create_vpc_endpoint - [ ] create_vpc_endpoint_connection_notification -- [ ] create_vpc_endpoint_service_configuration +- [X] create_vpc_endpoint_service_configuration - [X] create_vpc_peering_connection - [X] create_vpn_connection - [ ] create_vpn_connection_route @@ -1455,7 +1455,7 @@ - [X] delete_volume - [X] delete_vpc - [ ] delete_vpc_endpoint_connection_notifications -- [ ] delete_vpc_endpoint_service_configurations +- [X] delete_vpc_endpoint_service_configurations - [X] delete_vpc_endpoints - [X] delete_vpc_peering_connection - [X] delete_vpn_connection @@ -1593,8 +1593,8 @@ - [ ] describe_vpc_classic_link_dns_support - [ ] describe_vpc_endpoint_connection_notifications - [ ] describe_vpc_endpoint_connections -- [ ] describe_vpc_endpoint_service_configurations -- [ ] describe_vpc_endpoint_service_permissions +- [X] describe_vpc_endpoint_service_configurations +- [X] describe_vpc_endpoint_service_permissions - [X] describe_vpc_endpoint_services - [X] describe_vpc_endpoints - [X] describe_vpc_peering_connections @@ -1728,9 +1728,9 @@ - [X] modify_vpc_attribute - [ ] modify_vpc_endpoint - [ ] modify_vpc_endpoint_connection_notification -- [ ] modify_vpc_endpoint_service_configuration +- [X] modify_vpc_endpoint_service_configuration - [ ] modify_vpc_endpoint_service_payer_responsibility -- [ ] modify_vpc_endpoint_service_permissions +- [X] modify_vpc_endpoint_service_permissions - [X] modify_vpc_peering_connection_options - [X] modify_vpc_tenancy - [ ] modify_vpn_connection diff --git a/MANIFEST.in b/MANIFEST.in index 567be3ce6..303594116 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include README.md LICENSE AUTHORS.md include requirements.txt requirements-dev.txt tox.ini include moto/config/resources/aws_managed_rules.json +include moto/ec2/_models/*.py include moto/ec2/resources/instance_types.json include moto/ec2/resources/instance_type_offerings/*/*.json include moto/ec2/resources/amis.json diff --git a/docs/docs/services/ec2.rst b/docs/docs/services/ec2.rst index c15fa8eac..7fa9ea0ae 100644 --- a/docs/docs/services/ec2.rst +++ b/docs/docs/services/ec2.rst @@ -141,7 +141,7 @@ ec2 - [X] create_vpc - [X] create_vpc_endpoint - [ ] create_vpc_endpoint_connection_notification -- [ ] create_vpc_endpoint_service_configuration +- [X] create_vpc_endpoint_service_configuration - [X] create_vpc_peering_connection - [X] create_vpn_connection - [ ] create_vpn_connection_route @@ -202,7 +202,7 @@ ec2 - [X] delete_volume - [X] delete_vpc - [ ] delete_vpc_endpoint_connection_notifications -- [ ] delete_vpc_endpoint_service_configurations +- [X] delete_vpc_endpoint_service_configurations - [X] delete_vpc_endpoints - [X] delete_vpc_peering_connection - [X] delete_vpn_connection @@ -344,8 +344,16 @@ ec2 - [ ] describe_vpc_classic_link_dns_support - [ ] describe_vpc_endpoint_connection_notifications - [ ] describe_vpc_endpoint_connections -- [ ] describe_vpc_endpoint_service_configurations -- [ ] describe_vpc_endpoint_service_permissions +- [X] describe_vpc_endpoint_service_configurations + + The Filters, MaxResults, NextToken parameters are not yet implemented + + +- [X] describe_vpc_endpoint_service_permissions + + The Filters, MaxResults, NextToken parameters are not yet implemented + + - [X] describe_vpc_endpoint_services Return info on services to which you can create a VPC endpoint. @@ -490,9 +498,13 @@ ec2 - [X] modify_vpc_attribute - [ ] modify_vpc_endpoint - [ ] modify_vpc_endpoint_connection_notification -- [ ] modify_vpc_endpoint_service_configuration +- [X] modify_vpc_endpoint_service_configuration + + The following parameters are not yet implemented: RemovePrivateDnsName, AddNetworkLoadBalancerArns, RemoveNetworkLoadBalancerArns, AddGatewayLoadBalancerArns, RemoveGatewayLoadBalancerArns + + - [ ] modify_vpc_endpoint_service_payer_responsibility -- [ ] modify_vpc_endpoint_service_permissions +- [X] modify_vpc_endpoint_service_permissions - [X] modify_vpc_peering_connection_options - [X] modify_vpc_tenancy - [ ] modify_vpn_connection diff --git a/moto/ec2/_models/core.py b/moto/ec2/_models/core.py new file mode 100644 index 000000000..8941321bc --- /dev/null +++ b/moto/ec2/_models/core.py @@ -0,0 +1,39 @@ +from moto.core.models import BaseModel + +from ..exceptions import FilterNotImplementedError + + +class TaggedEC2Resource(BaseModel): + def get_tags(self, *args, **kwargs): + tags = [] + if self.id: + tags = self.ec2_backend.describe_tags(filters={"resource-id": [self.id]}) + return tags + + def add_tag(self, key, value): + self.ec2_backend.create_tags([self.id], {key: value}) + + def add_tags(self, tag_map): + for key, value in tag_map.items(): + self.ec2_backend.create_tags([self.id], {key: value}) + + def get_filter_value(self, filter_name, method_name=None): + tags = self.get_tags() + + if filter_name.startswith("tag:"): + tagname = filter_name.replace("tag:", "", 1) + for tag in tags: + if tag["key"] == tagname: + return tag["value"] + + return None + elif filter_name == "tag-key": + return [tag["key"] for tag in tags] + elif filter_name == "tag-value": + return [tag["value"] for tag in tags] + + value = getattr(self, filter_name.lower().replace("-", "_"), None) + if value is not None: + return value + + raise FilterNotImplementedError(filter_name, method_name) diff --git a/moto/ec2/_models/vpc_service_configuration.py b/moto/ec2/_models/vpc_service_configuration.py new file mode 100644 index 000000000..9992bf6c4 --- /dev/null +++ b/moto/ec2/_models/vpc_service_configuration.py @@ -0,0 +1,132 @@ +from moto.core.models import CloudFormationModel +from moto.core.utils import get_random_hex +from .core import TaggedEC2Resource +from ..exceptions import UnknownVpcEndpointService + + +class VPCServiceConfiguration(TaggedEC2Resource, CloudFormationModel): + def __init__( + self, load_balancers, region, acceptance_required, private_dns_name, ec2_backend + ): + self.id = f"vpce-svc-{get_random_hex(length=8)}" + self.service_name = f"com.amazonaws.vpce.{region}.{self.id}" + self.service_state = "Available" + + self.availability_zones = [] + for lb in load_balancers: + for subnet in lb.subnets: + self.availability_zones.append(subnet.availability_zone) + + self.gateway_load_balancer_arns = [] + self.network_load_balancer_arns = [] + for lb in load_balancers: + if lb.loadbalancer_type == "network": + self.service_type = "Interface" + self.network_load_balancer_arns.append(lb.arn) + else: + self.service_type = "Gateway" + self.gateway_load_balancer_arns.append(lb.arn) + + self.acceptance_required = acceptance_required + self.manages_vpc_endpoints = False + self.private_dns_name = private_dns_name + self.endpoint_dns_name = f"{self.id}.{region}.vpce.amazonaws.com" + + self.principals = [] + self.ec2_backend = ec2_backend + + +class VPCServiceConfigurationBackend(object): + def __init__(self): + self.configurations = {} + super().__init__() + + @property + def elbv2_backend(self): + from moto.elbv2.models import elbv2_backends + + return elbv2_backends[self.region_name] + + def get_vpc_endpoint_service(self, resource_id): + return self.configurations.get(resource_id) + + def create_vpc_endpoint_service_configuration( + self, lb_arns, acceptance_required, private_dns_name, tags + ): + lbs = self.elbv2_backend.describe_load_balancers(arns=lb_arns, names=None) + config = VPCServiceConfiguration( + load_balancers=lbs, + region=self.region_name, + acceptance_required=acceptance_required, + private_dns_name=private_dns_name, + ec2_backend=self, + ) + for tag in tags or []: + tag_key = tag.get("Key") + tag_value = tag.get("Value") + config.add_tag(tag_key, tag_value) + + self.configurations[config.id] = config + return config + + def describe_vpc_endpoint_service_configurations(self, service_ids): + """ + The Filters, MaxResults, NextToken parameters are not yet implemented + """ + if service_ids: + found_configs = [] + for service_id in service_ids: + if service_id in self.configurations: + found_configs.append(self.configurations[service_id]) + else: + raise UnknownVpcEndpointService(service_id) + return found_configs + return self.configurations.values() + + def delete_vpc_endpoint_service_configurations(self, service_ids): + missing = [s for s in service_ids if s not in self.configurations] + for s in service_ids: + self.configurations.pop(s, None) + return missing + + def describe_vpc_endpoint_service_permissions(self, service_id): + """ + The Filters, MaxResults, NextToken parameters are not yet implemented + """ + config = self.describe_vpc_endpoint_service_configurations([service_id])[0] + return config.principals + + def modify_vpc_endpoint_service_permissions( + self, service_id, add_principals, remove_principals + ): + config = self.describe_vpc_endpoint_service_configurations([service_id])[0] + config.principals += add_principals + config.principals = [p for p in config.principals if p not in remove_principals] + config.principals = list(set(config.principals)) + + def modify_vpc_endpoint_service_configuration( + self, + service_id, + acceptance_required, + private_dns_name, + add_network_lbs, + remove_network_lbs, + add_gateway_lbs, + remove_gateway_lbs, + ): + """ + The following parameters are not yet implemented: RemovePrivateDnsName + """ + config = self.describe_vpc_endpoint_service_configurations([service_id])[0] + if private_dns_name is not None: + config.private_dns_name = private_dns_name + if acceptance_required is not None: + config.acceptance_required = str(acceptance_required).lower() == "true" + for lb in add_network_lbs: + config.network_load_balancer_arns.append(lb) + for lb in remove_network_lbs: + config.network_load_balancer_arns.remove(lb) + for lb in add_gateway_lbs: + config.gateway_load_balancer_arns.append(lb) + for lb in remove_gateway_lbs: + config.gateway_load_balancer_arns.remove(lb) diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index 447ababd7..243c2eafa 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -734,3 +734,19 @@ class InvalidCarrierGatewayID(EC2ClientError): "InvalidCarrierGatewayID.NotFound", "The CarrierGateway ID '{0}' does not exist".format(carrier_gateway_id), ) + + +class NoLoadBalancersProvided(EC2ClientError): + def __init__(self): + super().__init__( + "InvalidParameter", + "exactly one of network_load_balancer_arn or gateway_load_balancer_arn is a required member", + ) + + +class UnknownVpcEndpointService(EC2ClientError): + def __init__(self, service_id): + super().__init__( + "InvalidVpcEndpointServiceId.NotFound", + f"The VpcEndpointService Id '{service_id}' does not exist", + ) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index b04ab44f6..72eac17ae 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -17,7 +17,7 @@ from boto3 import Session from moto.core import ACCOUNT_ID from moto.core import BaseBackend -from moto.core.models import Model, BaseModel, CloudFormationModel +from moto.core.models import Model, CloudFormationModel from moto.core.utils import ( iso_8601_datetime_with_milliseconds, camelcase_to_underscores, @@ -121,6 +121,8 @@ from .exceptions import ( InvalidGatewayIDError, InvalidCarrierGatewayID, ) +from ._models.core import TaggedEC2Resource +from ._models.vpc_service_configuration import VPCServiceConfigurationBackend from .utils import ( EC2_RESOURCE_TO_PREFIX, EC2_PREFIX_TO_RESOURCE, @@ -239,42 +241,6 @@ class StateReason(object): self.code = code -class TaggedEC2Resource(BaseModel): - def get_tags(self, *args, **kwargs): - tags = [] - if self.id: - tags = self.ec2_backend.describe_tags(filters={"resource-id": [self.id]}) - return tags - - def add_tag(self, key, value): - self.ec2_backend.create_tags([self.id], {key: value}) - - def add_tags(self, tag_map): - for key, value in tag_map.items(): - self.ec2_backend.create_tags([self.id], {key: value}) - - def get_filter_value(self, filter_name, method_name=None): - tags = self.get_tags() - - if filter_name.startswith("tag:"): - tagname = filter_name.replace("tag:", "", 1) - for tag in tags: - if tag["key"] == tagname: - return tag["value"] - - return None - elif filter_name == "tag-key": - return [tag["key"] for tag in tags] - elif filter_name == "tag-value": - return [tag["value"] for tag in tags] - - value = getattr(self, filter_name.lower().replace("-", "_"), None) - if value is not None: - return value - - raise FilterNotImplementedError(filter_name, method_name) - - class NetworkInterface(TaggedEC2Resource, CloudFormationModel): def __init__( self, @@ -503,6 +469,8 @@ class NetworkInterface(TaggedEC2Resource, CloudFormationModel): return self.description elif filter_name == "attachment.instance-id": return self.instance.id if self.instance else None + elif filter_name == "attachment.instance-owner-id": + return self.owner_id else: return super().get_filter_value(filter_name, "DescribeNetworkInterfaces") @@ -1509,27 +1477,6 @@ class SettingsBackend(object): class TagBackend(object): VALID_TAG_FILTERS = ["key", "resource-id", "resource-type", "value"] - VALID_TAG_RESOURCE_FILTER_TYPES = [ - "customer-gateway", - "dhcp-options", - "image", - "instance", - "internet-gateway", - "network-acl", - "network-interface", - "reserved-instances", - "route-table", - "security-group", - "snapshot", - "spot-instances-request", - "subnet", - "volume", - "vpc", - "vpc-flow-log", - "vpc-peering-connection" "vpn-connection", - "vpn-gateway", - ] - def __init__(self): self.tags = defaultdict(dict) super().__init__() @@ -8667,6 +8614,7 @@ class EC2Backend( FlowLogsBackend, NetworkInterfaceBackend, VPNConnectionBackend, + VPCServiceConfigurationBackend, VPCPeeringConnectionBackend, RouteTableBackend, RouteBackend, @@ -8781,6 +8729,8 @@ class EC2Backend( self.get_volume(volume_id=resource_id) elif resource_prefix == EC2_RESOURCE_TO_PREFIX["vpc"]: self.get_vpc(vpc_id=resource_id) + elif resource_prefix == EC2_RESOURCE_TO_PREFIX["vpc-endpoint-service"]: + self.get_vpc_endpoint_service(resource_id) elif resource_prefix == EC2_RESOURCE_TO_PREFIX["vpc-peering-connection"]: self.get_vpc_peering_connection(vpc_pcx_id=resource_id) elif resource_prefix == EC2_RESOURCE_TO_PREFIX["vpn-connection"]: diff --git a/moto/ec2/responses/__init__.py b/moto/ec2/responses/__init__.py index 2fd67e3de..24be4b8f8 100644 --- a/moto/ec2/responses/__init__.py +++ b/moto/ec2/responses/__init__.py @@ -30,6 +30,7 @@ from .virtual_private_gateways import VirtualPrivateGateways from .vm_export import VMExport from .vm_import import VMImport from .vpcs import VPCs +from .vpc_service_configuration import VPCEndpointServiceConfiguration from .vpc_peering_connections import VPCPeeringConnections from .vpn_connections import VPNConnections from .windows import Windows @@ -74,6 +75,7 @@ class EC2Response( VMExport, VMImport, VPCs, + VPCEndpointServiceConfiguration, VPCPeeringConnections, VPNConnections, Windows, diff --git a/moto/ec2/responses/vpc_service_configuration.py b/moto/ec2/responses/vpc_service_configuration.py new file mode 100644 index 000000000..25f19aadf --- /dev/null +++ b/moto/ec2/responses/vpc_service_configuration.py @@ -0,0 +1,223 @@ +from moto.core.responses import BaseResponse + +from ..exceptions import NoLoadBalancersProvided + + +class VPCEndpointServiceConfiguration(BaseResponse): + def create_vpc_endpoint_service_configuration(self): + gateway_lbs = self._get_multi_param("GatewayLoadBalancerArn") + network_lbs = self._get_multi_param("NetworkLoadBalancerArn") + if not gateway_lbs and not network_lbs: + raise NoLoadBalancersProvided + + tags = self._get_multi_param("TagSpecification") + if tags: + tags = tags[0].get("Tag") + acceptance_required = ( + str(self._get_param("AcceptanceRequired", "true")).lower() == "true" + ) + private_dns_name = self._get_param("PrivateDnsName") + + config = self.ec2_backend.create_vpc_endpoint_service_configuration( + gateway_lbs or network_lbs, + acceptance_required=acceptance_required, + private_dns_name=private_dns_name, + tags=tags, + ) + template = self.response_template(CREATE_VPC_ENDPOINT_SERVICE_CONFIGURATION) + return template.render(config=config) + + def describe_vpc_endpoint_service_configurations(self): + service_ids = self._get_multi_param("ServiceId") + + configs = self.ec2_backend.describe_vpc_endpoint_service_configurations( + service_ids + ) + + template = self.response_template(DESCRIBE_VPC_ENDPOINT_SERVICE_CONFIGURATION) + return template.render(configs=configs) + + def delete_vpc_endpoint_service_configurations(self): + service_ids = self._get_multi_param("ServiceId") + missing_configs = self.ec2_backend.delete_vpc_endpoint_service_configurations( + service_ids + ) + + template = self.response_template(DELETE_VPC_ENDPOINT_SERVICE_CONFIGURATION) + return template.render(missing=missing_configs) + + def describe_vpc_endpoint_service_permissions(self): + service_id = self._get_param("ServiceId") + + principals = self.ec2_backend.describe_vpc_endpoint_service_permissions( + service_id + ) + + template = self.response_template(DESCRIBE_VPC_ENDPOINT_SERVICE_PERMISSIONS) + return template.render(principals=principals) + + def modify_vpc_endpoint_service_configuration(self): + service_id = self._get_param("ServiceId") + private_dns_name = self._get_param("PrivateDnsName") + acceptance_required = self._get_param("AcceptanceRequired") + add_network_lbs = self._get_multi_param("AddNetworkLoadBalancerArn") + remove_network_lbs = self._get_multi_param("RemoveNetworkLoadBalancerArn") + add_gateway_lbs = self._get_multi_param("AddGatewayLoadBalancerArn") + remove_gateway_lbs = self._get_multi_param("RemoveGatewayLoadBalancerArn") + + self.ec2_backend.modify_vpc_endpoint_service_configuration( + service_id, + acceptance_required=acceptance_required, + private_dns_name=private_dns_name, + add_network_lbs=add_network_lbs, + remove_network_lbs=remove_network_lbs, + add_gateway_lbs=add_gateway_lbs, + remove_gateway_lbs=remove_gateway_lbs, + ) + + return MODIFY_VPC_ENDPOINT_SERVICE_CONFIGURATION + + def modify_vpc_endpoint_service_permissions(self): + service_id = self._get_param("ServiceId") + add_principals = self._get_multi_param("AddAllowedPrincipals") + remove_principals = self._get_multi_param("RemoveAllowedPrincipals") + + self.ec2_backend.modify_vpc_endpoint_service_permissions( + service_id, add_principals, remove_principals + ) + + return MODIFY_VPC_ENDPOINT_SERVICE_PERMISSIONS + + +CREATE_VPC_ENDPOINT_SERVICE_CONFIGURATION = """ + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + + {{ config.service_type }} + + {{ config.id }} + {{ config.service_name }} + {{ config.service_state }} + + {% for zone in config.availability_zones %}{{ zone }}{% endfor %} + + {{ 'true' if config.acceptance_required else 'false' }} + {{ 'true' if config.manages_vpc_endpoints else 'false' }} + {%- if config.network_load_balancer_arns %} + + {% for lb in config.network_load_balancer_arns %}{{ lb }}{% endfor %} + + {% endif -%} + {%- if config.gateway_load_balancer_arns %} + + {% for lb in config.gateway_load_balancer_arns %}{{ lb }}{% endfor %} + + {% endif -%} + {{ config.endpoint_dns_name }} + {{ config.private_dns_name }} + + {% if config.private_dns_name %} + verified + TXT + val + n + {% endif %} + + + +""" + + +DESCRIBE_VPC_ENDPOINT_SERVICE_CONFIGURATION = """ + + + {% for config in configs %} + + + {{ config.service_type }} + + {{ config.id }} + {{ config.service_name }} + {{ config.service_state }} + + {% for zone in config.availability_zones %}{{ zone }}{% endfor %} + + {{ 'true' if config.acceptance_required else 'false' }} + {{ 'true' if config.manages_vpc_endpoints else 'false' }} + {%- if config.network_load_balancer_arns %} + + {% for lb in config.network_load_balancer_arns %}{{ lb }}{% endfor %} + + {% endif -%} + {%- if config.gateway_load_balancer_arns %} + + {% for lb in config.gateway_load_balancer_arns %}{{ lb }}{% endfor %} + + {% endif -%} + {{ config.endpoint_dns_name }} + {{ config.private_dns_name }} + + {% if config.private_dns_name %} + verified + TXT + val + n + {% endif %} + + + {% for tag in config.get_tags() %} + + {{ tag.key }} + {{ tag.value }} + + {% endfor %} + + + {% endfor %} + + +""" + + +DELETE_VPC_ENDPOINT_SERVICE_CONFIGURATION = """ + + + {% for m in missing %} + + + InvalidVpcEndpointService.NotFound + The VpcEndpointService Id '{{ m }}' does not exist + + {{ m }} + + {% endfor %} + + +""" + + +DESCRIBE_VPC_ENDPOINT_SERVICE_PERMISSIONS = """ + + + {% for principal in principals %} + + {{ principal }} + + {% endfor %} + + +""" + +MODIFY_VPC_ENDPOINT_SERVICE_PERMISSIONS = """ + +true + +""" + + +MODIFY_VPC_ENDPOINT_SERVICE_CONFIGURATION = """ + +true + +""" diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 7247fe500..35b37e584 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -43,6 +43,7 @@ EC2_RESOURCE_TO_PREFIX = { "volume": "vol", "vpc": "vpc", "vpc-endpoint": "vpce", + "vpc-endpoint-service": "vpce-svc", "managed-prefix-list": "pl", "vpc-cidr-association-id": "vpc-cidr-assoc", "vpc-elastic-ip": "eipalloc", @@ -626,6 +627,8 @@ def get_prefix(resource_id): if resource_id_prefix == EC2_RESOURCE_TO_PREFIX["network-interface"]: if after.startswith("attach"): resource_id_prefix = EC2_RESOURCE_TO_PREFIX["network-interface-attachment"] + if resource_id.startswith(EC2_RESOURCE_TO_PREFIX["vpc-endpoint-service"]): + resource_id_prefix = EC2_RESOURCE_TO_PREFIX["vpc-endpoint-service"] if resource_id_prefix not in EC2_RESOURCE_TO_PREFIX.values(): uuid4hex = re.compile(r"[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z", re.I) if uuid4hex.match(resource_id) is not None: diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index d8883e4b5..d02127aea 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -4,7 +4,7 @@ from jinja2 import Template from botocore.exceptions import ParamValidationError from collections import OrderedDict from moto.core.exceptions import RESTError -from moto.core import BaseBackend, BaseModel, CloudFormationModel +from moto.core import ACCOUNT_ID, BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import ( iso_8601_datetime_with_milliseconds, get_random_hex, @@ -428,6 +428,7 @@ class FakeLoadBalancer(CloudFormationModel): "access_logs.s3.prefix", "deletion_protection.enabled", "idle_timeout.timeout_seconds", + "load_balancing.cross_zone.enabled", "routing.http2.enabled", "routing.http.drop_invalid_header_fields.enabled", } @@ -618,7 +619,7 @@ class ELBv2Backend(BaseBackend): vpc_id = subnets[0].vpc_id arn = make_arn_for_load_balancer( - account_id=1, name=name, region_name=self.region_name + account_id=ACCOUNT_ID, name=name, region_name=self.region_name ) dns_name = "%s-1.%s.elb.amazonaws.com" % (name, self.region_name) @@ -944,7 +945,7 @@ Member must satisfy regular expression pattern: {}".format( ) arn = make_arn_for_target_group( - account_id=1, name=name, region_name=self.region_name + account_id=ACCOUNT_ID, name=name, region_name=self.region_name ) target_group = FakeTargetGroup(name, arn, **kwargs) self.target_groups[target_group.arn] = target_group diff --git a/tests/terraform-tests.success.txt b/tests/terraform-tests.success.txt index 62bd1aef2..f533e2b9e 100644 --- a/tests/terraform-tests.success.txt +++ b/tests/terraform-tests.success.txt @@ -133,3 +133,4 @@ TestAccAWSDataSourceIAMRole TestAccAWSDataSourceIAMUser TestAccAWSIAMAccountAlias TestAccAWSIAMOpenIDConnectProvider +TestAccAWSVpcEndpointService diff --git a/tests/test_ec2/test_elastic_network_interfaces.py b/tests/test_ec2/test_elastic_network_interfaces.py index 236bad670..6209f85df 100644 --- a/tests/test_ec2/test_elastic_network_interfaces.py +++ b/tests/test_ec2/test_elastic_network_interfaces.py @@ -6,6 +6,7 @@ from botocore.exceptions import ClientError import sure # noqa # pylint: disable=unused-import from moto import mock_ec2, settings +from moto.core import ACCOUNT_ID from moto.ec2.utils import random_private_ip from tests import EXAMPLE_AMI_ID from uuid import uuid4 @@ -488,6 +489,38 @@ def test_elastic_network_interfaces_get_by_attachment_instance_id(): enis.get("NetworkInterfaces").should.have.length_of(0) +@mock_ec2 +def test_elastic_network_interfaces_get_by_attachment_instance_owner_id(): + ec2_resource = boto3.resource("ec2", region_name="us-west-2") + ec2_client = boto3.client("ec2", region_name="us-west-2") + + vpc = ec2_resource.create_vpc(CidrBlock="10.0.0.0/16") + subnet = ec2_resource.create_subnet( + VpcId=vpc.id, CidrBlock="10.0.0.0/24", AvailabilityZone="us-west-2a" + ) + + security_group1 = ec2_resource.create_security_group( + GroupName=str(uuid4()), Description="desc" + ) + + create_instances_result = ec2_resource.create_instances( + ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1 + ) + instance = create_instances_result[0] + + eni1 = ec2_resource.create_network_interface( + SubnetId=subnet.id, Groups=[security_group1.id] + ) + ec2_client.attach_network_interface( + NetworkInterfaceId=eni1.id, InstanceId=instance.id, DeviceIndex=1 + ) + + filters = [{"Name": "attachment.instance-owner-id", "Values": [ACCOUNT_ID]}] + enis = ec2_client.describe_network_interfaces(Filters=filters)["NetworkInterfaces"] + eni_ids = [eni["NetworkInterfaceId"] for eni in enis] + eni_ids.should.contain(eni1.id) + + @mock_ec2 def test_elastic_network_interfaces_describe_network_interfaces_with_filter(): ec2 = boto3.resource("ec2", region_name="us-west-2") diff --git a/tests/test_ec2/test_vpc_service_configuration.py b/tests/test_ec2/test_vpc_service_configuration.py new file mode 100644 index 000000000..5192d7a25 --- /dev/null +++ b/tests/test_ec2/test_vpc_service_configuration.py @@ -0,0 +1,397 @@ +import boto3 +import pytest +import sure # noqa # pylint: disable=unused-import + +from botocore.exceptions import ClientError +from moto import mock_ec2, mock_elbv2 +from moto.core.utils import get_random_hex + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html + + +@mock_ec2 +def test_create_vpc_endpoint_service_configuration_without_params(): + client = boto3.client("ec2", region_name="us-west-2") + + with pytest.raises(ClientError) as exc: + client.create_vpc_endpoint_service_configuration() + err = exc.value.response["Error"] + + err["Code"].should.equal("InvalidParameter") + err["Message"].should.equal( + "exactly one of network_load_balancer_arn or gateway_load_balancer_arn is a required member" + ) + + +@mock_ec2 +@mock_elbv2 +def test_create_vpc_endpoint_service_configuration_with_network_load_balancer(): + region_name = "eu-west-3" + client = boto3.client("ec2", region_name=region_name) + + lb_arn = create_load_balancer( + region_name=region_name, lb_type="network", zone="eu-west-3b" + ) + + resp = client.create_vpc_endpoint_service_configuration( + NetworkLoadBalancerArns=[lb_arn] + ) + resp.should.have.key("ServiceConfiguration") + config = resp["ServiceConfiguration"] + + config.should.have.key("ServiceType").equals([{"ServiceType": "Interface"}]) + config.should.have.key("ServiceId").match("^vpce-svc-") + config.should.have.key("ServiceName").equals( + f"com.amazonaws.vpce.eu-west-3.{config['ServiceId']}" + ) + config.should.have.key("ServiceState").equals("Available") + config.should.have.key("AvailabilityZones").equals(["eu-west-3b"]) + config.should.have.key("AcceptanceRequired").equals(True) + config.should.have.key("ManagesVpcEndpoints").equals(False) + config.should.have.key("NetworkLoadBalancerArns").equals([lb_arn]) + config.should.have.key("BaseEndpointDnsNames").equals( + [f"{config['ServiceId']}.eu-west-3.vpce.amazonaws.com"] + ) + config.should.have.key("PrivateDnsNameConfiguration").equals({}) + + config.shouldnt.have.key("GatewayLoadBalancerArns") + + +@mock_ec2 +@mock_elbv2 +def test_create_vpc_endpoint_service_configuration_with_gateway_load_balancer(): + region = "us-east-2" + client = boto3.client("ec2", region_name=region) + + lb_arn = create_load_balancer( + region_name=region, lb_type="gateway", zone="us-east-1c" + ) + + resp = client.create_vpc_endpoint_service_configuration( + GatewayLoadBalancerArns=[lb_arn] + ) + resp.should.have.key("ServiceConfiguration") + config = resp["ServiceConfiguration"] + + config.should.have.key("ServiceType").equals([{"ServiceType": "Gateway"}]) + config.should.have.key("ServiceId").match("^vpce-svc-") + config.should.have.key("ServiceName").equals( + f"com.amazonaws.vpce.us-east-2.{config['ServiceId']}" + ) + config.should.have.key("ServiceState").equals("Available") + config.should.have.key("AvailabilityZones").equals(["us-east-1c"]) + config.should.have.key("AcceptanceRequired").equals(True) + config.should.have.key("ManagesVpcEndpoints").equals(False) + config.should.have.key("GatewayLoadBalancerArns").equals([lb_arn]) + config.should.have.key("BaseEndpointDnsNames").equals( + [f"{config['ServiceId']}.us-east-2.vpce.amazonaws.com"] + ) + config.should.have.key("PrivateDnsNameConfiguration").equals({}) + + config.shouldnt.have.key("NetworkLoadBalancerArns") + + +@mock_ec2 +@mock_elbv2 +def test_create_vpc_endpoint_service_configuration_with_options(): + client = boto3.client("ec2", region_name="us-east-2") + + lb_arn = create_load_balancer( + region_name="us-east-2", lb_type="gateway", zone="us-east-1c" + ) + + resp = client.create_vpc_endpoint_service_configuration( + GatewayLoadBalancerArns=[lb_arn], + AcceptanceRequired=False, + PrivateDnsName="example.com", + ) + resp.should.have.key("ServiceConfiguration") + config = resp["ServiceConfiguration"] + + config.should.have.key("AcceptanceRequired").equals(False) + config.should.have.key("PrivateDnsName").equals("example.com") + config.should.have.key("PrivateDnsNameConfiguration").equals( + {"Name": "n", "State": "verified", "Type": "TXT", "Value": "val"} + ) + + +@mock_ec2 +@mock_elbv2 +def test_describe_vpc_endpoint_service_configurations(): + region = "us-east-2" + client = boto3.client("ec2", region_name=region) + + lb_arn = create_load_balancer( + region_name=region, lb_type="gateway", zone="us-east-1c" + ) + + config1 = client.create_vpc_endpoint_service_configuration( + GatewayLoadBalancerArns=[lb_arn] + )["ServiceConfiguration"]["ServiceId"] + config2 = client.create_vpc_endpoint_service_configuration( + GatewayLoadBalancerArns=[lb_arn] + )["ServiceConfiguration"]["ServiceId"] + + resp = client.describe_vpc_endpoint_service_configurations() + resp.should.have.key("ServiceConfigurations") + service_ids = [s["ServiceId"] for s in resp["ServiceConfigurations"]] + service_ids.should.contain(config1) + service_ids.should.contain(config2) + + resp = client.describe_vpc_endpoint_service_configurations(ServiceIds=[config2]) + + resp.should.have.key("ServiceConfigurations").length_of(1) + result = resp["ServiceConfigurations"][0] + + result.should.have.key("ServiceId").equals(config2) + result.should.have.key("ServiceName") + result.should.have.key("ServiceState") + result.should.have.key("GatewayLoadBalancerArns").equals([lb_arn]) + + +@mock_ec2 +@mock_elbv2 +@pytest.mark.parametrize( + "tags", + [ + [{"Key": "k1", "Value": "v1"}], + [{"Key": "k1", "Value": "v1"}, {"Key": "k2", "Value": "v2"}], + ], +) +def test_describe_vpc_endpoint_service_configurations_with_tags(tags): + region = "us-east-2" + client = boto3.client("ec2", region_name=region) + + lb_arn = create_load_balancer( + region_name=region, lb_type="gateway", zone="us-east-1c" + ) + + service_id = client.create_vpc_endpoint_service_configuration( + GatewayLoadBalancerArns=[lb_arn], + TagSpecifications=[{"ResourceType": "vpc-endpoint-service", "Tags": tags}], + )["ServiceConfiguration"]["ServiceId"] + + resp = client.describe_vpc_endpoint_service_configurations(ServiceIds=[service_id]) + + resp.should.have.key("ServiceConfigurations").length_of(1) + result = resp["ServiceConfigurations"][0] + result.should.have.key("Tags").length_of(len(tags)) + for tag in tags: + result["Tags"].should.contain(tag) + + +@mock_ec2 +@mock_elbv2 +def test_describe_vpc_endpoint_service_configurations_and_add_tags(): + tags = [{"Key": "k1", "Value": "v1"}] + region = "us-east-2" + client = boto3.client("ec2", region_name=region) + + lb_arn = create_load_balancer( + region_name=region, lb_type="gateway", zone="us-east-1c" + ) + + service_id = client.create_vpc_endpoint_service_configuration( + GatewayLoadBalancerArns=[lb_arn] + )["ServiceConfiguration"]["ServiceId"] + + client.create_tags(Resources=[service_id], Tags=tags) + + resp = client.describe_vpc_endpoint_service_configurations(ServiceIds=[service_id]) + + resp.should.have.key("ServiceConfigurations").length_of(1) + result = resp["ServiceConfigurations"][0] + result.should.have.key("Tags").length_of(len(tags)) + for tag in tags: + result["Tags"].should.contain(tag) + + +@mock_ec2 +def test_describe_vpc_endpoint_service_configurations_unknown(): + client = boto3.client("ec2", region_name="eu-west-3") + + with pytest.raises(ClientError) as exc: + # Will always fail if at least one ServiceId is unknown + client.describe_vpc_endpoint_service_configurations( + ServiceIds=["vpce-svc-unknown"] + ) + err = exc.value.response["Error"] + + err["Code"].should.equal("InvalidVpcEndpointServiceId.NotFound") + err["Message"].should.equal( + "The VpcEndpointService Id 'vpce-svc-unknown' does not exist" + ) + + +@mock_ec2 +@mock_elbv2 +def test_delete_vpc_endpoint_service_configurations(): + region = "us-east-2" + client = boto3.client("ec2", region_name=region) + + lb_arn = create_load_balancer( + region_name=region, lb_type="gateway", zone="us-east-1c" + ) + + service_id = client.create_vpc_endpoint_service_configuration( + GatewayLoadBalancerArns=[lb_arn] + )["ServiceConfiguration"]["ServiceId"] + + resp = client.delete_vpc_endpoint_service_configurations(ServiceIds=[service_id]) + resp.should.have.key("Unsuccessful").equals([]) + + +@mock_ec2 +def test_delete_vpc_endpoint_service_configurations_already_deleted(): + client = boto3.client("ec2", region_name="eu-west-3") + + resp = client.delete_vpc_endpoint_service_configurations( + ServiceIds=["vpce-svc-03cf101d15c3bff53"] + ) + resp.should.have.key("Unsuccessful").length_of(1) + + u = resp["Unsuccessful"][0] + u.should.have.key("ResourceId").equals("vpce-svc-03cf101d15c3bff53") + u.should.have.key("Error") + + u["Error"].should.have.key("Code").equals("InvalidVpcEndpointService.NotFound") + u["Error"].should.have.key("Message").equals( + "The VpcEndpointService Id 'vpce-svc-03cf101d15c3bff53' does not exist" + ) + + +@mock_ec2 +@mock_elbv2 +def test_describe_vpc_endpoint_service_permissions(): + region = "us-east-2" + client = boto3.client("ec2", region_name=region) + + lb_arn = create_load_balancer( + region_name=region, lb_type="gateway", zone="us-east-1c" + ) + + service_id = client.create_vpc_endpoint_service_configuration( + GatewayLoadBalancerArns=[lb_arn] + )["ServiceConfiguration"]["ServiceId"] + + resp = client.describe_vpc_endpoint_service_permissions(ServiceId=service_id) + resp.should.have.key("AllowedPrincipals").equals([]) + + +@mock_ec2 +@mock_elbv2 +def test_modify_vpc_endpoint_service_permissions(): + region = "us-east-2" + client = boto3.client("ec2", region_name=region) + + lb_arn = create_load_balancer( + region_name=region, lb_type="gateway", zone="us-east-1c" + ) + + service_id = client.create_vpc_endpoint_service_configuration( + GatewayLoadBalancerArns=[lb_arn] + )["ServiceConfiguration"]["ServiceId"] + + client.modify_vpc_endpoint_service_permissions( + ServiceId=service_id, AddAllowedPrincipals=["prin1", "cipal2"] + ) + + resp = client.describe_vpc_endpoint_service_permissions(ServiceId=service_id) + resp.should.have.key("AllowedPrincipals").length_of(2) + resp["AllowedPrincipals"].should.contain({"Principal": "prin1"}) + resp["AllowedPrincipals"].should.contain({"Principal": "cipal2"}) + + client.modify_vpc_endpoint_service_permissions( + ServiceId=service_id, RemoveAllowedPrincipals=["prin1"] + ) + + resp = client.describe_vpc_endpoint_service_permissions(ServiceId=service_id) + resp.should.have.key("AllowedPrincipals").length_of(1) + resp["AllowedPrincipals"].should.contain({"Principal": "cipal2"}) + + +@mock_ec2 +@mock_elbv2 +def test_modify_vpc_endpoint_service_configuration(): + region = "us-east-2" + client = boto3.client("ec2", region_name=region) + + lb_arn = create_load_balancer( + region_name=region, lb_type="gateway", zone="us-east-1c" + ) + + service_id = client.create_vpc_endpoint_service_configuration( + GatewayLoadBalancerArns=[lb_arn] + )["ServiceConfiguration"]["ServiceId"] + + client.modify_vpc_endpoint_service_configuration( + ServiceId=service_id, PrivateDnsName="dnsname", AcceptanceRequired=False + ) + + config = client.describe_vpc_endpoint_service_configurations( + ServiceIds=[service_id] + )["ServiceConfigurations"][0] + + config.should.have.key("AcceptanceRequired").equals(False) + config.should.have.key("PrivateDnsName").equals("dnsname") + + +@mock_ec2 +@mock_elbv2 +def test_modify_vpc_endpoint_service_configuration_with_new_loadbalancers(): + region = "us-east-2" + client = boto3.client("ec2", region_name=region) + + lb_arn = create_load_balancer( + region_name=region, lb_type="gateway", zone="us-east-1c" + ) + lb_arn2 = create_load_balancer( + region_name=region, lb_type="gateway", zone="us-east-1c" + ) + lb_arn3 = create_load_balancer( + region_name=region, lb_type="network", zone="us-east-1c" + ) + + service_id = client.create_vpc_endpoint_service_configuration( + GatewayLoadBalancerArns=[lb_arn] + )["ServiceConfiguration"]["ServiceId"] + + client.modify_vpc_endpoint_service_configuration( + ServiceId=service_id, + AddNetworkLoadBalancerArns=[lb_arn3], + AddGatewayLoadBalancerArns=[lb_arn2], + ) + + config = client.describe_vpc_endpoint_service_configurations( + ServiceIds=[service_id] + )["ServiceConfigurations"][0] + config["GatewayLoadBalancerArns"].should.equal([lb_arn, lb_arn2]) + config["NetworkLoadBalancerArns"].should.equal([lb_arn3]) + + client.modify_vpc_endpoint_service_configuration( + ServiceId=service_id, + RemoveNetworkLoadBalancerArns=[lb_arn3], + RemoveGatewayLoadBalancerArns=[lb_arn], + ) + + config = client.describe_vpc_endpoint_service_configurations( + ServiceIds=[service_id] + )["ServiceConfigurations"][0] + config["GatewayLoadBalancerArns"].should.equal([lb_arn2]) + config.shouldnt.have.key("NetworkLoadBalancerArns") + + +def create_load_balancer(region_name, zone, lb_type): + ec2 = boto3.resource("ec2", region_name=region_name) + elbv2 = boto3.client("elbv2", region_name=region_name) + + vpc = ec2.create_vpc(CidrBlock="172.28.7.0/24", InstanceTenancy="default") + subnet = ec2.create_subnet( + VpcId=vpc.id, CidrBlock="172.28.7.192/26", AvailabilityZone=zone + ) + lb_name = f"lb_vpce-{get_random_hex(length=10)}" + response = elbv2.create_load_balancer( + Name=lb_name, Subnets=[subnet.id], Scheme="internal", Type=lb_type + ) + return response["LoadBalancers"][0]["LoadBalancerArn"] diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index 037b52805..86303e5c8 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -19,7 +19,7 @@ def test_create_load_balancer(): lb = response.get("LoadBalancers")[0] lb.get("DNSName").should.equal("my-lb-1.us-east-1.elb.amazonaws.com") lb.get("LoadBalancerArn").should.equal( - "arn:aws:elasticloadbalancing:us-east-1:1:loadbalancer/my-lb/50dc6c495c0c9188" + f"arn:aws:elasticloadbalancing:us-east-1:{ACCOUNT_ID}:loadbalancer/my-lb/50dc6c495c0c9188" ) lb.get("SecurityGroups").should.equal([security_group.id]) lb.get("AvailabilityZones").should.equal( @@ -1224,6 +1224,25 @@ def test_modify_load_balancer_attributes_routing_http2_enabled(): routing_http2_enabled["Value"].should.equal("false") +@mock_elbv2 +@mock_ec2 +def test_modify_load_balancer_attributes_crosszone_enabled(): + response, _, _, _, _, client = create_load_balancer() + arn = response["LoadBalancers"][0]["LoadBalancerArn"] + + client.modify_load_balancer_attributes( + LoadBalancerArn=arn, + Attributes=[ + {"Key": "load_balancing.cross_zone.enabled", "Value": "false"}, + {"Key": "deletion_protection.enabled", "Value": "false"}, + ], + ) + + attrs = client.describe_load_balancer_attributes(LoadBalancerArn=arn)["Attributes"] + attrs.should.contain({"Key": "deletion_protection.enabled", "Value": "false"}) + attrs.should.contain({"Key": "load_balancing.cross_zone.enabled", "Value": "false"}) + + @mock_elbv2 @mock_ec2 def test_modify_load_balancer_attributes_routing_http_drop_invalid_header_fields_enabled(): diff --git a/tests/test_elbv2/test_elbv2_target_groups.py b/tests/test_elbv2/test_elbv2_target_groups.py index 2a665ae5b..e7f631f92 100644 --- a/tests/test_elbv2/test_elbv2_target_groups.py +++ b/tests/test_elbv2/test_elbv2_target_groups.py @@ -153,7 +153,7 @@ def test_create_target_group_and_listeners(): e.value.operation_name.should.equal("DeleteTargetGroup") e.value.args.should.equal( ( - "An error occurred (ResourceInUse) when calling the DeleteTargetGroup operation: The target group 'arn:aws:elasticloadbalancing:us-east-1:1:targetgroup/a-target/50dc6c495c0c9188' is currently in use by a listener or a rule", + f"An error occurred (ResourceInUse) when calling the DeleteTargetGroup operation: The target group 'arn:aws:elasticloadbalancing:us-east-1:{ACCOUNT_ID}:targetgroup/a-target/50dc6c495c0c9188' is currently in use by a listener or a rule", ) ) # NOQA