Merge branch 'master' into master

This commit is contained in:
John Corrales 2019-02-11 17:51:18 -08:00 committed by GitHub
commit a6f7f06c30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1015 additions and 95 deletions

View File

@ -477,12 +477,12 @@
- [X] create_stack - [X] create_stack
- [X] create_stack_instances - [X] create_stack_instances
- [X] create_stack_set - [X] create_stack_set
- [ ] delete_change_set - [X] delete_change_set
- [X] delete_stack - [X] delete_stack
- [X] delete_stack_instances - [X] delete_stack_instances
- [X] delete_stack_set - [X] delete_stack_set
- [ ] describe_account_limits - [ ] describe_account_limits
- [ ] describe_change_set - [X] describe_change_set
- [ ] describe_stack_events - [ ] describe_stack_events
- [X] describe_stack_instance - [X] describe_stack_instance
- [ ] describe_stack_resource - [ ] describe_stack_resource
@ -495,7 +495,7 @@
- [ ] get_stack_policy - [ ] get_stack_policy
- [ ] get_template - [ ] get_template
- [ ] get_template_summary - [ ] get_template_summary
- [ ] list_change_sets - [X] list_change_sets
- [X] list_exports - [X] list_exports
- [ ] list_imports - [ ] list_imports
- [X] list_stack_instances - [X] list_stack_instances
@ -2208,7 +2208,7 @@
- [ ] describe_event_types - [ ] describe_event_types
- [ ] describe_events - [ ] describe_events
## iam - 48% implemented ## iam - 62% implemented
- [ ] add_client_id_to_open_id_connect_provider - [ ] add_client_id_to_open_id_connect_provider
- [X] add_role_to_instance_profile - [X] add_role_to_instance_profile
- [X] add_user_to_group - [X] add_user_to_group
@ -2247,7 +2247,7 @@
- [X] delete_server_certificate - [X] delete_server_certificate
- [ ] delete_service_linked_role - [ ] delete_service_linked_role
- [ ] delete_service_specific_credential - [ ] delete_service_specific_credential
- [ ] delete_signing_certificate - [X] delete_signing_certificate
- [ ] delete_ssh_public_key - [ ] delete_ssh_public_key
- [X] delete_user - [X] delete_user
- [X] delete_user_policy - [X] delete_user_policy
@ -2279,7 +2279,7 @@
- [ ] get_ssh_public_key - [ ] get_ssh_public_key
- [X] get_user - [X] get_user
- [X] get_user_policy - [X] get_user_policy
- [ ] list_access_keys - [X] list_access_keys
- [X] list_account_aliases - [X] list_account_aliases
- [X] list_attached_group_policies - [X] list_attached_group_policies
- [X] list_attached_role_policies - [X] list_attached_role_policies
@ -2287,19 +2287,21 @@
- [ ] list_entities_for_policy - [ ] list_entities_for_policy
- [X] list_group_policies - [X] list_group_policies
- [X] list_groups - [X] list_groups
- [ ] list_groups_for_user - [X] list_groups_for_user
- [ ] list_instance_profiles - [X] list_instance_profiles
- [ ] list_instance_profiles_for_role - [X] list_instance_profiles_for_role
- [X] list_mfa_devices - [X] list_mfa_devices
- [ ] list_open_id_connect_providers - [ ] list_open_id_connect_providers
- [X] list_policies - [X] list_policies
- [X] list_policy_versions - [X] list_policy_versions
- [X] list_role_policies - [X] list_role_policies
- [ ] list_roles - [X] list_roles
- [X] list_role_tags
- [ ] list_user_tags
- [X] list_saml_providers - [X] list_saml_providers
- [ ] list_server_certificates - [X] list_server_certificates
- [ ] list_service_specific_credentials - [ ] list_service_specific_credentials
- [ ] list_signing_certificates - [X] list_signing_certificates
- [ ] list_ssh_public_keys - [ ] list_ssh_public_keys
- [X] list_user_policies - [X] list_user_policies
- [X] list_users - [X] list_users
@ -2315,6 +2317,10 @@
- [ ] set_default_policy_version - [ ] set_default_policy_version
- [ ] simulate_custom_policy - [ ] simulate_custom_policy
- [ ] simulate_principal_policy - [ ] simulate_principal_policy
- [X] tag_role
- [ ] tag_user
- [X] untag_role
- [ ] untag_user
- [X] update_access_key - [X] update_access_key
- [ ] update_account_password_policy - [ ] update_account_password_policy
- [ ] update_assume_role_policy - [ ] update_assume_role_policy
@ -2326,11 +2332,11 @@
- [X] update_saml_provider - [X] update_saml_provider
- [ ] update_server_certificate - [ ] update_server_certificate
- [ ] update_service_specific_credential - [ ] update_service_specific_credential
- [ ] update_signing_certificate - [X] update_signing_certificate
- [ ] update_ssh_public_key - [ ] update_ssh_public_key
- [ ] update_user - [ ] update_user
- [ ] upload_server_certificate - [X] upload_server_certificate
- [ ] upload_signing_certificate - [X] upload_signing_certificate
- [ ] upload_ssh_public_key - [ ] upload_ssh_public_key
## importexport - 0% implemented ## importexport - 0% implemented
@ -3542,7 +3548,7 @@
- [ ] get_bucket_inventory_configuration - [ ] get_bucket_inventory_configuration
- [ ] get_bucket_lifecycle - [ ] get_bucket_lifecycle
- [ ] get_bucket_lifecycle_configuration - [ ] get_bucket_lifecycle_configuration
- [ ] get_bucket_location - [X] get_bucket_location
- [ ] get_bucket_logging - [ ] get_bucket_logging
- [ ] get_bucket_metrics_configuration - [ ] get_bucket_metrics_configuration
- [ ] get_bucket_notification - [ ] get_bucket_notification

View File

