diff --git a/localstack-core/localstack/services/ec2/provider.py b/localstack-core/localstack/services/ec2/provider.py index 59a560cd7295e..401276a0da7bd 100644 --- a/localstack-core/localstack/services/ec2/provider.py +++ b/localstack-core/localstack/services/ec2/provider.py @@ -50,6 +50,8 @@ DnsOptionsSpecification, DnsRecordIpType, Ec2Api, + GetSecurityGroupsForVpcRequest, + GetSecurityGroupsForVpcResult, InstanceType, IpAddressType, LaunchTemplate, @@ -70,6 +72,7 @@ RevokeSecurityGroupEgressRequest, RevokeSecurityGroupEgressResult, RIProductDescription, + SecurityGroupForVpc, String, SubnetConfigurationsList, Tenancy, @@ -539,6 +542,30 @@ def create_flow_logs( return response + @handler("GetSecurityGroupsForVpc", expand=False) + def get_security_groups_for_vpc( + self, + context: RequestContext, + get_security_groups_for_vpc_request: GetSecurityGroupsForVpcRequest, + ) -> GetSecurityGroupsForVpcResult: + vpc_id = get_security_groups_for_vpc_request.get("VpcId") + backend = get_ec2_backend(context.account_id, context.region) + filters = {"vpc-id": [vpc_id]} + filtered_sgs = backend.describe_security_groups(filters=filters) + + sgs = [ + SecurityGroupForVpc( + Description=sg.description, + GroupId=sg.id, + GroupName=sg.name, + OwnerId=context.account_id, + PrimaryVpcId=sg.vpc_id, + Tags=[{"Key": tag.get("key"), "Value": tag.get("value")} for tag in sg.get_tags()], + ) + for sg in filtered_sgs + ] + return GetSecurityGroupsForVpcResult(SecurityGroupForVpcs=sgs, NextToken=None) + @patch(SubnetBackend.modify_subnet_attribute) def modify_subnet_attribute(fn, self, subnet_id, attr_name, attr_value): diff --git a/localstack-core/localstack/testing/pytest/fixtures.py b/localstack-core/localstack/testing/pytest/fixtures.py index 6bddcb162632f..b89d5aedf2a87 100644 --- a/localstack-core/localstack/testing/pytest/fixtures.py +++ b/localstack-core/localstack/testing/pytest/fixtures.py @@ -2009,11 +2009,16 @@ def factory(ports=None, ip_protocol: str = "tcp", **kwargs): :param ip_protocol: the ip protocol for the permissions (tcp by default) """ if "GroupName" not in kwargs: + # FIXME: This will fail against AWS since the sg prefix is not valid for GroupName + # > "Group names may not be in the format sg-*". kwargs["GroupName"] = f"sg-{short_uid()}" # Making sure the call to CreateSecurityGroup gets the right arguments _args = select_from_typed_dict(CreateSecurityGroupRequest, kwargs) security_group = aws_client.ec2.create_security_group(**_args) security_group_id = security_group["GroupId"] + + # FIXME: If 'ports' is None or an empty list, authorize_security_group_ingress will fail due to missing IpPermissions. + # Must ensure ports are explicitly provided or skip authorization entirely if not required. permissions = [ { "FromPort": port, diff --git a/tests/aws/services/ec2/test_ec2.py b/tests/aws/services/ec2/test_ec2.py index f0ba136034454..cd006fa90abb7 100644 --- a/tests/aws/services/ec2/test_ec2.py +++ b/tests/aws/services/ec2/test_ec2.py @@ -3,6 +3,7 @@ import pytest from botocore.exceptions import ClientError +from localstack_snapshot.snapshots.transformer import SortingTransformer from moto.ec2 import ec2_backends from moto.ec2.utils import ( random_security_group_id, @@ -694,6 +695,71 @@ def _create_security_group() -> dict: assert e.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400 assert e.value.response["Error"]["Code"] == "InvalidSecurityGroupId.DuplicateCustomId" + @markers.snapshot.skip_snapshot_verify( + paths=[ + "$..Tags", # Tags can differ between environments + "$..Vpc.IsDefault", # TODO: CreateVPC should return an IsDefault param + "$..Vpc.DhcpOptionsId", # FIXME: DhcpOptionsId uses different reference formats in AWS vs LocalStack + ] + ) + @markers.aws.validated + def test_get_security_groups_for_vpc( + self, snapshot, aws_client, create_vpc, ec2_create_security_group + ): + group_name = f"test-security-group-{short_uid()}" + group_description = f"Description for {group_name}" + + # Returned security groups appear to be sorted by the randomly generated GroupId field, + # so we should sort snapshots by this value to mitigate flakiness for runs against AWS. + snapshot.add_transformer( + SortingTransformer("SecurityGroupForVpcs", lambda x: x["GroupName"]) + ) + snapshot.add_transformer(snapshot.transform.key_value("GroupId")) + snapshot.add_transformer(snapshot.transform.key_value("GroupName")) + snapshot.add_transformer(snapshot.transform.key_value("VpcId")) + snapshot.add_transformer(snapshot.transform.key_value("AssociationId")) + snapshot.add_transformer(snapshot.transform.key_value("DhcpOptionsId")) + + # Create VPC for testing + vpc: dict = create_vpc( + cidr_block="10.0.0.0/16", + tag_specifications=[ + { + "ResourceType": "vpc", + "Tags": [ + {"Key": "test-key", "Value": "test-value"}, + ], + } + ], + ) + vpc_id: str = vpc["Vpc"]["VpcId"] + snapshot.match("create_vpc_response", vpc) + + # Wait to ensure VPC is available + waiter = aws_client.ec2.get_waiter("vpc_available") + waiter.wait(VpcIds=[vpc_id]) + + # Get all security groups in the VPC + get_security_groups_for_vpc = aws_client.ec2.get_security_groups_for_vpc(VpcId=vpc_id) + snapshot.match("get_security_groups_for_vpc", get_security_groups_for_vpc) + + # Create new security group in the VPC + create_security_group = ec2_create_security_group( + GroupName=group_name, + Description=group_description, + VpcId=vpc_id, + ports=[22], # TODO: Handle port issues in the fixture + ) + snapshot.match("create_security_group", create_security_group) + + # Ensure new security group is in the VPC + get_security_groups_for_vpc_after_addition = aws_client.ec2.get_security_groups_for_vpc( + VpcId=vpc_id + ) + snapshot.match( + "get_security_groups_for_vpc_after_addition", get_security_groups_for_vpc_after_addition + ) + @markers.snapshot.skip_snapshot_verify( # Moto and LS do not return the ClientToken diff --git a/tests/aws/services/ec2/test_ec2.snapshot.json b/tests/aws/services/ec2/test_ec2.snapshot.json index 3347bf78d1bdb..c76d1b6969e9a 100644 --- a/tests/aws/services/ec2/test_ec2.snapshot.json +++ b/tests/aws/services/ec2/test_ec2.snapshot.json @@ -335,5 +335,89 @@ } } } + }, + "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_get_security_groups_for_vpc": { + "recorded-date": "19-05-2025, 13:53:56", + "recorded-content": { + "create_vpc_response": { + "Vpc": { + "CidrBlock": "10.0.0.0/16", + "CidrBlockAssociationSet": [ + { + "AssociationId": "", + "CidrBlock": "10.0.0.0/16", + "CidrBlockState": { + "State": "associated" + } + } + ], + "DhcpOptionsId": "", + "InstanceTenancy": "", + "Ipv6CidrBlockAssociationSet": [], + "IsDefault": false, + "OwnerId": "111111111111", + "State": "pending", + "Tags": [ + { + "Key": "test-key", + "Value": "test-value" + } + ], + "VpcId": "" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "get_security_groups_for_vpc": { + "SecurityGroupForVpcs": [ + { + "Description": " VPC security group", + "GroupId": "", + "GroupName": "", + "OwnerId": "111111111111", + "PrimaryVpcId": "", + "Tags": [] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create_security_group": { + "GroupId": "", + "SecurityGroupArn": "arn::ec2::111111111111:security-group/", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "get_security_groups_for_vpc_after_addition": { + "SecurityGroupForVpcs": [ + { + "Description": " VPC security group", + "GroupId": "", + "GroupName": "", + "OwnerId": "111111111111", + "PrimaryVpcId": "", + "Tags": [] + }, + { + "Description": "Description for ", + "GroupId": "", + "GroupName": "", + "OwnerId": "111111111111", + "PrimaryVpcId": "", + "Tags": [] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/ec2/test_ec2.validation.json b/tests/aws/services/ec2/test_ec2.validation.json index 2a599a8011508..19e617efc962a 100644 --- a/tests/aws/services/ec2/test_ec2.validation.json +++ b/tests/aws/services/ec2/test_ec2.validation.json @@ -11,6 +11,9 @@ "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_describe_vpn_gateways_filter_by_vpc": { "last_validated_date": "2024-06-07T01:11:12+00:00" }, + "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_get_security_groups_for_vpc": { + "last_validated_date": "2025-05-19T13:54:09+00:00" + }, "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_vcp_peering_difference_regions": { "last_validated_date": "2024-06-07T21:28:25+00:00" },