From d33fe9e0861d741aaf41ed17ae0fda699205d808 Mon Sep 17 00:00:00 2001 From: Jordan Sanders Date: Fri, 24 Mar 2023 12:49:03 -0500 Subject: [PATCH] Implement CloudFormation Stack deletion for VPCs, Subnets (#6118) --- moto/ec2/models/subnets.py | 12 +++++ moto/ec2/models/vpcs.py | 12 +++++ .../test_cloudformation_stack_crud_boto3.py | 35 ++++++++++++ tests/test_ec2/test_ec2_cloudformation.py | 53 ++++++++++++++----- 4 files changed, 99 insertions(+), 13 deletions(-) diff --git a/moto/ec2/models/subnets.py b/moto/ec2/models/subnets.py index 0835e8cc8..9b8c0d6b5 100644 --- a/moto/ec2/models/subnets.py +++ b/moto/ec2/models/subnets.py @@ -108,6 +108,18 @@ class Subnet(TaggedEC2Resource, CloudFormationModel): return subnet + @classmethod + def delete_from_cloudformation_json( # type: ignore[misc] + cls, + resource_name: str, + cloudformation_json: Dict[str, Any], + account_id: str, + region_name: str, + ) -> None: + from ..models import ec2_backends + + ec2_backends[account_id][region_name].delete_subnet(resource_name) + @property def available_ip_addresses(self) -> str: enis = [ diff --git a/moto/ec2/models/vpcs.py b/moto/ec2/models/vpcs.py index f56d6fc5f..43407744a 100644 --- a/moto/ec2/models/vpcs.py +++ b/moto/ec2/models/vpcs.py @@ -239,6 +239,18 @@ class VPC(TaggedEC2Resource, CloudFormationModel): return vpc + @classmethod + def delete_from_cloudformation_json( # type: ignore[misc] + cls, + resource_name: str, + cloudformation_json: Dict[str, Any], + account_id: str, + region_name: str, + ) -> None: + from ..models import ec2_backends + + ec2_backends[account_id][region_name].delete_vpc(resource_name) + @property def physical_resource_id(self) -> str: return self.id diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index d6e28a97a..2b274789d 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -1785,13 +1785,48 @@ def test_delete_stack_by_name(): @mock_cloudformation +@mock_ec2 def test_delete_stack(): cf = boto3.client("cloudformation", region_name="us-east-1") + ec2 = boto3.client("ec2", region_name="us-east-1") + cf.create_stack(StackName="test_stack", TemplateBody=dummy_template_json) + state = ec2.describe_instances()["Reservations"][0]["Instances"][0]["State"] cf.delete_stack(StackName="test_stack") stacks = cf.list_stacks() assert stacks["StackSummaries"][0]["StackStatus"] == "DELETE_COMPLETE" + ec2.describe_instances()["Reservations"][0]["Instances"][0]["State"].shouldnt.equal( + state + ) + + +@mock_cloudformation +@mock_ec2 +@pytest.mark.skipif( + settings.TEST_SERVER_MODE, + reason="Can't patch model delete attributes in server mode.", +) +def test_delete_stack_delete_not_implemented(monkeypatch): + monkeypatch.delattr( + "moto.ec2.models.instances.Instance.delete_from_cloudformation_json" + ) + monkeypatch.delattr("moto.ec2.models.instances.Instance.delete") + + cf = boto3.client("cloudformation", region_name="us-east-1") + ec2 = boto3.client("ec2", region_name="us-east-1") + + cf.create_stack(StackName="test_stack", TemplateBody=dummy_template_json) + state = ec2.describe_instances()["Reservations"][0]["Instances"][0]["State"] + + # Mock stack deletion succeeds + cf.delete_stack(StackName="test_stack") + stacks = cf.list_stacks() + assert stacks["StackSummaries"][0]["StackStatus"] == "DELETE_COMPLETE" + # But the underlying resource is untouched + ec2.describe_instances()["Reservations"][0]["Instances"][0]["State"].should.equal( + state + ) @mock_cloudformation diff --git a/tests/test_ec2/test_ec2_cloudformation.py b/tests/test_ec2/test_ec2_cloudformation.py index 7b29f6734..23e5b0b7f 100644 --- a/tests/test_ec2/test_ec2_cloudformation.py +++ b/tests/test_ec2/test_ec2_cloudformation.py @@ -21,6 +21,18 @@ template_vpc = { }, } +template_subnet = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Create VPC", + "Resources": { + "VPC": {"Properties": {"CidrBlock": "192.168.0.0/16"}, "Type": "AWS::EC2::VPC"}, + "Subnet": { + "Properties": {"VpcId": {"Ref": "VPC"}, "CidrBlock": "192.168.0.0/18"}, + "Type": "AWS::EC2::Subnet", + }, + }, +} + @mock_ec2 @mock_cloudformation @@ -84,29 +96,44 @@ def test_vpc_single_instance_in_subnet(): @mock_cloudformation @mock_ec2 -def test_delete_stack_with_resource_missing_delete_attr(): +def test_delete_stack_with_vpc(): cf = boto3.client("cloudformation", region_name="us-east-1") ec2 = boto3.client("ec2", region_name="us-east-1") name = str(uuid4())[0:6] cf.create_stack(StackName=name, TemplateBody=json.dumps(template_vpc)) - cf.describe_stacks(StackName=name)["Stacks"].should.have.length_of(1) - resources = cf.list_stack_resources(StackName=name)["StackResourceSummaries"] vpc_id = resources[0]["PhysicalResourceId"] - cf.delete_stack( - StackName=name - ) # should succeed, despite the fact that the resource itself cannot be deleted - with pytest.raises(ClientError) as exc: - cf.describe_stacks(StackName=name) - err = exc.value.response["Error"] - err.should.have.key("Code").equals("ValidationError") - err.should.have.key("Message").equals(f"Stack with id {name} does not exist") - - # We still have our VPC, as the VPC-object does not have a delete-method yet ec2.describe_vpcs(VpcIds=[vpc_id])["Vpcs"].should.have.length_of(1) + cf.delete_stack(StackName=name) + with pytest.raises(ec2.exceptions.ClientError): + ec2.describe_vpcs(VpcIds=[vpc_id]) + + +@mock_cloudformation +@mock_ec2 +def test_delete_stack_with_subnet(): + cf = boto3.client("cloudformation", region_name="us-east-1") + ec2 = boto3.client("ec2", region_name="us-east-1") + name = str(uuid4())[0:6] + + cf.create_stack(StackName=name, TemplateBody=json.dumps(template_subnet)) + subnet_ids = [ + resource["PhysicalResourceId"] + for resource in cf.list_stack_resources(StackName=name)[ + "StackResourceSummaries" + ] + if resource["ResourceType"] == "AWS::EC2::Subnet" + ] + + ec2.describe_subnets(SubnetIds=subnet_ids)["Subnets"].should.have.length_of(1) + + cf.delete_stack(StackName=name) + with pytest.raises(ec2.exceptions.ClientError): + ec2.describe_subnets(SubnetIds=subnet_ids) + @mock_ec2 @mock_cloudformation