@ -271,6 +271,49 @@ class FakeStack(BaseModel):
self.status = "DELETE_COMPLETE" self.status = "DELETE_COMPLETE"
class FakeChange(BaseModel):
def __init__(self, action, logical_resource_id, resource_type):
self.action = action
self.logical_resource_id = logical_resource_id
self.resource_type = resource_type
class FakeChangeSet(FakeStack):
def __init__(self, stack_id, stack_name, stack_template, change_set_id, change_set_name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None, cross_stack_resources=None):
super(FakeChangeSet, self).__init__(
stack_id,
stack_name,
stack_template,
parameters,
region_name,
notification_arns=notification_arns,
tags=tags,
role_arn=role_arn,
cross_stack_resources=cross_stack_resources,
create_change_set=True,
)
self.stack_name = stack_name
self.change_set_id = change_set_id
self.change_set_name = change_set_name
self.changes = self.diff(template=template, parameters=parameters)
def diff(self, template, parameters=None):
self.template = template
self._parse_template()
changes = []
resources_by_action = self.resource_map.diff(self.template_dict, parameters)
for action, resources in resources_by_action.items():
for resource_name, resource in resources.items():
changes.append(FakeChange(
action=action,
logical_resource_id=resource_name,
resource_type=resource['ResourceType'],
))
return changes
class FakeEvent(BaseModel): class FakeEvent(BaseModel):
def __init__(self, stack_id, stack_name, logical_resource_id, physical_resource_id, resource_type, resource_status, resource_status_reason=None, resource_properties=None): def __init__(self, stack_id, stack_name, logical_resource_id, physical_resource_id, resource_type, resource_status, resource_status_reason=None, resource_properties=None):
@ -377,24 +420,62 @@ class CloudFormationBackend(BaseBackend):
return new_stack return new_stack
def create_change_set(self, stack_name, change_set_name, template, parameters, region_name, change_set_type, notification_arns=None, tags=None, role_arn=None): def create_change_set(self, stack_name, change_set_name, template, parameters, region_name, change_set_type, notification_arns=None, tags=None, role_arn=None):
stack_id = None
stack_template = None
if change_set_type == 'UPDATE': if change_set_type == 'UPDATE':
stacks = self.stacks.values() stacks = self.stacks.values()
stack = None stack = None
for s in stacks: for s in stacks:
if s.name == stack_name: if s.name == stack_name:
stack = s stack = s
stack_id = stack.stack_id
stack_template = stack.template
if stack is None: if stack is None:
raise ValidationError(stack_name) raise ValidationError(stack_name)
else: else:
stack = self.create_stack(stack_name, template, parameters, stack_id = generate_stack_id(stack_name)
region_name, notification_arns, tags, stack_template = template
role_arn, create_change_set=True)
change_set_id = generate_changeset_id(change_set_name, region_name) change_set_id = generate_changeset_id(change_set_name, region_name)
self.stacks[change_set_name] = {'Id': change_set_id, new_change_set = FakeChangeSet(
'StackId': stack.stack_id} stack_id=stack_id,
self.change_sets[change_set_id] = stack stack_name=stack_name,
return change_set_id, stack.stack_id stack_template=stack_template,
change_set_id=change_set_id,
change_set_name=change_set_name,
template=template,
parameters=parameters,
region_name=region_name,
notification_arns=notification_arns,
tags=tags,
role_arn=role_arn,
cross_stack_resources=self.exports
)
self.change_sets[change_set_id] = new_change_set
self.stacks[stack_id] = new_change_set
return change_set_id, stack_id
def delete_change_set(self, change_set_name, stack_name=None):
if change_set_name in self.change_sets:
# This means arn was passed in
del self.change_sets[change_set_name]
else:
for cs in self.change_sets:
if self.change_sets[cs].change_set_name == change_set_name:
del self.change_sets[cs]
def describe_change_set(self, change_set_name, stack_name=None):
change_set = None
if change_set_name in self.change_sets:
# This means arn was passed in
change_set = self.change_sets[change_set_name]
else:
for cs in self.change_sets:
if self.change_sets[cs].change_set_name == change_set_name:
change_set = self.change_sets[cs]
if change_set is None:
raise ValidationError(change_set_name)
return change_set
def execute_change_set(self, change_set_name, stack_name=None): def execute_change_set(self, change_set_name, stack_name=None):
stack = None stack = None
@ -403,7 +484,7 @@ class CloudFormationBackend(BaseBackend):
stack = self.change_sets[change_set_name] stack = self.change_sets[change_set_name]
else: else:
for cs in self.change_sets: for cs in self.change_sets:
if self.change_sets[cs].name == change_set_name: if self.change_sets[cs].change_set_name == change_set_name:
stack = self.change_sets[cs] stack = self.change_sets[cs]
if stack is None: if stack is None:
raise ValidationError(stack_name) raise ValidationError(stack_name)
@ -429,6 +510,9 @@ class CloudFormationBackend(BaseBackend):
else: else:
return list(stacks) return list(stacks)
def list_change_sets(self):
return self.change_sets.values()
def list_stacks(self): def list_stacks(self):
return [ return [
v for v in self.stacks.values() v for v in self.stacks.values()

View File

@ -465,36 +465,70 @@ class ResourceMap(collections.Mapping):
ec2_models.ec2_backends[self._region_name].create_tags( ec2_models.ec2_backends[self._region_name].create_tags(
[self[resource].physical_resource_id], self.tags) [self[resource].physical_resource_id], self.tags)
def update(self, template, parameters=None): def diff(self, template, parameters=None):
if parameters: if parameters:
self.input_parameters = parameters self.input_parameters = parameters
self.load_mapping() self.load_mapping()
self.load_parameters() self.load_parameters()
self.load_conditions() self.load_conditions()
old_template = self._resource_json_map
new_template = template['Resources']
resource_names_by_action = {
'Add': set(new_template) - set(old_template),
'Modify': set(name for name in new_template if name in old_template and new_template[
name] != old_template[name]),
'Remove': set(old_template) - set(new_template)
}
resources_by_action = {
'Add': {},
'Modify': {},
'Remove': {},
}
for resource_name in resource_names_by_action['Add']:
resources_by_action['Add'][resource_name] = {
'LogicalResourceId': resource_name,
'ResourceType': new_template[resource_name]['Type']
}
for resource_name in resource_names_by_action['Modify']:
resources_by_action['Modify'][resource_name] = {
'LogicalResourceId': resource_name,
'ResourceType': new_template[resource_name]['Type']
}
for resource_name in resource_names_by_action['Remove']:
resources_by_action['Remove'][resource_name] = {
'LogicalResourceId': resource_name,
'ResourceType': old_template[resource_name]['Type']
}
return resources_by_action
def update(self, template, parameters=None):
resources_by_action = self.diff(template, parameters)
old_template = self._resource_json_map old_template = self._resource_json_map
new_template = template['Resources'] new_template = template['Resources']
self._resource_json_map = new_template self._resource_json_map = new_template
new_resource_names = set(new_template) - set(old_template) for resource_name, resource in resources_by_action['Add'].items():
for resource_name in new_resource_names:
resource_json = new_template[resource_name] resource_json = new_template[resource_name]
new_resource = parse_and_create_resource( new_resource = parse_and_create_resource(
resource_name, resource_json, self, self._region_name) resource_name, resource_json, self, self._region_name)
self._parsed_resources[resource_name] = new_resource self._parsed_resources[resource_name] = new_resource
removed_resource_names = set(old_template) - set(new_template) for resource_name, resource in resources_by_action['Remove'].items():
for resource_name in removed_resource_names:
resource_json = old_template[resource_name] resource_json = old_template[resource_name]
parse_and_delete_resource( parse_and_delete_resource(
resource_name, resource_json, self, self._region_name) resource_name, resource_json, self, self._region_name)
self._parsed_resources.pop(resource_name) self._parsed_resources.pop(resource_name)
resources_to_update = set(name for name in new_template if name in old_template and new_template[
name] != old_template[name])
tries = 1 tries = 1
while resources_to_update and tries < 5: while resources_by_action['Modify'] and tries < 5:
for resource_name in resources_to_update.copy(): for resource_name, resource in resources_by_action['Modify'].copy().items():
resource_json = new_template[resource_name] resource_json = new_template[resource_name]
try: try:
changed_resource = parse_and_update_resource( changed_resource = parse_and_update_resource(
@ -505,7 +539,7 @@ class ResourceMap(collections.Mapping):
last_exception = e last_exception = e
else: else:
self._parsed_resources[resource_name] = changed_resource self._parsed_resources[resource_name] = changed_resource
resources_to_update.remove(resource_name) del resources_by_action['Modify'][resource_name]
tries += 1 tries += 1
if tries == 5: if tries == 5:
raise last_exception raise last_exception

View File

@ -120,6 +120,31 @@ class CloudFormationResponse(BaseResponse):
template = self.response_template(CREATE_CHANGE_SET_RESPONSE_TEMPLATE) template = self.response_template(CREATE_CHANGE_SET_RESPONSE_TEMPLATE)
return template.render(stack_id=stack_id, change_set_id=change_set_id) return template.render(stack_id=stack_id, change_set_id=change_set_id)
def delete_change_set(self):
stack_name = self._get_param('StackName')
change_set_name = self._get_param('ChangeSetName')
self.cloudformation_backend.delete_change_set(change_set_name=change_set_name, stack_name=stack_name)
if self.request_json:
return json.dumps({
'DeleteChangeSetResponse': {
'DeleteChangeSetResult': {},
}
})
else:
template = self.response_template(DELETE_CHANGE_SET_RESPONSE_TEMPLATE)
return template.render()
def describe_change_set(self):
stack_name = self._get_param('StackName')
change_set_name = self._get_param('ChangeSetName')
change_set = self.cloudformation_backend.describe_change_set(
change_set_name=change_set_name,
stack_name=stack_name,
)
template = self.response_template(DESCRIBE_CHANGE_SET_RESPONSE_TEMPLATE)
return template.render(change_set=change_set)
@amzn_request_id @amzn_request_id
def execute_change_set(self): def execute_change_set(self):
stack_name = self._get_param('StackName') stack_name = self._get_param('StackName')
@ -187,6 +212,11 @@ class CloudFormationResponse(BaseResponse):
template = self.response_template(DESCRIBE_STACK_EVENTS_RESPONSE) template = self.response_template(DESCRIBE_STACK_EVENTS_RESPONSE)
return template.render(stack=stack) return template.render(stack=stack)
def list_change_sets(self):
change_sets = self.cloudformation_backend.list_change_sets()
template = self.response_template(LIST_CHANGE_SETS_RESPONSE)
return template.render(change_sets=change_sets)
def list_stacks(self): def list_stacks(self):
stacks = self.cloudformation_backend.list_stacks() stacks = self.cloudformation_backend.list_stacks()
template = self.response_template(LIST_STACKS_RESPONSE) template = self.response_template(LIST_STACKS_RESPONSE)
@ -523,6 +553,66 @@ CREATE_CHANGE_SET_RESPONSE_TEMPLATE = """<CreateStackResponse>
</CreateStackResponse> </CreateStackResponse>
""" """
DELETE_CHANGE_SET_RESPONSE_TEMPLATE = """<DeleteChangeSetResponse>
<DeleteChangeSetResult>
</DeleteChangeSetResult>
<ResponseMetadata>
<RequestId>3d3200a1-810e-3023-6cc3-example</RequestId>
</ResponseMetadata>
</DeleteChangeSetResponse>
"""
DESCRIBE_CHANGE_SET_RESPONSE_TEMPLATE = """<DescribeChangeSetResponse>
<DescribeChangeSetResult>
<ChangeSetId>{{ change_set.change_set_id }}</ChangeSetId>
<ChangeSetName>{{ change_set.change_set_name }}</ChangeSetName>
<StackId>{{ change_set.stack_id }}</StackId>
<StackName>{{ change_set.stack_name }}</StackName>
<Description>{{ change_set.description }}</Description>
<Parameters>
{% for param_name, param_value in change_set.stack_parameters.items() %}
<member>
<ParameterKey>{{ param_name }}</ParameterKey>
<ParameterValue>{{ param_value }}</ParameterValue>
</member>
{% endfor %}
</Parameters>
<CreationTime>2011-05-23T15:47:44Z</CreationTime>
<ExecutionStatus>{{ change_set.execution_status }}</ExecutionStatus>
<Status>{{ change_set.status }}</Status>
<StatusReason>{{ change_set.status_reason }}</StatusReason>
{% if change_set.notification_arns %}
<NotificationARNs>
{% for notification_arn in change_set.notification_arns %}
<member>{{ notification_arn }}</member>
{% endfor %}
</NotificationARNs>
{% else %}
<NotificationARNs/>
{% endif %}
{% if change_set.role_arn %}
<RoleARN>{{ change_set.role_arn }}</RoleARN>
{% endif %}
{% if change_set.changes %}
<Changes>
{% for change in change_set.changes %}
<member>
<Type>Resource</Type>
<ResourceChange>
<Action>{{ change.action }}</Action>
<LogicalResourceId>{{ change.logical_resource_id }}</LogicalResourceId>
<ResourceType>{{ change.resource_type }}</ResourceType>
</ResourceChange>
</member>
{% endfor %}
</Changes>
{% endif %}
{% if next_token %}
<NextToken>{{ next_token }}</NextToken>
{% endif %}
</DescribeChangeSetResult>
</DescribeChangeSetResponse>"""
EXECUTE_CHANGE_SET_RESPONSE_TEMPLATE = """<ExecuteChangeSetResponse> EXECUTE_CHANGE_SET_RESPONSE_TEMPLATE = """<ExecuteChangeSetResponse>
<ExecuteChangeSetResult> <ExecuteChangeSetResult>
<ExecuteChangeSetResult/> <ExecuteChangeSetResult/>
@ -648,6 +738,27 @@ DESCRIBE_STACK_EVENTS_RESPONSE = """<DescribeStackEventsResponse xmlns="http://c
</DescribeStackEventsResponse>""" </DescribeStackEventsResponse>"""
LIST_CHANGE_SETS_RESPONSE = """<ListChangeSetsResponse>
<ListChangeSetsResult>
<Summaries>
{% for change_set in change_sets %}
<member>
<StackId>{{ change_set.stack_id }}</StackId>
<StackName>{{ change_set.stack_name }}</StackName>
<ChangeSetId>{{ change_set.change_set_id }}</ChangeSetId>
<ChangeSetName>{{ change_set.change_set_name }}</ChangeSetName>
<ExecutionStatus>{{ change_set.execution_status }}</ExecutionStatus>
<Status>{{ change_set.status }}</Status>
<StatusReason>{{ change_set.status_reason }}</StatusReason>
<CreationTime>2011-05-23T15:47:44Z</CreationTime>
<Description>{{ change_set.description }}</Description>
</member>
{% endfor %}
</Summaries>
</ListChangeSetsResult>
</ListChangeSetsResponse>"""
LIST_STACKS_RESPONSE = """<ListStacksResponse> LIST_STACKS_RESPONSE = """<ListStacksResponse>
<ListStacksResult> <ListStacksResult>
<StackSummaries> <StackSummaries>

View File

@ -66,6 +66,8 @@ class DynamoType(object):
return int(self.value) return int(self.value)
except ValueError: except ValueError:
return float(self.value) return float(self.value)
elif self.is_set():
return set(self.value)
else: else:
return self.value return self.value
@ -509,14 +511,11 @@ class Table(BaseModel):
elif 'Value' in val and DynamoType(val['Value']).value != current_attr[key].value: elif 'Value' in val and DynamoType(val['Value']).value != current_attr[key].value:
raise ValueError("The conditional request failed") raise ValueError("The conditional request failed")
elif 'ComparisonOperator' in val: elif 'ComparisonOperator' in val:
comparison_func = get_comparison_func(
val['ComparisonOperator'])
dynamo_types = [ dynamo_types = [
DynamoType(ele) for ele in DynamoType(ele) for ele in
val.get("AttributeValueList", []) val.get("AttributeValueList", [])
] ]
for t in dynamo_types: if not current_attr[key].compare(val['ComparisonOperator'], dynamo_types):
if not comparison_func(current_attr[key].value, t.value):
raise ValueError('The conditional request failed') raise ValueError('The conditional request failed')
if range_value: if range_value:
self.items[hash_value][range_value] = item self.items[hash_value][range_value] = item
@ -946,14 +945,11 @@ class DynamoDBBackend(BaseBackend):
elif 'Value' in val and DynamoType(val['Value']).value != item_attr[key].value: elif 'Value' in val and DynamoType(val['Value']).value != item_attr[key].value:
raise ValueError("The conditional request failed") raise ValueError("The conditional request failed")
elif 'ComparisonOperator' in val: elif 'ComparisonOperator' in val:
comparison_func = get_comparison_func(
val['ComparisonOperator'])
dynamo_types = [ dynamo_types = [
DynamoType(ele) for ele in DynamoType(ele) for ele in
val.get("AttributeValueList", []) val.get("AttributeValueList", [])
] ]
for t in dynamo_types: if not item_attr[key].compare(val['ComparisonOperator'], dynamo_types):
if not comparison_func(item_attr[key].value, t.value):
raise ValueError('The conditional request failed') raise ValueError('The conditional request failed')
# Update does not fail on new items, so create one # Update does not fail on new items, so create one

55
moto/ec2/models.py Executable file → Normal file
View File

@ -2464,7 +2464,7 @@ class SubnetBackend(object):
default_for_az, map_public_ip_on_launch) default_for_az, map_public_ip_on_launch)
# AWS associates a new subnet with the default Network ACL # AWS associates a new subnet with the default Network ACL
self.associate_default_network_acl_with_subnet(subnet_id) self.associate_default_network_acl_with_subnet(subnet_id, vpc_id)
self.subnets[availability_zone][subnet_id] = subnet self.subnets[availability_zone][subnet_id] = subnet
return subnet return subnet
@ -2879,7 +2879,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
def __init__(self, ec2_backend, spot_request_id, price, image_id, type, def __init__(self, ec2_backend, spot_request_id, price, image_id, type,
valid_from, valid_until, launch_group, availability_zone_group, valid_from, valid_until, launch_group, availability_zone_group,
key_name, security_groups, user_data, instance_type, placement, key_name, security_groups, user_data, instance_type, placement,
kernel_id, ramdisk_id, monitoring_enabled, subnet_id, spot_fleet_id, kernel_id, ramdisk_id, monitoring_enabled, subnet_id, tags, spot_fleet_id,
**kwargs): **kwargs):
super(SpotInstanceRequest, self).__init__(**kwargs) super(SpotInstanceRequest, self).__init__(**kwargs)
ls = LaunchSpecification() ls = LaunchSpecification()
@ -2903,6 +2903,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
ls.monitored = monitoring_enabled ls.monitored = monitoring_enabled
ls.subnet_id = subnet_id ls.subnet_id = subnet_id
self.spot_fleet_id = spot_fleet_id self.spot_fleet_id = spot_fleet_id
self.tags = tags
if security_groups: if security_groups:
for group_name in security_groups: for group_name in security_groups:
@ -2936,6 +2937,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
security_group_names=[], security_group_names=[],
security_group_ids=self.launch_specification.groups, security_group_ids=self.launch_specification.groups,
spot_fleet_id=self.spot_fleet_id, spot_fleet_id=self.spot_fleet_id,
tags=self.tags,
) )
instance = reservation.instances[0] instance = reservation.instances[0]
return instance return instance
@ -2951,15 +2953,16 @@ class SpotRequestBackend(object):
valid_until, launch_group, availability_zone_group, valid_until, launch_group, availability_zone_group,
key_name, security_groups, user_data, key_name, security_groups, user_data,
instance_type, placement, kernel_id, ramdisk_id, instance_type, placement, kernel_id, ramdisk_id,
monitoring_enabled, subnet_id, spot_fleet_id=None): monitoring_enabled, subnet_id, tags=None, spot_fleet_id=None):
requests = [] requests = []
tags = tags or {}
for _ in range(count): for _ in range(count):
spot_request_id = random_spot_request_id() spot_request_id = random_spot_request_id()
request = SpotInstanceRequest(self, request = SpotInstanceRequest(self,
spot_request_id, price, image_id, type, valid_from, valid_until, spot_request_id, price, image_id, type, valid_from, valid_until,
launch_group, availability_zone_group, key_name, security_groups, launch_group, availability_zone_group, key_name, security_groups,
user_data, instance_type, placement, kernel_id, ramdisk_id, user_data, instance_type, placement, kernel_id, ramdisk_id,
monitoring_enabled, subnet_id, spot_fleet_id) monitoring_enabled, subnet_id, tags, spot_fleet_id)
self.spot_instance_requests[spot_request_id] = request self.spot_instance_requests[spot_request_id] = request
requests.append(request) requests.append(request)
return requests return requests
@ -2979,8 +2982,8 @@ class SpotRequestBackend(object):
class SpotFleetLaunchSpec(object): class SpotFleetLaunchSpec(object):
def __init__(self, ebs_optimized, group_set, iam_instance_profile, image_id, def __init__(self, ebs_optimized, group_set, iam_instance_profile, image_id,
instance_type, key_name, monitoring, spot_price, subnet_id, user_data, instance_type, key_name, monitoring, spot_price, subnet_id, tag_specifications,
weighted_capacity): user_data, weighted_capacity):
self.ebs_optimized = ebs_optimized self.ebs_optimized = ebs_optimized
self.group_set = group_set self.group_set = group_set
self.iam_instance_profile = iam_instance_profile self.iam_instance_profile = iam_instance_profile
@ -2990,6 +2993,7 @@ class SpotFleetLaunchSpec(object):
self.monitoring = monitoring self.monitoring = monitoring
self.spot_price = spot_price self.spot_price = spot_price
self.subnet_id = subnet_id self.subnet_id = subnet_id
self.tag_specifications = tag_specifications
self.user_data = user_data self.user_data = user_data
self.weighted_capacity = float(weighted_capacity) self.weighted_capacity = float(weighted_capacity)
@ -3020,6 +3024,7 @@ class SpotFleetRequest(TaggedEC2Resource):
monitoring=spec.get('monitoring._enabled'), monitoring=spec.get('monitoring._enabled'),
spot_price=spec.get('spot_price', self.spot_price), spot_price=spec.get('spot_price', self.spot_price),
subnet_id=spec['subnet_id'], subnet_id=spec['subnet_id'],
tag_specifications=self._parse_tag_specifications(spec),
user_data=spec.get('user_data'), user_data=spec.get('user_data'),
weighted_capacity=spec['weighted_capacity'], weighted_capacity=spec['weighted_capacity'],
) )
@ -3102,6 +3107,7 @@ class SpotFleetRequest(TaggedEC2Resource):
monitoring_enabled=launch_spec.monitoring, monitoring_enabled=launch_spec.monitoring,
subnet_id=launch_spec.subnet_id, subnet_id=launch_spec.subnet_id,
spot_fleet_id=self.id, spot_fleet_id=self.id,
tags=launch_spec.tag_specifications,
) )
self.spot_requests.extend(requests) self.spot_requests.extend(requests)
self.fulfilled_capacity += added_weight self.fulfilled_capacity += added_weight
@ -3124,6 +3130,25 @@ class SpotFleetRequest(TaggedEC2Resource):
self.spot_requests = [req for req in self.spot_requests if req.instance.id not in instance_ids] self.spot_requests = [req for req in self.spot_requests if req.instance.id not in instance_ids]
self.ec2_backend.terminate_instances(instance_ids) self.ec2_backend.terminate_instances(instance_ids)
def _parse_tag_specifications(self, spec):
try:
tag_spec_num = max([int(key.split('.')[1]) for key in spec if key.startswith("tag_specification_set")])
except ValueError: # no tag specifications
return {}
tag_specifications = {}
for si in range(1, tag_spec_num + 1):
resource_type = spec["tag_specification_set.{si}._resource_type".format(si=si)]
tags = [key for key in spec if key.startswith("tag_specification_set.{si}._tag".format(si=si))]
tag_num = max([int(key.split('.')[3]) for key in tags])
tag_specifications[resource_type] = dict((
spec["tag_specification_set.{si}._tag.{ti}._key".format(si=si, ti=ti)],
spec["tag_specification_set.{si}._tag.{ti}._value".format(si=si, ti=ti)],
) for ti in range(1, tag_num + 1))
return tag_specifications
class SpotFleetBackend(object): class SpotFleetBackend(object):
def __init__(self): def __init__(self):
@ -3560,8 +3585,22 @@ class NetworkAclBackend(object):
self.get_vpc(vpc_id) self.get_vpc(vpc_id)
network_acl = NetworkAcl(self, network_acl_id, vpc_id, default) network_acl = NetworkAcl(self, network_acl_id, vpc_id, default)
self.network_acls[network_acl_id] = network_acl self.network_acls[network_acl_id] = network_acl
if default:
self.add_default_entries(network_acl_id)
return network_acl return network_acl
def add_default_entries(self, network_acl_id):
default_acl_entries = [
{'rule_number': 100, 'rule_action': 'allow', 'egress': 'true'},
{'rule_number': 32767, 'rule_action': 'deny', 'egress': 'true'},
{'rule_number': 100, 'rule_action': 'allow', 'egress': 'false'},
{'rule_number': 32767, 'rule_action': 'deny', 'egress': 'false'}
]
for entry in default_acl_entries:
self.create_network_acl_entry(network_acl_id=network_acl_id, rule_number=entry['rule_number'], protocol='-1',
rule_action=entry['rule_action'], egress=entry['egress'], cidr_block='0.0.0.0/0',
icmp_code=None, icmp_type=None, port_range_from=None, port_range_to=None)
def get_all_network_acls(self, network_acl_ids=None, filters=None): def get_all_network_acls(self, network_acl_ids=None, filters=None):
network_acls = self.network_acls.values() network_acls = self.network_acls.values()
@ -3636,9 +3675,9 @@ class NetworkAclBackend(object):
new_acl.associations[new_assoc_id] = association new_acl.associations[new_assoc_id] = association
return association return association
def associate_default_network_acl_with_subnet(self, subnet_id): def associate_default_network_acl_with_subnet(self, subnet_id, vpc_id):
association_id = random_network_acl_subnet_association_id() association_id = random_network_acl_subnet_association_id()
acl = next(acl for acl in self.network_acls.values() if acl.default) acl = next(acl for acl in self.network_acls.values() if acl.default and acl.vpc_id == vpc_id)
acl.associations[association_id] = NetworkAclAssociation(self, association_id, acl.associations[association_id] = NetworkAclAssociation(self, association_id,
subnet_id, acl.id) subnet_id, acl.id)

