Implement IAM instance profile associations (#3482)
* Add associate_iam_instance_profile describe_iam_instance_profile_associations, disassociate_iam_instance_profile, replace_iam_instance_profile_association * More tests, removed type hints, filter fix * Ec2 fix
This commit is contained in:
parent
54e296eb53
commit
689cd8f285
@ -583,3 +583,19 @@ class InvalidParameterDependency(EC2ClientError):
|
|||||||
param, param_needed
|
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),
|
||||||
|
)
|
||||||
|
@ -99,6 +99,8 @@ from .exceptions import (
|
|||||||
RulesPerSecurityGroupLimitExceededError,
|
RulesPerSecurityGroupLimitExceededError,
|
||||||
TagLimitExceeded,
|
TagLimitExceeded,
|
||||||
InvalidParameterDependency,
|
InvalidParameterDependency,
|
||||||
|
IncorrectStateIamProfileAssociationError,
|
||||||
|
InvalidAssociationIDIamProfileAssociationError,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
EC2_RESOURCE_TO_PREFIX,
|
EC2_RESOURCE_TO_PREFIX,
|
||||||
@ -136,6 +138,7 @@ from .utils import (
|
|||||||
random_vpc_id,
|
random_vpc_id,
|
||||||
random_vpc_cidr_association_id,
|
random_vpc_cidr_association_id,
|
||||||
random_vpc_peering_connection_id,
|
random_vpc_peering_connection_id,
|
||||||
|
random_iam_instance_profile_association_id,
|
||||||
generic_filter,
|
generic_filter,
|
||||||
is_valid_resource_id,
|
is_valid_resource_id,
|
||||||
get_prefix,
|
get_prefix,
|
||||||
@ -143,6 +146,8 @@ from .utils import (
|
|||||||
is_valid_cidr,
|
is_valid_cidr,
|
||||||
filter_internet_gateways,
|
filter_internet_gateways,
|
||||||
filter_reservations,
|
filter_reservations,
|
||||||
|
filter_iam_instance_profile_associations,
|
||||||
|
filter_iam_instance_profiles,
|
||||||
random_network_acl_id,
|
random_network_acl_id,
|
||||||
random_network_acl_subnet_association_id,
|
random_network_acl_subnet_association_id,
|
||||||
random_vpn_gateway_id,
|
random_vpn_gateway_id,
|
||||||
@ -674,6 +679,16 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
|
|||||||
instance = reservation.instances[0]
|
instance = reservation.instances[0]
|
||||||
for tag in properties.get("Tags", []):
|
for tag in properties.get("Tags", []):
|
||||||
instance.add_tag(tag["Key"], tag["Value"])
|
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
|
return instance
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -759,6 +774,15 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
|
|||||||
"Client.UserInitiatedShutdown",
|
"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):
|
def reboot(self, *args, **kwargs):
|
||||||
self._state.name = "running"
|
self._state.name = "running"
|
||||||
self._state.code = 16
|
self._state.code = 16
|
||||||
@ -5868,6 +5892,121 @@ class LaunchTemplateBackend(object):
|
|||||||
return generic_filter(filters, templates)
|
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(
|
class EC2Backend(
|
||||||
BaseBackend,
|
BaseBackend,
|
||||||
InstanceBackend,
|
InstanceBackend,
|
||||||
@ -5897,6 +6036,7 @@ class EC2Backend(
|
|||||||
CustomerGatewayBackend,
|
CustomerGatewayBackend,
|
||||||
NatGatewayBackend,
|
NatGatewayBackend,
|
||||||
LaunchTemplateBackend,
|
LaunchTemplateBackend,
|
||||||
|
IamInstanceProfileAssociationBackend,
|
||||||
):
|
):
|
||||||
def __init__(self, region_name):
|
def __init__(self, region_name):
|
||||||
self.region_name = region_name
|
self.region_name = region_name
|
||||||
@ -5983,6 +6123,13 @@ class EC2Backend(
|
|||||||
self.describe_vpn_connections(vpn_connection_ids=[resource_id])
|
self.describe_vpn_connections(vpn_connection_ids=[resource_id])
|
||||||
elif resource_prefix == EC2_RESOURCE_TO_PREFIX["vpn-gateway"]:
|
elif resource_prefix == EC2_RESOURCE_TO_PREFIX["vpn-gateway"]:
|
||||||
self.get_vpn_gateway(vpn_gateway_id=resource_id)
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ from .vpc_peering_connections import VPCPeeringConnections
|
|||||||
from .vpn_connections import VPNConnections
|
from .vpn_connections import VPNConnections
|
||||||
from .windows import Windows
|
from .windows import Windows
|
||||||
from .nat_gateways import NatGateways
|
from .nat_gateways import NatGateways
|
||||||
|
from .iam_instance_profiles import IamInstanceProfiles
|
||||||
|
|
||||||
|
|
||||||
class EC2Response(
|
class EC2Response(
|
||||||
@ -71,6 +72,7 @@ class EC2Response(
|
|||||||
VPNConnections,
|
VPNConnections,
|
||||||
Windows,
|
Windows,
|
||||||
NatGateways,
|
NatGateways,
|
||||||
|
IamInstanceProfiles,
|
||||||
):
|
):
|
||||||
@property
|
@property
|
||||||
def ec2_backend(self):
|
def ec2_backend(self):
|
||||||
|
89
moto/ec2/responses/iam_instance_profiles.py
Normal file
89
moto/ec2/responses/iam_instance_profiles.py
Normal file
@ -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 = """
|
||||||
|
<AssociateIamInstanceProfileResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
|
||||||
|
<requestId>e10deeaf-7cda-48e7-950b-example</requestId>
|
||||||
|
<iamInstanceProfileAssociation>
|
||||||
|
<associationId>{{ iam_association.id }}</associationId>
|
||||||
|
{% if iam_association.iam_instance_profile %}
|
||||||
|
<iamInstanceProfile>
|
||||||
|
<arn>{{ iam_association.iam_instance_profile.arn }}</arn>
|
||||||
|
<id>{{ iam_association.iam_instance_profile.id }}</id>
|
||||||
|
</iamInstanceProfile>
|
||||||
|
{% endif %}
|
||||||
|
<instanceId>{{ iam_association.instance.id }}</instanceId>
|
||||||
|
<state>{{ state }}</state>
|
||||||
|
</iamInstanceProfileAssociation>
|
||||||
|
</AssociateIamInstanceProfileResponse>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# 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 = """
|
||||||
|
<DescribeIamInstanceProfileAssociationsResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
|
||||||
|
<requestId>84c2d2a6-12dc-491f-a9ee-example</requestId>
|
||||||
|
{% if next_token %}<nextToken>{{ next_token }}</nextToken>{% endif %}
|
||||||
|
<iamInstanceProfileAssociationSet>
|
||||||
|
{% for iam_association in iam_associations %}
|
||||||
|
<item>
|
||||||
|
<associationId>{{ iam_association.id }}</associationId>
|
||||||
|
<iamInstanceProfile>
|
||||||
|
<arn>{{ iam_association.iam_instance_profile.arn }}</arn>
|
||||||
|
<id>{{ iam_association.iam_instance_profile.id }}</id>
|
||||||
|
</iamInstanceProfile>
|
||||||
|
<instanceId>{{ iam_association.instance.id }}</instanceId>
|
||||||
|
<state>{{ iam_association.state }}</state>
|
||||||
|
</item>
|
||||||
|
{% endfor %}
|
||||||
|
</iamInstanceProfileAssociationSet>
|
||||||
|
</DescribeIamInstanceProfileAssociationsResponse>
|
||||||
|
"""
|
@ -12,6 +12,7 @@ from cryptography.hazmat.backends import default_backend
|
|||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
|
||||||
from moto.core import ACCOUNT_ID
|
from moto.core import ACCOUNT_ID
|
||||||
|
from moto.iam import iam_backends
|
||||||
|
|
||||||
EC2_RESOURCE_TO_PREFIX = {
|
EC2_RESOURCE_TO_PREFIX = {
|
||||||
"customer-gateway": "cgw",
|
"customer-gateway": "cgw",
|
||||||
@ -43,6 +44,7 @@ EC2_RESOURCE_TO_PREFIX = {
|
|||||||
"vpc-peering-connection": "pcx",
|
"vpc-peering-connection": "pcx",
|
||||||
"vpn-connection": "vpn",
|
"vpn-connection": "vpn",
|
||||||
"vpn-gateway": "vgw",
|
"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)
|
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():
|
def random_public_ip():
|
||||||
return "54.214.{0}.{1}".format(random.choice(range(255)), random.choice(range(255)))
|
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_hex = hashlib.md5(key_data).hexdigest()
|
||||||
fingerprint = re.sub(r"([a-f0-9]{2})(?!$)", r"\1:", fingerprint_hex)
|
fingerprint = re.sub(r"([a-f0-9]{2})(?!$)", r"\1:", fingerprint_hex)
|
||||||
return fingerprint
|
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
|
||||||
|
@ -1852,6 +1852,13 @@ class IAMBackend(BaseBackend):
|
|||||||
"Instance profile {0} not found".format(profile_name)
|
"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):
|
def get_instance_profiles(self):
|
||||||
return self.instance_profiles.values()
|
return self.instance_profiles.values()
|
||||||
|
|
||||||
|
345
tests/test_ec2/test_iam_instance_profile_associations.py
Normal file
345
tests/test_ec2/test_iam_instance_profile_associations.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user