diff --git a/moto/iam/models.py b/moto/iam/models.py index 6a21e0781..1231544cb 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -957,7 +957,9 @@ class InstanceProfile(CloudFormationModel): account_id: str, region_name: str, ) -> None: - iam_backends[account_id]["global"].delete_instance_profile(resource_name) + iam_backends[account_id]["global"].delete_instance_profile( + resource_name, ignore_attached_roles=True + ) def delete_role(self, role_name: str) -> None: self.roles = [role for role in self.roles if role.name != role_name] @@ -2370,9 +2372,11 @@ class IAMBackend(BaseBackend): self.instance_profiles[name] = instance_profile return instance_profile - def delete_instance_profile(self, name: str) -> None: + def delete_instance_profile( + self, name: str, ignore_attached_roles: bool = False + ) -> None: instance_profile = self.get_instance_profile(name) - if len(instance_profile.roles) > 0: + if len(instance_profile.roles) > 0 and not ignore_attached_roles: raise IAMConflictException( code="DeleteConflict", message="Cannot delete entity, must remove roles from instance profile first.", diff --git a/tests/test_iam/__init__.py b/tests/test_iam/__init__.py index 08a1c1568..466103fb0 100644 --- a/tests/test_iam/__init__.py +++ b/tests/test_iam/__init__.py @@ -1 +1,28 @@ -# This file is intentionally left blank. +import os +from functools import wraps + +from moto import mock_aws + + +def iam_aws_verified(func): + """ + Function that is verified to work against AWS. + Can be run against AWS at any time by setting: + MOTO_TEST_ALLOW_AWS_REQUEST=true + + If this environment variable is not set, the function runs in a `mock_aws` context. + """ + + @wraps(func) + def pagination_wrapper(): + allow_aws_request = ( + os.environ.get("MOTO_TEST_ALLOW_AWS_REQUEST", "false").lower() == "true" + ) + + if allow_aws_request: + return func() + else: + with mock_aws(): + return func() + + return pagination_wrapper diff --git a/tests/test_iam/test_iam_cloudformation.py b/tests/test_iam/test_iam_cloudformation.py index aae856d18..2ac97ff82 100644 --- a/tests/test_iam/test_iam_cloudformation.py +++ b/tests/test_iam/test_iam_cloudformation.py @@ -1,4 +1,5 @@ import json +from uuid import uuid4 import boto3 import pytest @@ -8,6 +9,8 @@ from botocore.exceptions import ClientError from moto import mock_aws from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID from tests import EXAMPLE_AMI_ID +from tests.test_iam import iam_aws_verified +from tests.test_iam.test_iam import MOCK_STS_EC2_POLICY_DOCUMENT TEMPLATE_MINIMAL_ROLE = """ AWSTemplateFormatVersion: 2010-09-09 @@ -1609,3 +1612,46 @@ def test_iam_roles(): if resource["ResourceType"] == "AWS::IAM::Role" ] assert {r["PhysicalResourceId"] for r in role_resources} == set(role_names) + + +template_with_instance_profile = """ +Parameters: + InputRole: + Type: String + Default: "test-emr-role" + +Resources: + emrEc2InstanceProfile: + Type: 'AWS::IAM::InstanceProfile' + Properties: + Path: / + Roles: + - !Ref InputRole +""" + + +@iam_aws_verified +def test_delete_instance_profile_with_existing_role(): + region = "us-east-1" + iam = boto3.client("iam", region_name=region) + iam_role_name = f"moto_{str(uuid4())[0:6]}" + iam.create_role( + RoleName=iam_role_name, AssumeRolePolicyDocument=MOCK_STS_EC2_POLICY_DOCUMENT + ) + + try: + cf = boto3.client("cloudformation", region_name=region) + cf.create_stack( + StackName="teststack", + TemplateBody=template_with_instance_profile, + Parameters=[{"ParameterKey": "InputRole", "ParameterValue": iam_role_name}], + Capabilities=["CAPABILITY_NAMED_IAM"], + ) + + # Just verify that we can delete the InstanceProfile + cf.delete_stack(StackName="teststack") + + # The role still exists at this point + iam.get_role(RoleName=iam_role_name) + finally: + iam.delete_role(RoleName=iam_role_name)