View File

@ -107,6 +107,21 @@ DESCRIBE_SPOT_FLEET_TEMPLATE = """<DescribeSpotFleetRequestsResponse xmlns="http
</item> </item>
{% endfor %} {% endfor %}
</groupSet> </groupSet>
<tagSpecificationSet>
{% for resource_type in launch_spec.tag_specifications %}
<item>
<resourceType>{{ resource_type }}</resourceType>
<tag>
{% for key, value in launch_spec.tag_specifications[resource_type].items() %}
<item>
<key>{{ key }}</key>
<value>{{ value }}</value>
</item>
{% endfor %}
</tag>
</item>
{% endfor %}
</tagSpecificationSet>
</item> </item>
{% endfor %} {% endfor %}
</launchSpecifications> </launchSpecifications>

View File

@ -3,6 +3,7 @@ from .responses import ECRResponse
url_bases = [ url_bases = [
"https?://ecr.(.+).amazonaws.com", "https?://ecr.(.+).amazonaws.com",
"https?://api.ecr.(.+).amazonaws.com",
] ]
url_paths = { url_paths = {

View File

@ -32,3 +32,48 @@ class MalformedCertificate(RESTError):
def __init__(self, cert): def __init__(self, cert):
super(MalformedCertificate, self).__init__( super(MalformedCertificate, self).__init__(
'MalformedCertificate', 'Certificate {cert} is malformed'.format(cert=cert)) 'MalformedCertificate', 'Certificate {cert} is malformed'.format(cert=cert))
class DuplicateTags(RESTError):
code = 400
def __init__(self):
super(DuplicateTags, self).__init__(
'InvalidInput', 'Duplicate tag keys found. Please note that Tag keys are case insensitive.')
class TagKeyTooBig(RESTError):
code = 400
def __init__(self, tag, param='tags.X.member.key'):
super(TagKeyTooBig, self).__init__(
'ValidationError', "1 validation error detected: Value '{}' at '{}' failed to satisfy "
"constraint: Member must have length less than or equal to 128.".format(tag, param))
class TagValueTooBig(RESTError):
code = 400
def __init__(self, tag):
super(TagValueTooBig, self).__init__(
'ValidationError', "1 validation error detected: Value '{}' at 'tags.X.member.value' failed to satisfy "
"constraint: Member must have length less than or equal to 256.".format(tag))
class InvalidTagCharacters(RESTError):
code = 400
def __init__(self, tag, param='tags.X.member.key'):
message = "1 validation error detected: Value '{}' at '{}' failed to satisfy ".format(tag, param)
message += "constraint: Member must satisfy regular expression pattern: [\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]+"
super(InvalidTagCharacters, self).__init__('ValidationError', message)
class TooManyTags(RESTError):
code = 400
def __init__(self, tags, param='tags'):
super(TooManyTags, self).__init__(
'ValidationError', "1 validation error detected: Value '{}' at '{}' failed to satisfy "
"constraint: Member must have length less than or equal to 50.".format(tags, param))

View File

@ -3,6 +3,7 @@ import base64
import sys import sys
from datetime import datetime from datetime import datetime
import json import json
import re
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
@ -12,7 +13,8 @@ from moto.core import BaseBackend, BaseModel
from moto.core.utils import iso_8601_datetime_without_milliseconds from moto.core.utils import iso_8601_datetime_without_milliseconds
from .aws_managed_policies import aws_managed_policies_data from .aws_managed_policies import aws_managed_policies_data
from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, MalformedCertificate from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, MalformedCertificate, \
DuplicateTags, TagKeyTooBig, InvalidTagCharacters, TooManyTags, TagValueTooBig
from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id
ACCOUNT_ID = 123456789012 ACCOUNT_ID = 123456789012
@ -32,7 +34,6 @@ class MFADevice(object):
class Policy(BaseModel): class Policy(BaseModel):
is_attachable = False is_attachable = False
def __init__(self, def __init__(self,
@ -132,6 +133,7 @@ class Role(BaseModel):
self.policies = {} self.policies = {}
self.managed_policies = {} self.managed_policies = {}
self.create_date = datetime.now(pytz.utc) self.create_date = datetime.now(pytz.utc)
self.tags = {}
@classmethod @classmethod
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
@ -175,6 +177,9 @@ class Role(BaseModel):
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
raise UnformattedGetAttTemplateException() raise UnformattedGetAttTemplateException()
def get_tags(self):
return [self.tags[tag] for tag in self.tags]
class InstanceProfile(BaseModel): class InstanceProfile(BaseModel):
@ -614,6 +619,86 @@ class IAMBackend(BaseBackend):
role = self.get_role(role_name) role = self.get_role(role_name)
return role.policies.keys() return role.policies.keys()
def _validate_tag_key(self, tag_key, exception_param='tags.X.member.key'):
"""Validates the tag key.
:param all_tags: Dict to check if there is a duplicate tag.
:param tag_key: The tag key to check against.
:param exception_param: The exception parameter to send over to help format the message. This is to reflect
the difference between the tag and untag APIs.
:return:
"""
# Validate that the key length is correct:
if len(tag_key) > 128:
raise TagKeyTooBig(tag_key, param=exception_param)
# Validate that the tag key fits the proper Regex:
# [\w\s_.:/=+\-@]+ SHOULD be the same as the Java regex on the AWS documentation: [\p{L}\p{Z}\p{N}_.:/=+\-@]+
match = re.findall(r'[\w\s_.:/=+\-@]+', tag_key)
# Kudos if you can come up with a better way of doing a global search :)
if not len(match) or len(match[0]) < len(tag_key):
raise InvalidTagCharacters(tag_key, param=exception_param)
def _check_tag_duplicate(self, all_tags, tag_key):
"""Validates that a tag key is not a duplicate
:param all_tags: Dict to check if there is a duplicate tag.
:param tag_key: The tag key to check against.
:return:
"""
if tag_key in all_tags:
raise DuplicateTags()
def list_role_tags(self, role_name, marker, max_items=100):
role = self.get_role(role_name)
max_items = int(max_items)
tag_index = sorted(role.tags)
start_idx = int(marker) if marker else 0
tag_index = tag_index[start_idx:start_idx + max_items]
if len(role.tags) <= (start_idx + max_items):
marker = None
else:
marker = str(start_idx + max_items)
# Make the tag list of dict's:
tags = [role.tags[tag] for tag in tag_index]
return tags, marker
def tag_role(self, role_name, tags):
if len(tags) > 50:
raise TooManyTags(tags)
role = self.get_role(role_name)
tag_keys = {}
for tag in tags:
# Need to index by the lowercase tag key since the keys are case insensitive, but their case is retained.
ref_key = tag['Key'].lower()
self._check_tag_duplicate(tag_keys, ref_key)
self._validate_tag_key(tag['Key'])
if len(tag['Value']) > 256:
raise TagValueTooBig(tag['Value'])
tag_keys[ref_key] = tag
role.tags.update(tag_keys)
def untag_role(self, role_name, tag_keys):
if len(tag_keys) > 50:
raise TooManyTags(tag_keys, param='tagKeys')
role = self.get_role(role_name)
for key in tag_keys:
ref_key = key.lower()
self._validate_tag_key(key, exception_param='tagKeys')
role.tags.pop(ref_key, None)
def create_policy_version(self, policy_arn, policy_document, set_as_default): def create_policy_version(self, policy_arn, policy_document, set_as_default):
policy = self.get_policy(policy_arn) policy = self.get_policy(policy_arn)
if not policy: if not policy:

View File

@ -554,7 +554,8 @@ class IamResponse(BaseResponse):
policies=account_details['managed_policies'], policies=account_details['managed_policies'],
users=account_details['users'], users=account_details['users'],
groups=account_details['groups'], groups=account_details['groups'],
roles=account_details['roles'] roles=account_details['roles'],
get_groups_for_user=iam_backend.get_groups_for_user
) )
def create_saml_provider(self): def create_saml_provider(self):
@ -625,6 +626,34 @@ class IamResponse(BaseResponse):
template = self.response_template(LIST_SIGNING_CERTIFICATES_TEMPLATE) template = self.response_template(LIST_SIGNING_CERTIFICATES_TEMPLATE)
return template.render(user_name=user_name, certificates=certs) return template.render(user_name=user_name, certificates=certs)
def list_role_tags(self):
role_name = self._get_param('RoleName')
marker = self._get_param('Marker')
max_items = self._get_param('MaxItems', 100)
tags, marker = iam_backend.list_role_tags(role_name, marker, max_items)
template = self.response_template(LIST_ROLE_TAG_TEMPLATE)
return template.render(tags=tags, marker=marker)
def tag_role(self):
role_name = self._get_param('RoleName')
tags = self._get_multi_param('Tags.member')
iam_backend.tag_role(role_name, tags)
template = self.response_template(TAG_ROLE_TEMPLATE)
return template.render()
def untag_role(self):
role_name = self._get_param('RoleName')
tag_keys = self._get_multi_param('TagKeys.member')
iam_backend.untag_role(role_name, tag_keys)
template = self.response_template(UNTAG_ROLE_TEMPLATE)
return template.render()
ATTACH_ROLE_POLICY_TEMPLATE = """<AttachRolePolicyResponse> ATTACH_ROLE_POLICY_TEMPLATE = """<AttachRolePolicyResponse>
<ResponseMetadata> <ResponseMetadata>
@ -878,6 +907,16 @@ GET_ROLE_TEMPLATE = """<GetRoleResponse xmlns="https://iam.amazonaws.com/doc/201
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument> <AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
<CreateDate>{{ role.create_date }}</CreateDate> <CreateDate>{{ role.create_date }}</CreateDate>
<RoleId>{{ role.id }}</RoleId> <RoleId>{{ role.id }}</RoleId>
{% if role.tags %}
<Tags>
{% for tag in role.get_tags() %}
<member>
<Key>{{ tag['Key'] }}</Key>
<Value>{{ tag['Value'] }}</Value>
</member>
{% endfor %}
</Tags>
{% endif %}
</Role> </Role>
</GetRoleResult> </GetRoleResult>
<ResponseMetadata> <ResponseMetadata>
@ -1461,8 +1500,19 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
<UserDetailList> <UserDetailList>
{% for user in users %} {% for user in users %}
<member> <member>
<GroupList /> <GroupList>
<AttachedManagedPolicies/> {% for group in get_groups_for_user(user.name) %}
<member>{{ group.name }}</member>
{% endfor %}
</GroupList>
<AttachedManagedPolicies>
{% for policy in user.managed_policies %}
<member>
<PolicyName>{{ user.managed_policies[policy].name }}</PolicyName>
<PolicyArn>{{ policy }}</PolicyArn>
</member>
{% endfor %}
</AttachedManagedPolicies>
<UserId>{{ user.id }}</UserId> <UserId>{{ user.id }}</UserId>
<Path>{{ user.path }}</Path> <Path>{{ user.path }}</Path>
<UserName>{{ user.name }}</UserName> <UserName>{{ user.name }}</UserName>
@ -1476,10 +1526,10 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
<member> <member>
<GroupId>{{ group.id }}</GroupId> <GroupId>{{ group.id }}</GroupId>
<AttachedManagedPolicies> <AttachedManagedPolicies>
{% for policy in group.managed_policies %} {% for policy_arn in group.managed_policies %}
<member> <member>
<PolicyName>{{ policy.name }}</PolicyName> <PolicyName>{{ group.managed_policies[policy_arn].name }}</PolicyName>
<PolicyArn>{{ policy.arn }}</PolicyArn> <PolicyArn>{{ policy_arn }}</PolicyArn>
</member> </member>
{% endfor %} {% endfor %}
</AttachedManagedPolicies> </AttachedManagedPolicies>
@ -1487,22 +1537,44 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
<Path>{{ group.path }}</Path> <Path>{{ group.path }}</Path>
<Arn>{{ group.arn }}</Arn> <Arn>{{ group.arn }}</Arn>
<CreateDate>{{ group.create_date }}</CreateDate> <CreateDate>{{ group.create_date }}</CreateDate>
<GroupPolicyList/> <GroupPolicyList>
{% for policy in group.policies %}
<member>
<PolicyName>{{ policy }}</PolicyName>
<PolicyDocument>{{ group.get_policy(policy) }}</PolicyDocument>
</member>
{% endfor %}
</GroupPolicyList>
</member> </member>
{% endfor %} {% endfor %}
</GroupDetailList> </GroupDetailList>
<RoleDetailList> <RoleDetailList>
{% for role in roles %} {% for role in roles %}
<member> <member>
<RolePolicyList/> <RolePolicyList>
<AttachedManagedPolicies> {% for inline_policy in role.policies %}
{% for policy in role.managed_policies %}
<member> <member>
<PolicyName>{{ policy.name }}</PolicyName> <PolicyName>{{ inline_policy }}</PolicyName>
<PolicyArn>{{ policy.arn }}</PolicyArn> <PolicyDocument>{{ role.policies[inline_policy] }}</PolicyDocument>
</member>
{% endfor %}
</RolePolicyList>
<AttachedManagedPolicies>
{% for policy_arn in role.managed_policies %}
<member>
<PolicyName>{{ role.managed_policies[policy_arn].name }}</PolicyName>
<PolicyArn>{{ policy_arn }}</PolicyArn>
</member> </member>
{% endfor %} {% endfor %}
</AttachedManagedPolicies> </AttachedManagedPolicies>
<Tags>
{% for tag in role.get_tags() %}
<member>
<Key>{{ tag['Key'] }}</Key>
<Value>{{ tag['Value'] }}</Value>
</member>
{% endfor %}
</Tags>
<InstanceProfileList> <InstanceProfileList>
{% for profile in instance_profiles %} {% for profile in instance_profiles %}
<member> <member>
@ -1543,19 +1615,14 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
<PolicyId>{{ policy.id }}</PolicyId> <PolicyId>{{ policy.id }}</PolicyId>
<Path>{{ policy.path }}</Path> <Path>{{ policy.path }}</Path>
<PolicyVersionList> <PolicyVersionList>
{% for policy_version in policy.versions %}
<member> <member>
<Document> <Document>{{ policy_version.document }}</Document>
{"Version":"2012-10-17","Statement":{"Effect":"Allow", <IsDefaultVersion>{{ policy_version.is_default }}</IsDefaultVersion>
"Action":["iam:CreatePolicy","iam:CreatePolicyVersion", <VersionId>{{ policy_version.version_id }}</VersionId>
"iam:DeletePolicy","iam:DeletePolicyVersion","iam:GetPolicy", <CreateDate>{{ policy_version.create_datetime }}</CreateDate>
"iam:GetPolicyVersion","iam:ListPolicies",
"iam:ListPolicyVersions","iam:SetDefaultPolicyVersion"],
"Resource":"*"}}
</Document>
<IsDefaultVersion>true</IsDefaultVersion>
<VersionId>v1</VersionId>
<CreateDate>2012-05-09T16:27:11Z</CreateDate>
</member> </member>
{% endfor %}
</PolicyVersionList> </PolicyVersionList>
<Arn>{{ policy.arn }}</Arn> <Arn>{{ policy.arn }}</Arn>
<AttachmentCount>1</AttachmentCount> <AttachmentCount>1</AttachmentCount>
@ -1671,3 +1738,38 @@ LIST_SIGNING_CERTIFICATES_TEMPLATE = """<ListSigningCertificatesResponse>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId> <RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata> </ResponseMetadata>
</ListSigningCertificatesResponse>""" </ListSigningCertificatesResponse>"""
TAG_ROLE_TEMPLATE = """<TagRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<ResponseMetadata>
<RequestId>EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE</RequestId>
</ResponseMetadata>
</TagRoleResponse>"""
LIST_ROLE_TAG_TEMPLATE = """<ListRoleTagsResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<ListRoleTagsResult>
<IsTruncated>{{ 'true' if marker else 'false' }}</IsTruncated>
{% if marker %}
<Marker>{{ marker }}</Marker>
{% endif %}
<Tags>
{% for tag in tags %}
<member>
<Key>{{ tag['Key'] }}</Key>
<Value>{{ tag['Value'] }}</Value>
</member>
{% endfor %}
</Tags>
</ListRoleTagsResult>
<ResponseMetadata>
<RequestId>EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE</RequestId>
</ResponseMetadata>
</ListRoleTagsResponse>"""
UNTAG_ROLE_TEMPLATE = """<UntagRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<ResponseMetadata>
<RequestId>EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE</RequestId>
</ResponseMetadata>
</UntagRoleResponse>"""

View File

@ -21,8 +21,8 @@ def read(*parts):
install_requires = [ install_requires = [
"Jinja2>=2.7.3", "Jinja2>=2.7.3",
"boto>=2.36.0", "boto>=2.36.0",
"boto3>=1.6.16", "boto3>=1.9.86",
"botocore>=1.12.13", "botocore>=1.12.86",
"cryptography>=2.3.0", "cryptography>=2.3.0",
"requests>=2.5", "requests>=2.5",
"xmltodict", "xmltodict",

View File

@ -816,6 +816,32 @@ def test_create_change_set_from_s3_url():
assert 'arn:aws:cloudformation:us-east-1:123456789:stack/NewStack' in response['StackId'] assert 'arn:aws:cloudformation:us-east-1:123456789:stack/NewStack' in response['StackId']
@mock_cloudformation
def test_describe_change_set():
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
cf_conn.create_change_set(
StackName='NewStack',
TemplateBody=dummy_template_json,
ChangeSetName='NewChangeSet',
ChangeSetType='CREATE',
)
stack = cf_conn.describe_change_set(ChangeSetName="NewChangeSet")
stack['ChangeSetName'].should.equal('NewChangeSet')
stack['StackName'].should.equal('NewStack')
cf_conn.create_change_set(
StackName='NewStack',
TemplateBody=dummy_update_template_json,
ChangeSetName='NewChangeSet2',
ChangeSetType='UPDATE',
)
stack = cf_conn.describe_change_set(ChangeSetName="NewChangeSet2")
stack['ChangeSetName'].should.equal('NewChangeSet2')
stack['StackName'].should.equal('NewStack')
stack['Changes'].should.have.length_of(2)
@mock_cloudformation @mock_cloudformation
def test_execute_change_set_w_arn(): def test_execute_change_set_w_arn():
cf_conn = boto3.client('cloudformation', region_name='us-east-1') cf_conn = boto3.client('cloudformation', region_name='us-east-1')
@ -837,7 +863,7 @@ def test_execute_change_set_w_name():
ChangeSetName='NewChangeSet', ChangeSetName='NewChangeSet',
ChangeSetType='CREATE', ChangeSetType='CREATE',
) )
cf_conn.execute_change_set(ChangeSetName='NewStack', StackName='NewStack') cf_conn.execute_change_set(ChangeSetName='NewChangeSet', StackName='NewStack')
@mock_cloudformation @mock_cloudformation
@ -906,6 +932,20 @@ def test_describe_stack_by_stack_id():
stack_by_id['StackName'].should.equal("test_stack") stack_by_id['StackName'].should.equal("test_stack")
@mock_cloudformation
def test_list_change_sets():
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
cf_conn.create_change_set(
StackName='NewStack2',
TemplateBody=dummy_template_json,
ChangeSetName='NewChangeSet2',
ChangeSetType='CREATE',
)
change_set = cf_conn.list_change_sets(StackName='NewStack2')['Summaries'][0]
change_set['StackName'].should.equal('NewStack2')
change_set['ChangeSetName'].should.equal('NewChangeSet2')
@mock_cloudformation @mock_cloudformation
def test_list_stacks(): def test_list_stacks():
cf = boto3.resource('cloudformation', region_name='us-east-1') cf = boto3.resource('cloudformation', region_name='us-east-1')
@ -938,6 +978,22 @@ def test_delete_stack_from_resource():
list(cf.stacks.all()).should.have.length_of(0) list(cf.stacks.all()).should.have.length_of(0)
@mock_cloudformation
@mock_ec2
def test_delete_change_set():
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
cf_conn.create_change_set(
StackName='NewStack',
TemplateBody=dummy_template_json,
ChangeSetName='NewChangeSet',
ChangeSetType='CREATE',
)
cf_conn.list_change_sets(StackName='NewStack')['Summaries'].should.have.length_of(1)
cf_conn.delete_change_set(ChangeSetName='NewChangeSet', StackName='NewStack')
cf_conn.list_change_sets(StackName='NewStack')['Summaries'].should.have.length_of(0)
@mock_cloudformation @mock_cloudformation
@mock_ec2 @mock_ec2
def test_delete_stack_by_name(): def test_delete_stack_by_name():

View File

@ -750,6 +750,47 @@ def test_boto3_update_item_conditions_pass_because_expect_exists_by_compare_to_n
returned_item = table.get_item(Key={'username': 'johndoe'}) returned_item = table.get_item(Key={'username': 'johndoe'})
assert dict(returned_item)['Item']['foo'].should.equal("baz") assert dict(returned_item)['Item']['foo'].should.equal("baz")
@mock_dynamodb2
def test_boto3_update_settype_item_with_conditions():
class OrderedSet(set):
"""A set with predictable iteration order"""
def __init__(self, values):
super(OrderedSet, self).__init__(values)
self.__ordered_values = values
def __iter__(self):
return iter(self.__ordered_values)
table = _create_user_table()
table.put_item(Item={'username': 'johndoe'})
table.update_item(
Key={'username': 'johndoe'},
UpdateExpression='SET foo=:new_value',
ExpressionAttributeValues={
':new_value': OrderedSet(['hello', 'world']),
},
)
table.update_item(
Key={'username': 'johndoe'},
UpdateExpression='SET foo=:new_value',
ExpressionAttributeValues={
':new_value': set(['baz']),
},
Expected={
'foo': {
'ComparisonOperator': 'EQ',
'AttributeValueList': [
OrderedSet(['world', 'hello']), # Opposite order to original
],
}
},
)
returned_item = table.get_item(Key={'username': 'johndoe'})
assert dict(returned_item)['Item']['foo'].should.equal(set(['baz']))
@mock_dynamodb2 @mock_dynamodb2
def test_boto3_put_item_conditions_pass(): def test_boto3_put_item_conditions_pass():
table = _create_user_table() table = _create_user_table()

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import boto import boto
import boto3
import sure # noqa import sure # noqa
from moto import mock_ec2_deprecated from moto import mock_ec2_deprecated, mock_ec2
@mock_ec2_deprecated @mock_ec2_deprecated
@ -173,3 +174,43 @@ def test_network_acl_tagging():
if na.id == network_acl.id) if na.id == network_acl.id)
test_network_acl.tags.should.have.length_of(1) test_network_acl.tags.should.have.length_of(1)
test_network_acl.tags["a key"].should.equal("some value") test_network_acl.tags["a key"].should.equal("some value")
@mock_ec2
def test_new_subnet_in_new_vpc_associates_with_default_network_acl():
ec2 = boto3.resource('ec2', region_name='us-west-1')
new_vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16')
new_vpc.reload()
subnet = ec2.create_subnet(VpcId=new_vpc.id, CidrBlock='10.0.0.0/24')
subnet.reload()
new_vpcs_default_network_acl = next(iter(new_vpc.network_acls.all()), None)
new_vpcs_default_network_acl.reload()
new_vpcs_default_network_acl.vpc_id.should.equal(new_vpc.id)
new_vpcs_default_network_acl.associations.should.have.length_of(1)
new_vpcs_default_network_acl.associations[0]['SubnetId'].should.equal(subnet.id)
@mock_ec2
def test_default_network_acl_default_entries():
ec2 = boto3.resource('ec2', region_name='us-west-1')
default_network_acl = next(iter(ec2.network_acls.all()), None)
default_network_acl.is_default.should.be.ok
default_network_acl.entries.should.have.length_of(4)
unique_entries = []
for entry in default_network_acl.entries:
entry['CidrBlock'].should.equal('0.0.0.0/0')
entry['Protocol'].should.equal('-1')
entry['RuleNumber'].should.be.within([100, 32767])
entry['RuleAction'].should.be.within(['allow', 'deny'])
assert type(entry['Egress']) is bool
if entry['RuleAction'] == 'allow':
entry['RuleNumber'].should.be.equal(100)
else:
entry['RuleNumber'].should.be.equal(32767)
if entry not in unique_entries:
unique_entries.append(entry)
unique_entries.should.have.length_of(4)

View File

@ -54,7 +54,7 @@ def spot_config(subnet_id, allocation_strategy="lowestPrice"):
}, },
'EbsOptimized': False, 'EbsOptimized': False,
'WeightedCapacity': 2.0, 'WeightedCapacity': 2.0,
'SpotPrice': '0.13' 'SpotPrice': '0.13',
}, { }, {
'ImageId': 'ami-123', 'ImageId': 'ami-123',
'KeyName': 'my-key', 'KeyName': 'my-key',
@ -148,6 +148,48 @@ def test_create_diversified_spot_fleet():
instances[0]['InstanceId'].should.contain("i-") instances[0]['InstanceId'].should.contain("i-")
@mock_ec2
def test_create_spot_fleet_request_with_tag_spec():
conn = boto3.client("ec2", region_name='us-west-2')
subnet_id = get_subnet_id(conn)
tag_spec = [
{
'ResourceType': 'instance',
'Tags': [
{
'Key': 'tag-1',
'Value': 'foo',
},
{
'Key': 'tag-2',
'Value': 'bar',
},
]
},
]
config = spot_config(subnet_id)
config['LaunchSpecifications'][0]['TagSpecifications'] = tag_spec
spot_fleet_res = conn.request_spot_fleet(
SpotFleetRequestConfig=config
)
spot_fleet_id = spot_fleet_res['SpotFleetRequestId']
spot_fleet_requests = conn.describe_spot_fleet_requests(
SpotFleetRequestIds=[spot_fleet_id])['SpotFleetRequestConfigs']
spot_fleet_config = spot_fleet_requests[0]['SpotFleetRequestConfig']
spot_fleet_config['LaunchSpecifications'][0]['TagSpecifications'][0][
'ResourceType'].should.equal('instance')
for tag in tag_spec[0]['Tags']:
spot_fleet_config['LaunchSpecifications'][0]['TagSpecifications'][0]['Tags'].should.contain(tag)
instance_res = conn.describe_spot_fleet_instances(
SpotFleetRequestId=spot_fleet_id)
instances = conn.describe_instances(InstanceIds=[i['InstanceId'] for i in instance_res['ActiveInstances']])
for instance in instances['Reservations'][0]['Instances']:
for tag in tag_spec[0]['Tags']:
instance['Tags'].should.contain(tag)
@mock_ec2 @mock_ec2
def test_cancel_spot_fleet_request(): def test_cancel_spot_fleet_request():
conn = boto3.client("ec2", region_name='us-west-2') conn = boto3.client("ec2", region_name='us-west-2')

View File

@ -306,6 +306,7 @@ def test_create_policy_versions():
PolicyDocument='{"some":"policy"}') PolicyDocument='{"some":"policy"}')
version.get('PolicyVersion').get('Document').should.equal({'some': 'policy'}) version.get('PolicyVersion').get('Document').should.equal({'some': 'policy'})
@mock_iam @mock_iam
def test_get_policy(): def test_get_policy():
conn = boto3.client('iam', region_name='us-east-1') conn = boto3.client('iam', region_name='us-east-1')
@ -579,6 +580,7 @@ def test_get_credential_report():
'get_credential_report_result']['content'].encode('ascii')).decode('ascii') 'get_credential_report_result']['content'].encode('ascii')).decode('ascii')
report.should.match(r'.*my-user.*') report.should.match(r'.*my-user.*')
@mock_iam @mock_iam
def test_boto3_get_credential_report(): def test_boto3_get_credential_report():
conn = boto3.client('iam', region_name='us-east-1') conn = boto3.client('iam', region_name='us-east-1')
@ -757,14 +759,7 @@ def test_get_access_key_last_used():
@mock_iam @mock_iam
def test_get_account_authorization_details(): def test_get_account_authorization_details():
import json import json
conn = boto3.client('iam', region_name='us-east-1') test_policy = json.dumps({
conn.create_role(RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="/my-path/")
conn.create_user(Path='/', UserName='testUser')
conn.create_group(Path='/', GroupName='testGroup')
conn.create_policy(
PolicyName='testPolicy',
Path='/',
PolicyDocument=json.dumps({
"Version": "2012-10-17", "Version": "2012-10-17",
"Statement": [ "Statement": [
{ {
@ -773,12 +768,43 @@ def test_get_account_authorization_details():
"Effect": "Allow", "Effect": "Allow",
} }
] ]
}), })
conn = boto3.client('iam', region_name='us-east-1')
conn.create_role(RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="/my-path/")
conn.create_user(Path='/', UserName='testUser')
conn.create_group(Path='/', GroupName='testGroup')
conn.create_policy(
PolicyName='testPolicy',
Path='/',
PolicyDocument=test_policy,
Description='Test Policy' Description='Test Policy'
) )
# Attach things to the user and group:
conn.put_user_policy(UserName='testUser', PolicyName='testPolicy', PolicyDocument=test_policy)
conn.put_group_policy(GroupName='testGroup', PolicyName='testPolicy', PolicyDocument=test_policy)
conn.attach_user_policy(UserName='testUser', PolicyArn='arn:aws:iam::123456789012:policy/testPolicy')
conn.attach_group_policy(GroupName='testGroup', PolicyArn='arn:aws:iam::123456789012:policy/testPolicy')
conn.add_user_to_group(UserName='testUser', GroupName='testGroup')
# Add things to the role:
conn.create_instance_profile(InstanceProfileName='ipn') conn.create_instance_profile(InstanceProfileName='ipn')
conn.add_role_to_instance_profile(InstanceProfileName='ipn', RoleName='my-role') conn.add_role_to_instance_profile(InstanceProfileName='ipn', RoleName='my-role')
conn.tag_role(RoleName='my-role', Tags=[
{
'Key': 'somekey',
'Value': 'somevalue'
},
{
'Key': 'someotherkey',
'Value': 'someothervalue'
}
])
conn.put_role_policy(RoleName='my-role', PolicyName='test-policy', PolicyDocument=test_policy)
conn.attach_role_policy(RoleName='my-role', PolicyArn='arn:aws:iam::123456789012:policy/testPolicy')
result = conn.get_account_authorization_details(Filter=['Role']) result = conn.get_account_authorization_details(Filter=['Role'])
assert len(result['RoleDetailList']) == 1 assert len(result['RoleDetailList']) == 1
@ -786,24 +812,41 @@ def test_get_account_authorization_details():
assert len(result['GroupDetailList']) == 0 assert len(result['GroupDetailList']) == 0
assert len(result['Policies']) == 0 assert len(result['Policies']) == 0
assert len(result['RoleDetailList'][0]['InstanceProfileList']) == 1 assert len(result['RoleDetailList'][0]['InstanceProfileList']) == 1
assert len(result['RoleDetailList'][0]['Tags']) == 2
assert len(result['RoleDetailList'][0]['RolePolicyList']) == 1
assert len(result['RoleDetailList'][0]['AttachedManagedPolicies']) == 1
assert result['RoleDetailList'][0]['AttachedManagedPolicies'][0]['PolicyName'] == 'testPolicy'
assert result['RoleDetailList'][0]['AttachedManagedPolicies'][0]['PolicyArn'] == \
'arn:aws:iam::123456789012:policy/testPolicy'
result = conn.get_account_authorization_details(Filter=['User']) result = conn.get_account_authorization_details(Filter=['User'])
assert len(result['RoleDetailList']) == 0 assert len(result['RoleDetailList']) == 0
assert len(result['UserDetailList']) == 1 assert len(result['UserDetailList']) == 1
assert len(result['UserDetailList'][0]['GroupList']) == 1
assert len(result['UserDetailList'][0]['AttachedManagedPolicies']) == 1
assert len(result['GroupDetailList']) == 0 assert len(result['GroupDetailList']) == 0
assert len(result['Policies']) == 0 assert len(result['Policies']) == 0
assert result['UserDetailList'][0]['AttachedManagedPolicies'][0]['PolicyName'] == 'testPolicy'
assert result['UserDetailList'][0]['AttachedManagedPolicies'][0]['PolicyArn'] == \
'arn:aws:iam::123456789012:policy/testPolicy'
result = conn.get_account_authorization_details(Filter=['Group']) result = conn.get_account_authorization_details(Filter=['Group'])
assert len(result['RoleDetailList']) == 0 assert len(result['RoleDetailList']) == 0
assert len(result['UserDetailList']) == 0 assert len(result['UserDetailList']) == 0
assert len(result['GroupDetailList']) == 1 assert len(result['GroupDetailList']) == 1
assert len(result['GroupDetailList'][0]['GroupPolicyList']) == 1
assert len(result['GroupDetailList'][0]['AttachedManagedPolicies']) == 1
assert len(result['Policies']) == 0 assert len(result['Policies']) == 0
assert result['GroupDetailList'][0]['AttachedManagedPolicies'][0]['PolicyName'] == 'testPolicy'
assert result['GroupDetailList'][0]['AttachedManagedPolicies'][0]['PolicyArn'] == \
'arn:aws:iam::123456789012:policy/testPolicy'
result = conn.get_account_authorization_details(Filter=['LocalManagedPolicy']) result = conn.get_account_authorization_details(Filter=['LocalManagedPolicy'])
assert len(result['RoleDetailList']) == 0 assert len(result['RoleDetailList']) == 0
assert len(result['UserDetailList']) == 0 assert len(result['UserDetailList']) == 0
assert len(result['GroupDetailList']) == 0 assert len(result['GroupDetailList']) == 0
assert len(result['Policies']) == 1 assert len(result['Policies']) == 1
assert len(result['Policies'][0]['PolicyVersionList']) == 1
# Check for greater than 1 since this should always be greater than one but might change. # Check for greater than 1 since this should always be greater than one but might change.
# See iam/aws_managed_policies.py # See iam/aws_managed_policies.py
@ -872,6 +915,7 @@ def test_signing_certs():
with assert_raises(ClientError): with assert_raises(ClientError):
client.delete_signing_certificate(UserName='notauser', CertificateId=cert_id) client.delete_signing_certificate(UserName='notauser', CertificateId=cert_id)
@mock_iam() @mock_iam()
def test_create_saml_provider(): def test_create_saml_provider():
conn = boto3.client('iam', region_name='us-east-1') conn = boto3.client('iam', region_name='us-east-1')
@ -881,6 +925,7 @@ def test_create_saml_provider():
) )
response['SAMLProviderArn'].should.equal("arn:aws:iam::123456789012:saml-provider/TestSAMLProvider") response['SAMLProviderArn'].should.equal("arn:aws:iam::123456789012:saml-provider/TestSAMLProvider")
@mock_iam() @mock_iam()
def test_get_saml_provider(): def test_get_saml_provider():
conn = boto3.client('iam', region_name='us-east-1') conn = boto3.client('iam', region_name='us-east-1')
@ -893,6 +938,7 @@ def test_get_saml_provider():
) )
response['SAMLMetadataDocument'].should.equal('a' * 1024) response['SAMLMetadataDocument'].should.equal('a' * 1024)
@mock_iam() @mock_iam()
def test_list_saml_providers(): def test_list_saml_providers():
conn = boto3.client('iam', region_name='us-east-1') conn = boto3.client('iam', region_name='us-east-1')
@ -903,6 +949,7 @@ def test_list_saml_providers():
response = conn.list_saml_providers() response = conn.list_saml_providers()
response['SAMLProviderList'][0]['Arn'].should.equal("arn:aws:iam::123456789012:saml-provider/TestSAMLProvider") response['SAMLProviderList'][0]['Arn'].should.equal("arn:aws:iam::123456789012:saml-provider/TestSAMLProvider")
@mock_iam() @mock_iam()
def test_delete_saml_provider(): def test_delete_saml_provider():
conn = boto3.client('iam', region_name='us-east-1') conn = boto3.client('iam', region_name='us-east-1')
@ -929,3 +976,178 @@ def test_delete_saml_provider():
# Verify that it's not in the list: # Verify that it's not in the list:
resp = conn.list_signing_certificates(UserName='testing') resp = conn.list_signing_certificates(UserName='testing')
assert not resp['Certificates'] assert not resp['Certificates']
@mock_iam()
def test_tag_role():
"""Tests both the tag_role and get_role_tags capability"""
conn = boto3.client('iam', region_name='us-east-1')
conn.create_role(RoleName="my-role", AssumeRolePolicyDocument="{}")
# Get without tags:
role = conn.get_role(RoleName='my-role')['Role']
assert not role.get('Tags')
# With proper tag values:
conn.tag_role(RoleName='my-role', Tags=[
{
'Key': 'somekey',
'Value': 'somevalue'
},
{
'Key': 'someotherkey',
'Value': 'someothervalue'
}
])
# Get role:
role = conn.get_role(RoleName='my-role')['Role']
assert len(role['Tags']) == 2
assert role['Tags'][0]['Key'] == 'somekey'
assert role['Tags'][0]['Value'] == 'somevalue'
assert role['Tags'][1]['Key'] == 'someotherkey'
assert role['Tags'][1]['Value'] == 'someothervalue'
# Same -- but for list_role_tags:
tags = conn.list_role_tags(RoleName='my-role')
assert len(tags['Tags']) == 2
assert role['Tags'][0]['Key'] == 'somekey'
assert role['Tags'][0]['Value'] == 'somevalue'
assert role['Tags'][1]['Key'] == 'someotherkey'
assert role['Tags'][1]['Value'] == 'someothervalue'
assert not tags['IsTruncated']
assert not tags.get('Marker')
# Test pagination:
tags = conn.list_role_tags(RoleName='my-role', MaxItems=1)
assert len(tags['Tags']) == 1
assert tags['IsTruncated']
assert tags['Tags'][0]['Key'] == 'somekey'
assert tags['Tags'][0]['Value'] == 'somevalue'
assert tags['Marker'] == '1'
tags = conn.list_role_tags(RoleName='my-role', Marker=tags['Marker'])
assert len(tags['Tags']) == 1
assert tags['Tags'][0]['Key'] == 'someotherkey'
assert tags['Tags'][0]['Value'] == 'someothervalue'
assert not tags['IsTruncated']
assert not tags.get('Marker')
# Test updating an existing tag:
conn.tag_role(RoleName='my-role', Tags=[
{
'Key': 'somekey',
'Value': 'somenewvalue'
}
])
tags = conn.list_role_tags(RoleName='my-role')
assert len(tags['Tags']) == 2
assert tags['Tags'][0]['Key'] == 'somekey'
assert tags['Tags'][0]['Value'] == 'somenewvalue'
# Empty is good:
conn.tag_role(RoleName='my-role', Tags=[
{
'Key': 'somekey',
'Value': ''
}
])
tags = conn.list_role_tags(RoleName='my-role')
assert len(tags['Tags']) == 2
assert tags['Tags'][0]['Key'] == 'somekey'
assert tags['Tags'][0]['Value'] == ''
# Test creating tags with invalid values:
# With more than 50 tags:
with assert_raises(ClientError) as ce:
too_many_tags = list(map(lambda x: {'Key': str(x), 'Value': str(x)}, range(0, 51)))
conn.tag_role(RoleName='my-role', Tags=too_many_tags)
assert 'failed to satisfy constraint: Member must have length less than or equal to 50.' \
in ce.exception.response['Error']['Message']
# With a duplicate tag:
with assert_raises(ClientError) as ce:
conn.tag_role(RoleName='my-role', Tags=[{'Key': '0', 'Value': ''}, {'Key': '0', 'Value': ''}])
assert 'Duplicate tag keys found. Please note that Tag keys are case insensitive.' \
in ce.exception.response['Error']['Message']
# Duplicate tag with different casing:
with assert_raises(ClientError) as ce:
conn.tag_role(RoleName='my-role', Tags=[{'Key': 'a', 'Value': ''}, {'Key': 'A', 'Value': ''}])
assert 'Duplicate tag keys found. Please note that Tag keys are case insensitive.' \
in ce.exception.response['Error']['Message']
# With a really big key:
with assert_raises(ClientError) as ce:
conn.tag_role(RoleName='my-role', Tags=[{'Key': '0' * 129, 'Value': ''}])
assert 'Member must have length less than or equal to 128.' in ce.exception.response['Error']['Message']
# With a really big value:
with assert_raises(ClientError) as ce:
conn.tag_role(RoleName='my-role', Tags=[{'Key': '0', 'Value': '0' * 257}])
assert 'Member must have length less than or equal to 256.' in ce.exception.response['Error']['Message']
# With an invalid character:
with assert_raises(ClientError) as ce:
conn.tag_role(RoleName='my-role', Tags=[{'Key': 'NOWAY!', 'Value': ''}])
assert 'Member must satisfy regular expression pattern: [\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]+' \
in ce.exception.response['Error']['Message']
# With a role that doesn't exist:
with assert_raises(ClientError):
conn.tag_role(RoleName='notarole', Tags=[{'Key': 'some', 'Value': 'value'}])
@mock_iam
def test_untag_role():
conn = boto3.client('iam', region_name='us-east-1')
conn.create_role(RoleName="my-role", AssumeRolePolicyDocument="{}")
# With proper tag values:
conn.tag_role(RoleName='my-role', Tags=[
{
'Key': 'somekey',
'Value': 'somevalue'
},
{
'Key': 'someotherkey',
'Value': 'someothervalue'
}
])
# Remove them:
conn.untag_role(RoleName='my-role', TagKeys=['somekey'])
tags = conn.list_role_tags(RoleName='my-role')
assert len(tags['Tags']) == 1
assert tags['Tags'][0]['Key'] == 'someotherkey'
assert tags['Tags'][0]['Value'] == 'someothervalue'
# And again:
conn.untag_role(RoleName='my-role', TagKeys=['someotherkey'])
tags = conn.list_role_tags(RoleName='my-role')
assert not tags['Tags']
# Test removing tags with invalid values:
# With more than 50 tags:
with assert_raises(ClientError) as ce:
conn.untag_role(RoleName='my-role', TagKeys=[str(x) for x in range(0, 51)])
assert 'failed to satisfy constraint: Member must have length less than or equal to 50.' \
in ce.exception.response['Error']['Message']
assert 'tagKeys' in ce.exception.response['Error']['Message']
# With a really big key:
with assert_raises(ClientError) as ce:
conn.untag_role(RoleName='my-role', TagKeys=['0' * 129])
assert 'Member must have length less than or equal to 128.' in ce.exception.response['Error']['Message']
assert 'tagKeys' in ce.exception.response['Error']['Message']
# With an invalid character:
with assert_raises(ClientError) as ce:
conn.untag_role(RoleName='my-role', TagKeys=['NOWAY!'])
assert 'Member must satisfy regular expression pattern: [\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]+' \
in ce.exception.response['Error']['Message']
assert 'tagKeys' in ce.exception.response['Error']['Message']
# With a role that doesn't exist:
with assert_raises(ClientError):
conn.untag_role(RoleName='notarole', TagKeys=['somevalue'])