Repository: getmoto/moto

Issue:
Feature Request: IAM: Delete InstanceProfile and Role via CF
Both the InstanceProfile and Roles cannot be deleted via CloudFormation at the moment.

Needed to do some debugging for another issue when this popped up, so the simplest implementation would be:

In class InstanceProfile:
```
@classmethod
def delete_from_cloudformation_json(
    cls, resource_name, cloudformation_json, region_name
):
    iam_backend.delete_instance_profile(resource_name)

def delete_role(self, role_name):
    self.roles = [role for role in self.roles if role.name != role_name]
```

In class Role:
```
@classmethod
def delete_from_cloudformation_json(
    cls, resource_name, cloudformation_json, region_name
):
    role_name = cloudformation_json["Properties"]["RoleName"]
    for profile_name, profile in iam_backend.instance_profiles.items():
        profile.delete_role(role_name=role_name)

    iam_backend.delete_role(role_name)
```




Discussion:

Resolving patch:
diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -347,8 +347,9 @@ def parse_and_update_resource(logical_id, resource_json, resources_map, region_n
         return None
 
 
-def parse_and_delete_resource(resource_name, resource_json, resources_map, region_name):
-    resource_class, resource_json, _ = parse_resource(resource_json, resources_map)
+def parse_and_delete_resource(resource_name, resource_json, region_name):
+    resource_type = resource_json["Type"]
+    resource_class = resource_class_from_type(resource_type)
     if not hasattr(
         resource_class.delete_from_cloudformation_json, "__isabstractmethod__"
     ):
@@ -663,9 +664,7 @@ def update(self, template, parameters=None):
                 ].physical_resource_id
             else:
                 resource_name = None
-            parse_and_delete_resource(
-                resource_name, resource_json, self, self._region_name
-            )
+            parse_and_delete_resource(resource_name, resource_json, self._region_name)
             self._parsed_resources.pop(logical_name)
 
         self._template = template
@@ -726,7 +725,7 @@ def delete(self):
                             ]
 
                             parse_and_delete_resource(
-                                resource_name, resource_json, self, self._region_name,
+                                resource_name, resource_json, self._region_name,
                             )
 
                         self._parsed_resources.pop(parsed_resource.logical_resource_id)
