diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index e14a60bf1..348c3f723 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -583,3 +583,19 @@ class InvalidParameterDependency(EC2ClientError): param, param_needed ), ) + + +class IncorrectStateIamProfileAssociationError(EC2ClientError): + def __init__(self, instance_id): + super(IncorrectStateIamProfileAssociationError, self).__init__( + "IncorrectState", + "There is an existing association for instance {0}".format(instance_id), + ) + + +class InvalidAssociationIDIamProfileAssociationError(EC2ClientError): + def __init__(self, association_id): + super(InvalidAssociationIDIamProfileAssociationError, self).__init__( + "InvalidAssociationID.NotFound", + "An invalid association-id of '{0}' was given".format(association_id), + ) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 586f49dcf..9b5e692a7 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -99,6 +99,8 @@ from .exceptions import ( RulesPerSecurityGroupLimitExceededError, TagLimitExceeded, InvalidParameterDependency, + IncorrectStateIamProfileAssociationError, + InvalidAssociationIDIamProfileAssociationError, ) from .utils import ( EC2_RESOURCE_TO_PREFIX, @@ -136,6 +138,7 @@ from .utils import ( random_vpc_id, random_vpc_cidr_association_id, random_vpc_peering_connection_id, + random_iam_instance_profile_association_id, generic_filter, is_valid_resource_id, get_prefix, @@ -143,6 +146,8 @@ from .utils import ( is_valid_cidr, filter_internet_gateways, filter_reservations, + filter_iam_instance_profile_associations, + filter_iam_instance_profiles, random_network_acl_id, random_network_acl_subnet_association_id, random_vpn_gateway_id, @@ -674,6 +679,16 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): instance = reservation.instances[0] for tag in properties.get("Tags", []): instance.add_tag(tag["Key"], tag["Value"]) + + # Associating iam instance profile. + # TODO: Don't forget to implement replace_iam_instance_profile_association once update_from_cloudformation_json + # for ec2 instance will be implemented. + if properties.get("IamInstanceProfile"): + ec2_backend.associate_iam_instance_profile( + instance_id=instance.id, + iam_instance_profile_name=properties.get("IamInstanceProfile"), + ) + return instance @classmethod @@ -759,6 +774,15 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): "Client.UserInitiatedShutdown", ) + # Disassociate iam instance profile if associated, otherwise iam_instance_profile_associations will + # be pointing to None. + if self.ec2_backend.iam_instance_profile_associations.get(self.id): + self.ec2_backend.disassociate_iam_instance_profile( + association_id=self.ec2_backend.iam_instance_profile_associations[ + self.id + ].id + ) + def reboot(self, *args, **kwargs): self._state.name = "running" self._state.code = 16 @@ -5868,6 +5892,121 @@ class LaunchTemplateBackend(object): return generic_filter(filters, templates) +class IamInstanceProfileAssociation(CloudFormationModel): + def __init__(self, ec2_backend, association_id, instance, iam_instance_profile): + self.ec2_backend = ec2_backend + self.id = association_id + self.instance = instance + self.iam_instance_profile = iam_instance_profile + self.state = "associated" + + +class IamInstanceProfileAssociationBackend(object): + def __init__(self): + self.iam_instance_profile_associations = {} + super(IamInstanceProfileAssociationBackend, self).__init__() + + def associate_iam_instance_profile( + self, + instance_id, + iam_instance_profile_name=None, + iam_instance_profile_arn=None, + ): + iam_association_id = random_iam_instance_profile_association_id() + + instance_profile = filter_iam_instance_profiles( + iam_instance_profile_arn, iam_instance_profile_name + ) + + if instance_id in self.iam_instance_profile_associations.keys(): + raise IncorrectStateIamProfileAssociationError(instance_id) + + iam_instance_profile_associations = IamInstanceProfileAssociation( + self, + iam_association_id, + self.get_instance(instance_id) if instance_id else None, + instance_profile, + ) + # Regarding to AWS there can be only one association with ec2. + self.iam_instance_profile_associations[ + instance_id + ] = iam_instance_profile_associations + return iam_instance_profile_associations + + def describe_iam_instance_profile_associations( + self, association_ids, filters=None, max_results=100, next_token=None + ): + associations_list = [] + if association_ids: + for association in self.iam_instance_profile_associations.values(): + if association.id in association_ids: + associations_list.append(association) + else: + # That's mean that no association id were given. Showing all. + associations_list.extend(self.iam_instance_profile_associations.values()) + + associations_list = filter_iam_instance_profile_associations( + associations_list, filters + ) + + starting_point = int(next_token or 0) + ending_point = starting_point + int(max_results or 100) + associations_page = associations_list[starting_point:ending_point] + new_next_token = ( + str(ending_point) if ending_point < len(associations_list) else None + ) + + return associations_page, new_next_token + + def disassociate_iam_instance_profile(self, association_id): + iam_instance_profile_associations = None + for association_key in self.iam_instance_profile_associations.keys(): + if ( + self.iam_instance_profile_associations[association_key].id + == association_id + ): + iam_instance_profile_associations = self.iam_instance_profile_associations[ + association_key + ] + del self.iam_instance_profile_associations[association_key] + # Deleting once and avoiding `RuntimeError: dictionary changed size during iteration` + break + + if not iam_instance_profile_associations: + raise InvalidAssociationIDIamProfileAssociationError(association_id) + + return iam_instance_profile_associations + + def replace_iam_instance_profile_association( + self, + association_id, + iam_instance_profile_name=None, + iam_instance_profile_arn=None, + ): + instance_profile = filter_iam_instance_profiles( + iam_instance_profile_arn, iam_instance_profile_name + ) + + iam_instance_profile_association = None + for association_key in self.iam_instance_profile_associations.keys(): + if ( + self.iam_instance_profile_associations[association_key].id + == association_id + ): + self.iam_instance_profile_associations[ + association_key + ].iam_instance_profile = instance_profile + iam_instance_profile_association = self.iam_instance_profile_associations[ + association_key + ] + break + + if not iam_instance_profile_association: + raise InvalidAssociationIDIamProfileAssociationError(association_id) + + return iam_instance_profile_association + + class EC2Backend( BaseBackend, InstanceBackend, @@ -5897,6 +6036,7 @@ class EC2Backend( CustomerGatewayBackend, NatGatewayBackend, LaunchTemplateBackend, + IamInstanceProfileAssociationBackend, ): def __init__(self, region_name): self.region_name = region_name @@ -5983,6 +6123,13 @@ class EC2Backend( self.describe_vpn_connections(vpn_connection_ids=[resource_id]) elif resource_prefix == EC2_RESOURCE_TO_PREFIX["vpn-gateway"]: self.get_vpn_gateway(vpn_gateway_id=resource_id) + elif ( + resource_prefix + == EC2_RESOURCE_TO_PREFIX["iam-instance-profile-association"] + ): + self.describe_iam_instance_profile_associations( + association_ids=[resource_id] + ) return True diff --git a/moto/ec2/responses/__init__.py b/moto/ec2/responses/__init__.py index 893a25e89..515ae1f31 100644 --- a/moto/ec2/responses/__init__.py +++ b/moto/ec2/responses/__init__.py @@ -34,6 +34,7 @@ from .vpc_peering_connections import VPCPeeringConnections from .vpn_connections import VPNConnections from .windows import Windows from .nat_gateways import NatGateways +from .iam_instance_profiles import IamInstanceProfiles class EC2Response( @@ -71,6 +72,7 @@ class EC2Response( VPNConnections, Windows, NatGateways, + IamInstanceProfiles, ): @property def ec2_backend(self): diff --git a/moto/ec2/responses/iam_instance_profiles.py b/moto/ec2/responses/iam_instance_profiles.py new file mode 100644 index 000000000..3d2525ba7 --- /dev/null +++ b/moto/ec2/responses/iam_instance_profiles.py @@ -0,0 +1,89 @@ +from __future__ import unicode_literals +from moto.core.responses import BaseResponse + + +class IamInstanceProfiles(BaseResponse): + def associate_iam_instance_profile(self): + instance_id = self._get_param("InstanceId") + iam_instance_profile_name = self._get_param("IamInstanceProfile.Name") + iam_instance_profile_arn = self._get_param("IamInstanceProfile.Arn") + iam_association = self.ec2_backend.associate_iam_instance_profile( + instance_id, iam_instance_profile_name, iam_instance_profile_arn + ) + template = self.response_template(IAM_INSTANCE_PROFILE_RESPONSE) + return template.render(iam_association=iam_association, state="associating") + + def describe_iam_instance_profile_associations(self): + association_ids = self._get_multi_param("AssociationId") + filters = self._get_object_map("Filter") + max_items = self._get_param("MaxItems") + next_token = self._get_param("NextToken") + ( + iam_associations, + next_token, + ) = self.ec2_backend.describe_iam_instance_profile_associations( + association_ids, filters, max_items, next_token + ) + template = self.response_template(DESCRIBE_IAM_INSTANCE_PROFILE_RESPONSE) + return template.render(iam_associations=iam_associations, next_token=next_token) + + def disassociate_iam_instance_profile(self): + association_id = self._get_param("AssociationId") + iam_association = self.ec2_backend.disassociate_iam_instance_profile( + association_id + ) + template = self.response_template(IAM_INSTANCE_PROFILE_RESPONSE) + return template.render(iam_association=iam_association, state="disassociating") + + def replace_iam_instance_profile_association(self): + association_id = self._get_param("AssociationId") + iam_instance_profile_name = self._get_param("IamInstanceProfile.Name") + iam_instance_profile_arn = self._get_param("IamInstanceProfile.Arn") + iam_association = self.ec2_backend.replace_iam_instance_profile_association( + association_id, iam_instance_profile_name, iam_instance_profile_arn + ) + template = self.response_template(IAM_INSTANCE_PROFILE_RESPONSE) + return template.render(iam_association=iam_association, state="associating") + + +# https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_AssociateIamInstanceProfile.html +IAM_INSTANCE_PROFILE_RESPONSE = """ + + e10deeaf-7cda-48e7-950b-example + + {{ iam_association.id }} + {% if iam_association.iam_instance_profile %} + + {{ iam_association.iam_instance_profile.arn }} + {{ iam_association.iam_instance_profile.id }} + + {% endif %} + {{ iam_association.instance.id }} + {{ state }} + + +""" + + +# https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeIamInstanceProfileAssociations.html +# Note: this API description page contains an error! Provided `iamInstanceProfileAssociations` doesn't work, you +# should use `iamInstanceProfileAssociationSet` instead. +DESCRIBE_IAM_INSTANCE_PROFILE_RESPONSE = """ + + 84c2d2a6-12dc-491f-a9ee-example + {% if next_token %}{{ next_token }}{% endif %} + + {% for iam_association in iam_associations %} + + {{ iam_association.id }} + + {{ iam_association.iam_instance_profile.arn }} + {{ iam_association.iam_instance_profile.id }} + + {{ iam_association.instance.id }} + {{ iam_association.state }} + + {% endfor %} + + +""" diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index e6763fec1..4a101f923 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -12,6 +12,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa from moto.core import ACCOUNT_ID +from moto.iam import iam_backends EC2_RESOURCE_TO_PREFIX = { "customer-gateway": "cgw", @@ -43,6 +44,7 @@ EC2_RESOURCE_TO_PREFIX = { "vpc-peering-connection": "pcx", "vpn-connection": "vpn", "vpn-gateway": "vgw", + "iam-instance-profile-association": "iip-assoc", } @@ -171,6 +173,10 @@ def random_launch_template_id(): return random_id(prefix=EC2_RESOURCE_TO_PREFIX["launch-template"], size=17) +def random_iam_instance_profile_association_id(): + return random_id(prefix=EC2_RESOURCE_TO_PREFIX["iam-instance-profile-association"]) + + def random_public_ip(): return "54.214.{0}.{1}".format(random.choice(range(255)), random.choice(range(255))) @@ -597,3 +603,47 @@ def rsa_public_key_fingerprint(rsa_public_key): fingerprint_hex = hashlib.md5(key_data).hexdigest() fingerprint = re.sub(r"([a-f0-9]{2})(?!$)", r"\1:", fingerprint_hex) return fingerprint + + +def filter_iam_instance_profile_associations(iam_instance_associations, filter_dict): + if not filter_dict: + return iam_instance_associations + result = [] + for iam_instance_association in iam_instance_associations: + filter_passed = True + if filter_dict.get("instance-id"): + if ( + iam_instance_association.instance.id + not in filter_dict.get("instance-id").values() + ): + filter_passed = False + if filter_dict.get("state"): + if iam_instance_association.state not in filter_dict.get("state").values(): + filter_passed = False + if filter_passed: + result.append(iam_instance_association) + return result + + +def filter_iam_instance_profiles(iam_instance_profile_arn, iam_instance_profile_name): + instance_profile = None + instance_profile_by_name = None + instance_profile_by_arn = None + if iam_instance_profile_name: + instance_profile_by_name = iam_backends["global"].get_instance_profile( + iam_instance_profile_name + ) + instance_profile = instance_profile_by_name + if iam_instance_profile_arn: + instance_profile_by_arn = iam_backends["global"].get_instance_profile_by_arn( + iam_instance_profile_arn + ) + instance_profile = instance_profile_by_arn + # We would prefer instance profile that we found by arn + if iam_instance_profile_arn and iam_instance_profile_name: + if instance_profile_by_name == instance_profile_by_arn: + instance_profile = instance_profile_by_arn + else: + instance_profile = None + + return instance_profile diff --git a/moto/iam/models.py b/moto/iam/models.py index 76b824d60..ac8402e57 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -1852,6 +1852,13 @@ class IAMBackend(BaseBackend): "Instance profile {0} not found".format(profile_name) ) + def get_instance_profile_by_arn(self, profile_arn): + for profile in self.get_instance_profiles(): + if profile.arn == profile_arn: + return profile + + raise IAMNotFoundException("Instance profile {0} not found".format(profile_arn)) + def get_instance_profiles(self): return self.instance_profiles.values() diff --git a/tests/test_ec2/test_iam_instance_profile_associations.py b/tests/test_ec2/test_iam_instance_profile_associations.py new file mode 100644 index 000000000..6a7dcad30 --- /dev/null +++ b/tests/test_ec2/test_iam_instance_profile_associations.py @@ -0,0 +1,345 @@ +from __future__ import unicode_literals + +# Ensure 'pytest.raises' context manager support for Python 2.6 +import pytest + +import time +import json +import boto3 +from botocore.exceptions import ClientError +import sure # noqa + +from moto import mock_ec2, mock_iam, mock_cloudformation + + +def quick_instance_creation(): + image_id = "ami-1234abcd" + conn_ec2 = boto3.resource("ec2", "us-east-1") + test_instance = conn_ec2.create_instances(ImageId=image_id, MinCount=1, MaxCount=1) + # We only need instance id for this tests + return test_instance[0].id + + +def quick_instance_profile_creation(name): + conn_iam = boto3.resource("iam", "us-east-1") + test_instance_profile = conn_iam.create_instance_profile( + InstanceProfileName=name, Path="/" + ) + return test_instance_profile.arn, test_instance_profile.name + + +@mock_ec2 +@mock_iam +def test_associate(): + client = boto3.client("ec2", region_name="us-east-1") + instance_id = quick_instance_creation() + instance_profile_arn, instance_profile_name = quick_instance_profile_creation( + "test_profile" + ) + + association = client.associate_iam_instance_profile( + IamInstanceProfile={ + "Arn": instance_profile_arn, + "Name": instance_profile_name, + }, + InstanceId=instance_id, + ) + association["IamInstanceProfileAssociation"]["InstanceId"].should.equal(instance_id) + association["IamInstanceProfileAssociation"]["IamInstanceProfile"][ + "Arn" + ].should.equal(instance_profile_arn) + association["IamInstanceProfileAssociation"]["State"].should.equal("associating") + + +@mock_ec2 +@mock_iam +def test_invalid_associate(): + client = boto3.client("ec2", region_name="us-east-1") + instance_id = quick_instance_creation() + instance_profile_arn, instance_profile_name = quick_instance_profile_creation( + "test_profile" + ) + + client.associate_iam_instance_profile( + IamInstanceProfile={ + "Arn": instance_profile_arn, + "Name": instance_profile_name, + }, + InstanceId=instance_id, + ) + + # Duplicate + with pytest.raises(ClientError) as ex: + client.associate_iam_instance_profile( + IamInstanceProfile={ + "Arn": instance_profile_arn, + "Name": instance_profile_name, + }, + InstanceId=instance_id, + ) + ex.value.response["Error"]["Code"].should.equal("IncorrectState") + ex.value.response["Error"]["Message"].should.contain( + "There is an existing association for" + ) + + # Wrong instance profile + with pytest.raises(ClientError) as ex: + client.associate_iam_instance_profile( + IamInstanceProfile={"Arn": "fake", "Name": "fake"}, InstanceId=instance_id, + ) + ex.value.response["Error"]["Code"].should.equal("NoSuchEntity") + ex.value.response["Error"]["Message"].should.contain("not found") + + # Wrong instance id + with pytest.raises(ClientError) as ex: + client.associate_iam_instance_profile( + IamInstanceProfile={ + "Arn": instance_profile_arn, + "Name": instance_profile_name, + }, + InstanceId="fake", + ) + ex.value.response["Error"]["Code"].should.equal("InvalidInstanceID.NotFound") + ex.value.response["Error"]["Message"].should.contain("does not exist") + + +@mock_ec2 +@mock_iam +def test_describe(): + client = boto3.client("ec2", region_name="us-east-1") + + instance_id = quick_instance_creation() + instance_profile_arn, instance_profile_name = quick_instance_profile_creation( + "test_profile" + ) + client.associate_iam_instance_profile( + IamInstanceProfile={ + "Arn": instance_profile_arn, + "Name": instance_profile_name, + }, + InstanceId=instance_id, + ) + associations = client.describe_iam_instance_profile_associations() + associations["IamInstanceProfileAssociations"].should.have.length_of(1) + associations["IamInstanceProfileAssociations"][0]["InstanceId"].should.equal( + instance_id + ) + associations["IamInstanceProfileAssociations"][0]["IamInstanceProfile"][ + "Arn" + ].should.equal(instance_profile_arn) + associations["IamInstanceProfileAssociations"][0]["State"].should.equal( + "associated" + ) + + instance_id = quick_instance_creation() + instance_profile_arn, instance_profile_name = quick_instance_profile_creation( + "test_profile1" + ) + client.associate_iam_instance_profile( + IamInstanceProfile={ + "Arn": instance_profile_arn, + "Name": instance_profile_name, + }, + InstanceId=instance_id, + ) + + next_test_associations = client.describe_iam_instance_profile_associations() + next_test_associations["IamInstanceProfileAssociations"].should.have.length_of(2) + + associations = client.describe_iam_instance_profile_associations( + AssociationIds=[ + next_test_associations["IamInstanceProfileAssociations"][0][ + "AssociationId" + ], + ] + ) + associations["IamInstanceProfileAssociations"].should.have.length_of(1) + associations["IamInstanceProfileAssociations"][0]["IamInstanceProfile"][ + "Arn" + ].should.equal( + next_test_associations["IamInstanceProfileAssociations"][0][ + "IamInstanceProfile" + ]["Arn"] + ) + + associations = client.describe_iam_instance_profile_associations( + Filters=[ + { + "Name": "instance-id", + "Values": [ + next_test_associations["IamInstanceProfileAssociations"][0][ + "InstanceId" + ], + ], + }, + {"Name": "state", "Values": ["associated"]}, + ] + ) + associations["IamInstanceProfileAssociations"].should.have.length_of(1) + associations["IamInstanceProfileAssociations"][0]["IamInstanceProfile"][ + "Arn" + ].should.equal( + next_test_associations["IamInstanceProfileAssociations"][0][ + "IamInstanceProfile" + ]["Arn"] + ) + + +@mock_ec2 +@mock_iam +def test_replace(): + client = boto3.client("ec2", region_name="us-east-1") + instance_id1 = quick_instance_creation() + instance_profile_arn1, instance_profile_name1 = quick_instance_profile_creation( + "test_profile1" + ) + instance_profile_arn2, instance_profile_name2 = quick_instance_profile_creation( + "test_profile2" + ) + + association = client.associate_iam_instance_profile( + IamInstanceProfile={ + "Arn": instance_profile_arn1, + "Name": instance_profile_name1, + }, + InstanceId=instance_id1, + ) + + association = client.replace_iam_instance_profile_association( + IamInstanceProfile={ + "Arn": instance_profile_arn2, + "Name": instance_profile_name2, + }, + AssociationId=association["IamInstanceProfileAssociation"]["AssociationId"], + ) + + association["IamInstanceProfileAssociation"]["IamInstanceProfile"][ + "Arn" + ].should.equal(instance_profile_arn2) + association["IamInstanceProfileAssociation"]["State"].should.equal("associating") + + +@mock_ec2 +@mock_iam +def test_invalid_replace(): + client = boto3.client("ec2", region_name="us-east-1") + instance_id = quick_instance_creation() + instance_profile_arn, instance_profile_name = quick_instance_profile_creation( + "test_profile" + ) + instance_profile_arn2, instance_profile_name2 = quick_instance_profile_creation( + "test_profile2" + ) + + association = client.associate_iam_instance_profile( + IamInstanceProfile={ + "Arn": instance_profile_arn, + "Name": instance_profile_name, + }, + InstanceId=instance_id, + ) + + # Wrong id + with pytest.raises(ClientError) as ex: + client.replace_iam_instance_profile_association( + IamInstanceProfile={ + "Arn": instance_profile_arn2, + "Name": instance_profile_name2, + }, + AssociationId="fake", + ) + ex.value.response["Error"]["Code"].should.equal("InvalidAssociationID.NotFound") + ex.value.response["Error"]["Message"].should.contain("An invalid association-id of") + + # Wrong instance profile + with pytest.raises(ClientError) as ex: + client.replace_iam_instance_profile_association( + IamInstanceProfile={"Arn": "fake", "Name": "fake",}, + AssociationId=association["IamInstanceProfileAssociation"]["AssociationId"], + ) + ex.value.response["Error"]["Code"].should.equal("NoSuchEntity") + ex.value.response["Error"]["Message"].should.contain("not found") + + +@mock_ec2 +@mock_iam +def test_disassociate(): + client = boto3.client("ec2", region_name="us-east-1") + instance_id = quick_instance_creation() + instance_profile_arn, instance_profile_name = quick_instance_profile_creation( + "test_profile" + ) + + association = client.associate_iam_instance_profile( + IamInstanceProfile={ + "Arn": instance_profile_arn, + "Name": instance_profile_name, + }, + InstanceId=instance_id, + ) + + associations = client.describe_iam_instance_profile_associations() + associations["IamInstanceProfileAssociations"].should.have.length_of(1) + + disassociation = client.disassociate_iam_instance_profile( + AssociationId=association["IamInstanceProfileAssociation"]["AssociationId"], + ) + + disassociation["IamInstanceProfileAssociation"]["IamInstanceProfile"][ + "Arn" + ].should.equal(instance_profile_arn) + disassociation["IamInstanceProfileAssociation"]["State"].should.equal( + "disassociating" + ) + + associations = client.describe_iam_instance_profile_associations() + associations["IamInstanceProfileAssociations"].should.have.length_of(0) + + +@mock_ec2 +@mock_iam +def test_invalid_disassociate(): + client = boto3.client("ec2", region_name="us-east-1") + + # Wrong id + with pytest.raises(ClientError) as ex: + client.disassociate_iam_instance_profile(AssociationId="fake",) + ex.value.response["Error"]["Code"].should.equal("InvalidAssociationID.NotFound") + ex.value.response["Error"]["Message"].should.contain("An invalid association-id of") + + +@mock_ec2 +@mock_cloudformation +def test_cloudformation(): + dummy_template_json = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "InstanceProfile": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": {"Path": "/", "Roles": []}, + }, + "Ec2Instance": { + "Type": "AWS::EC2::Instance", + "Properties": { + "IamInstanceProfile": {"Ref": "InstanceProfile"}, + "KeyName": "mykey1", + "ImageId": "ami-7a11e213", + }, + }, + }, + } + + client = boto3.client("ec2", region_name="us-east-1") + cf_conn = boto3.client("cloudformation", region_name="us-east-1") + cf_conn.create_stack( + StackName="test_stack", TemplateBody=json.dumps(dummy_template_json) + ) + associations = client.describe_iam_instance_profile_associations() + associations["IamInstanceProfileAssociations"].should.have.length_of(1) + associations["IamInstanceProfileAssociations"][0]["IamInstanceProfile"][ + "Arn" + ].should.contain("test_stack") + + cf_conn.delete_stack(StackName="test_stack") + associations = client.describe_iam_instance_profile_associations() + associations["IamInstanceProfileAssociations"].should.have.length_of(0)