diff --git a/moto/iam/models.py b/moto/iam/models.py
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -598,6 +598,19 @@ def create_from_cloudformation_json(
 
         return role
 
+    @classmethod
+    def delete_from_cloudformation_json(
+        cls, resource_name, cloudformation_json, region_name
+    ):
+        for profile_name, profile in iam_backend.instance_profiles.items():
+            profile.delete_role(role_name=resource_name)
+
+        for _, role in iam_backend.roles.items():
+            if role.name == resource_name:
+                for arn, policy in role.policies.items():
+                    role.delete_policy(arn)
+        iam_backend.delete_role(resource_name)
+
     @property
     def arn(self):
         return "arn:aws:iam::{0}:role{1}{2}".format(ACCOUNT_ID, self.path, self.name)
@@ -678,7 +691,7 @@ def delete_policy(self, policy_name):
 
     @property
     def physical_resource_id(self):
-        return self.id
+        return self.name
 
     def get_cfn_attribute(self, attribute_name):
         from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
@@ -725,13 +738,22 @@ def create_from_cloudformation_json(
     ):
         properties = cloudformation_json["Properties"]
 
-        role_ids = properties["Roles"]
+        role_names = properties["Roles"]
         return iam_backend.create_instance_profile(
             name=resource_physical_name,
             path=properties.get("Path", "/"),
-            role_ids=role_ids,
+            role_names=role_names,
         )
 
+    @classmethod
+    def delete_from_cloudformation_json(
+        cls, resource_name, cloudformation_json, region_name
+    ):
+        iam_backend.delete_instance_profile(resource_name)
+
+    def delete_role(self, role_name):
+        self.roles = [role for role in self.roles if role.name != role_name]
+
     @property
     def arn(self):
         return "arn:aws:iam::{0}:instance-profile{1}{2}".format(
@@ -1878,7 +1900,7 @@ def delete_policy_version(self, policy_arn, version_id):
                 return
         raise IAMNotFoundException("Policy not found")
 
-    def create_instance_profile(self, name, path, role_ids, tags=None):
+    def create_instance_profile(self, name, path, role_names, tags=None):
         if self.instance_profiles.get(name):
             raise IAMConflictException(
                 code="EntityAlreadyExists",
@@ -1887,7 +1909,7 @@ def create_instance_profile(self, name, path, role_ids, tags=None):
 
         instance_profile_id = random_resource_id()
 
-        roles = [iam_backend.get_role_by_id(role_id) for role_id in role_ids]
+        roles = [iam_backend.get_role(role_name) for role_name in role_names]
         instance_profile = InstanceProfile(instance_profile_id, name, path, roles, tags)
         self.instance_profiles[name] = instance_profile
         return instance_profile
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -324,7 +324,7 @@ def create_instance_profile(self):
         tags = self._get_multi_param("Tags.member")
 
         profile = iam_backend.create_instance_profile(
-            profile_name, path, role_ids=[], tags=tags
+            profile_name, path, role_names=[], tags=tags
         )
         template = self.response_template(CREATE_INSTANCE_PROFILE_TEMPLATE)
         return template.render(profile=profile)


Test patch:
diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py
--- a/tests/test_cloudformation/test_cloudformation_stack_integration.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py
@@ -948,6 +948,7 @@ def test_iam_roles():
         "roles"
     ]
     role_name_to_id = {}
+    role_names = []
     for role_result in role_results:
         role = iam_conn.get_role(role_result.role_name)
         # Role name is not specified, so randomly generated - can't check exact name
@@ -958,6 +959,7 @@ def test_iam_roles():
             role_name_to_id["no-path"] = role.role_id
             role.role_name.should.equal("my-role-no-path-name")
             role.path.should.equal("/")
+        role_names.append(role.role_name)
 
     instance_profile_responses = iam_conn.list_instance_profiles()[
         "list_instance_profiles_response"
@@ -997,9 +999,7 @@ def test_iam_roles():
     role_resources = [
         resource for resource in resources if resource.resource_type == "AWS::IAM::Role"
     ]
-    {r.physical_resource_id for r in role_resources}.should.equal(
-        set(role_name_to_id.values())
-    )
+    {r.physical_resource_id for r in role_resources}.should.equal(set(role_names))
 
 
 @mock_ec2_deprecated()
diff --git a/tests/test_iam/test_iam_cloudformation.py b/tests/test_iam/test_iam_cloudformation.py
--- a/tests/test_iam/test_iam_cloudformation.py
+++ b/tests/test_iam/test_iam_cloudformation.py
@@ -8,6 +8,58 @@
 from moto import mock_iam, mock_cloudformation, mock_s3, mock_sts
 from moto.core import ACCOUNT_ID
 
+
+TEMPLATE_MINIMAL_ROLE = """
+AWSTemplateFormatVersion: 2010-09-09
+Resources:
+  RootRole:
+    Type: 'AWS::IAM::Role'
+    Properties:
+      AssumeRolePolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service:
+              - ec2.amazonaws.com
+            Action:
+              - 'sts:AssumeRole'
+"""
+
+
+TEMPLATE_ROLE_INSTANCE_PROFILE = """
+AWSTemplateFormatVersion: 2010-09-09
+Resources:
+  RootRole:
+    Type: 'AWS::IAM::Role'
+    Properties:
+      RoleName: {0}
+      AssumeRolePolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service:
+              - ec2.amazonaws.com
+            Action:
+              - 'sts:AssumeRole'
+      Path: /
+      Policies:
+        - PolicyName: root
+          PolicyDocument:
+            Version: 2012-10-17
+            Statement:
+              - Effect: Allow
+                Action: '*'
+                Resource: '*'
+  RootInstanceProfile:
+    Type: 'AWS::IAM::InstanceProfile'
+    Properties:
+      Path: /
+      Roles:
+        - !Ref RootRole
+"""
+
 # AWS::IAM::User Tests
 @mock_iam
 @mock_cloudformation
@@ -1384,3 +1436,62 @@ def test_iam_cloudformation_update_users_access_key_replacement():
 
     access_keys = iam_client.list_access_keys(UserName=other_user_name)
     access_key_id.should_not.equal(access_keys["AccessKeyMetadata"][0]["AccessKeyId"])
+
+
+@mock_iam
+@mock_cloudformation
+def test_iam_cloudformation_create_role():
+    cf_client = boto3.client("cloudformation", region_name="us-east-1")
+
+    stack_name = "MyStack"
+
+    template = TEMPLATE_MINIMAL_ROLE.strip()
+    cf_client.create_stack(StackName=stack_name, TemplateBody=template)
+
+    resources = cf_client.list_stack_resources(StackName=stack_name)[
+        "StackResourceSummaries"
+    ]
+    role = [res for res in resources if res["ResourceType"] == "AWS::IAM::Role"][0]
+    role["LogicalResourceId"].should.equal("RootRole")
+    role_name = role["PhysicalResourceId"]
+
+    iam_client = boto3.client("iam", region_name="us-east-1")
+    iam_client.list_roles()["Roles"].should.have.length_of(1)
+
+    cf_client.delete_stack(StackName=stack_name)
+
+    iam_client.list_roles()["Roles"].should.have.length_of(0)
+
+
+@mock_iam
+@mock_cloudformation
+def test_iam_cloudformation_create_role_and_instance_profile():
+    cf_client = boto3.client("cloudformation", region_name="us-east-1")
+
+    stack_name = "MyStack"
+    role_name = "MyUser"
+
+    template = TEMPLATE_ROLE_INSTANCE_PROFILE.strip().format(role_name)
+    cf_client.create_stack(StackName=stack_name, TemplateBody=template)
+
+    resources = cf_client.list_stack_resources(StackName=stack_name)[
+        "StackResourceSummaries"
+    ]
+    role = [res for res in resources if res["ResourceType"] == "AWS::IAM::Role"][0]
+    role["LogicalResourceId"].should.equal("RootRole")
+    role["PhysicalResourceId"].should.equal(role_name)
+    profile = [
+        res for res in resources if res["ResourceType"] == "AWS::IAM::InstanceProfile"
+    ][0]
+    profile["LogicalResourceId"].should.equal("RootInstanceProfile")
+    profile["PhysicalResourceId"].should.contain(
+        stack_name
+    )  # e.g. MyStack-RootInstanceProfile-73Y4H4ALFW3N
+    profile["PhysicalResourceId"].should.contain("RootInstanceProfile")
+
+    iam_client = boto3.client("iam", region_name="us-east-1")
+    iam_client.list_roles()["Roles"].should.have.length_of(1)
+
+    cf_client.delete_stack(StackName=stack_name)
+
+    iam_client.list_roles()["Roles"].should.have.length_of(0)