Iam cloudformation update, singificant cloudformation refactoring (#3218)

* IAM User Cloudformation Enhancements: update, delete, getatt.

* AWS::IAM::Policy Support

* Added unit tests for AWS:IAM:Policy for roles and groups.  Fixed bug related to groups.

* AWS:IAM:AccessKey CloudFormation support.

* Refactor of CloudFormation parsing.py methods to simplify and standardize how they call to the models.  Adjusted some models accordingly.

* Further model CloudFormation support changes to align with revised CloudFormation logic.  Mostly avoidance of getting resoure name from properties.

* Support for Kinesis Stream RetentionPeriodHours param.

* Kinesis Stream Cloudformation Tag Support.

* Added omitted 'region' param to boto3.client() calls in new tests.

Co-authored-by: Joseph Weitekamp <jweite@amazon.com>
This commit is contained in:
jweite 2020-08-27 05:11:47 -04:00 committed by GitHub
parent 3b06ce689e
commit 49d92861c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1914 additions and 320 deletions

View File

@ -702,10 +702,13 @@ class EventSourceMapping(CloudFormationModel):
) )
for esm in esms: for esm in esms:
if esm.logical_resource_id in resource_name: if esm.uuid == resource_name:
lambda_backend.delete_event_source_mapping
esm.delete(region_name) esm.delete(region_name)
@property
def physical_resource_id(self):
return self.uuid
class LambdaVersion(CloudFormationModel): class LambdaVersion(CloudFormationModel):
def __init__(self, spec): def __init__(self, spec):

View File

@ -246,12 +246,14 @@ def generate_resource_name(resource_type, stack_name, logical_id):
return "{0}{1}".format( return "{0}{1}".format(
stack_name[:max_stack_name_portion_len], right_hand_part_of_name stack_name[:max_stack_name_portion_len], right_hand_part_of_name
).lower() ).lower()
elif resource_type == "AWS::IAM::Policy":
return "{0}-{1}-{2}".format(stack_name[:5], logical_id[:4], random_suffix())
else: else:
return "{0}-{1}-{2}".format(stack_name, logical_id, random_suffix()) return "{0}-{1}-{2}".format(stack_name, logical_id, random_suffix())
def parse_resource( def parse_resource(
logical_id, resource_json, resources_map, add_name_to_resource_json=True resource_json, resources_map,
): ):
resource_type = resource_json["Type"] resource_type = resource_json["Type"]
resource_class = resource_class_from_type(resource_type) resource_class = resource_class_from_type(resource_type)
@ -263,21 +265,37 @@ def parse_resource(
) )
return None return None
if "Properties" not in resource_json:
resource_json["Properties"] = {}
resource_json = clean_json(resource_json, resources_map) resource_json = clean_json(resource_json, resources_map)
resource_name = generate_resource_name(
return resource_class, resource_json, resource_type
def parse_resource_and_generate_name(
logical_id, resource_json, resources_map,
):
resource_tuple = parse_resource(resource_json, resources_map)
if not resource_tuple:
return None
resource_class, resource_json, resource_type = resource_tuple
generated_resource_name = generate_resource_name(
resource_type, resources_map.get("AWS::StackName"), logical_id resource_type, resources_map.get("AWS::StackName"), logical_id
) )
resource_name_property = resource_name_property_from_type(resource_type) resource_name_property = resource_name_property_from_type(resource_type)
if resource_name_property: if resource_name_property:
if "Properties" not in resource_json:
resource_json["Properties"] = dict()
if ( if (
add_name_to_resource_json "Properties" in resource_json
and resource_name_property not in resource_json["Properties"] and resource_name_property in resource_json["Properties"]
): ):
resource_json["Properties"][resource_name_property] = resource_name
if resource_name_property in resource_json["Properties"]:
resource_name = resource_json["Properties"][resource_name_property] resource_name = resource_json["Properties"][resource_name_property]
else:
resource_name = generated_resource_name
else:
resource_name = generated_resource_name
return resource_class, resource_json, resource_name return resource_class, resource_json, resource_name
@ -289,12 +307,14 @@ def parse_and_create_resource(logical_id, resource_json, resources_map, region_n
return None return None
resource_type = resource_json["Type"] resource_type = resource_json["Type"]
resource_tuple = parse_resource(logical_id, resource_json, resources_map) resource_tuple = parse_resource_and_generate_name(
logical_id, resource_json, resources_map
)
if not resource_tuple: if not resource_tuple:
return None return None
resource_class, resource_json, resource_name = resource_tuple resource_class, resource_json, resource_physical_name = resource_tuple
resource = resource_class.create_from_cloudformation_json( resource = resource_class.create_from_cloudformation_json(
resource_name, resource_json, region_name resource_physical_name, resource_json, region_name
) )
resource.type = resource_type resource.type = resource_type
resource.logical_resource_id = logical_id resource.logical_resource_id = logical_id
@ -302,28 +322,34 @@ def parse_and_create_resource(logical_id, resource_json, resources_map, region_n
def parse_and_update_resource(logical_id, resource_json, resources_map, region_name): def parse_and_update_resource(logical_id, resource_json, resources_map, region_name):
resource_class, new_resource_json, new_resource_name = parse_resource( resource_class, resource_json, new_resource_name = parse_resource_and_generate_name(
logical_id, resource_json, resources_map, False
)
original_resource = resources_map[logical_id]
new_resource = resource_class.update_from_cloudformation_json(
original_resource=original_resource,
new_resource_name=new_resource_name,
cloudformation_json=new_resource_json,
region_name=region_name,
)
new_resource.type = resource_json["Type"]
new_resource.logical_resource_id = logical_id
return new_resource
def parse_and_delete_resource(logical_id, resource_json, resources_map, region_name):
resource_class, resource_json, resource_name = parse_resource(
logical_id, resource_json, resources_map logical_id, resource_json, resources_map
) )
resource_class.delete_from_cloudformation_json( original_resource = resources_map[logical_id]
resource_name, resource_json, region_name if not hasattr(
) resource_class.update_from_cloudformation_json, "__isabstractmethod__"
):
new_resource = resource_class.update_from_cloudformation_json(
original_resource=original_resource,
new_resource_name=new_resource_name,
cloudformation_json=resource_json,
region_name=region_name,
)
new_resource.type = resource_json["Type"]
new_resource.logical_resource_id = logical_id
return new_resource
else:
return None
def parse_and_delete_resource(resource_name, resource_json, resources_map, region_name):
resource_class, resource_json, _ = parse_resource(resource_json, resources_map)
if not hasattr(
resource_class.delete_from_cloudformation_json, "__isabstractmethod__"
):
resource_class.delete_from_cloudformation_json(
resource_name, resource_json, region_name
)
def parse_condition(condition, resources_map, condition_map): def parse_condition(condition, resources_map, condition_map):
@ -614,28 +640,36 @@ class ResourceMap(collections_abc.Mapping):
) )
self._parsed_resources[resource_name] = new_resource self._parsed_resources[resource_name] = new_resource
for resource_name, resource in resources_by_action["Remove"].items(): for logical_name, _ in resources_by_action["Remove"].items():
resource_json = old_template[resource_name] resource_json = old_template[logical_name]
resource = self._parsed_resources[logical_name]
# ToDo: Standardize this.
if hasattr(resource, "physical_resource_id"):
resource_name = self._parsed_resources[
logical_name
].physical_resource_id
else:
resource_name = None
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(logical_name)
tries = 1 tries = 1
while resources_by_action["Modify"] and tries < 5: while resources_by_action["Modify"] and tries < 5:
for resource_name, resource in resources_by_action["Modify"].copy().items(): for logical_name, _ in resources_by_action["Modify"].copy().items():
resource_json = new_template[resource_name] resource_json = new_template[logical_name]
try: try:
changed_resource = parse_and_update_resource( changed_resource = parse_and_update_resource(
resource_name, resource_json, self, self._region_name logical_name, resource_json, self, self._region_name
) )
except Exception as e: except Exception as e:
# skip over dependency violations, and try again in a # skip over dependency violations, and try again in a
# second pass # second pass
last_exception = e last_exception = e
else: else:
self._parsed_resources[resource_name] = changed_resource self._parsed_resources[logical_name] = changed_resource
del resources_by_action["Modify"][resource_name] del resources_by_action["Modify"][logical_name]
tries += 1 tries += 1
if tries == 5: if tries == 5:
raise last_exception raise last_exception
@ -650,22 +684,20 @@ class ResourceMap(collections_abc.Mapping):
if parsed_resource and hasattr(parsed_resource, "delete"): if parsed_resource and hasattr(parsed_resource, "delete"):
parsed_resource.delete(self._region_name) parsed_resource.delete(self._region_name)
else: else:
resource_name_attribute = ( if hasattr(parsed_resource, "physical_resource_id"):
parsed_resource.cloudformation_name_type() resource_name = parsed_resource.physical_resource_id
if hasattr(parsed_resource, "cloudformation_name_type") else:
else resource_name_property_from_type(parsed_resource.type) resource_name = None
resource_json = self._resource_json_map[
parsed_resource.logical_resource_id
]
parse_and_delete_resource(
resource_name, resource_json, self, self._region_name,
) )
if resource_name_attribute:
resource_json = self._resource_json_map[ self._parsed_resources.pop(parsed_resource.logical_resource_id)
parsed_resource.logical_resource_id
]
resource_name = resource_json["Properties"][
resource_name_attribute
]
parse_and_delete_resource(
resource_name, resource_json, self, self._region_name
)
self._parsed_resources.pop(parsed_resource.logical_resource_id)
except Exception as e: except Exception as e:
# skip over dependency violations, and try again in a # skip over dependency violations, and try again in a
# second pass # second pass

View File

@ -511,10 +511,9 @@ class LogGroup(CloudFormationModel):
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
log_group_name = properties["LogGroupName"]
tags = properties.get("Tags", {}) tags = properties.get("Tags", {})
return logs_backends[region_name].create_log_group( return logs_backends[region_name].create_log_group(
log_group_name, tags, **properties resource_name, tags, **properties
) )

View File

@ -90,9 +90,9 @@ class Pipeline(CloudFormationModel):
datapipeline_backend = datapipeline_backends[region_name] datapipeline_backend = datapipeline_backends[region_name]
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
cloudformation_unique_id = "cf-" + properties["Name"] cloudformation_unique_id = "cf-" + resource_name
pipeline = datapipeline_backend.create_pipeline( pipeline = datapipeline_backend.create_pipeline(
properties["Name"], cloudformation_unique_id resource_name, cloudformation_unique_id
) )
datapipeline_backend.put_pipeline_definition( datapipeline_backend.put_pipeline_definition(
pipeline.pipeline_id, properties["PipelineObjects"] pipeline.pipeline_id, properties["PipelineObjects"]

View File

@ -461,7 +461,7 @@ class Table(CloudFormationModel):
params["streams"] = properties["StreamSpecification"] params["streams"] = properties["StreamSpecification"]
table = dynamodb_backends[region_name].create_table( table = dynamodb_backends[region_name].create_table(
name=properties["TableName"], **params name=resource_name, **params
) )
return table return table
@ -469,11 +469,7 @@ class Table(CloudFormationModel):
def delete_from_cloudformation_json( def delete_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] table = dynamodb_backends[region_name].delete_table(name=resource_name)
table = dynamodb_backends[region_name].delete_table(
name=properties["TableName"]
)
return table return table
def _generate_arn(self, name): def _generate_arn(self, name):

View File

@ -80,15 +80,11 @@ class Repository(BaseObject, CloudFormationModel):
def create_from_cloudformation_json( def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"]
ecr_backend = ecr_backends[region_name] ecr_backend = ecr_backends[region_name]
return ecr_backend.create_repository( return ecr_backend.create_repository(
# RepositoryName is optional in CloudFormation, thus create a random # RepositoryName is optional in CloudFormation, thus create a random
# name if necessary # name if necessary
repository_name=properties.get( repository_name=resource_name
"RepositoryName", "ecrrepository{0}".format(int(random() * 10 ** 6))
)
) )
@classmethod @classmethod

View File

@ -82,36 +82,24 @@ class Cluster(BaseObject, CloudFormationModel):
def create_from_cloudformation_json( def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
# if properties is not provided, cloudformation will use the default values for all properties
if "Properties" in cloudformation_json:
properties = cloudformation_json["Properties"]
else:
properties = {}
ecs_backend = ecs_backends[region_name] ecs_backend = ecs_backends[region_name]
return ecs_backend.create_cluster( return ecs_backend.create_cluster(
# ClusterName is optional in CloudFormation, thus create a random # ClusterName is optional in CloudFormation, thus create a random
# name if necessary # name if necessary
cluster_name=properties.get( cluster_name=resource_name
"ClusterName", "ecscluster{0}".format(int(random() * 10 ** 6))
)
) )
@classmethod @classmethod
def update_from_cloudformation_json( def update_from_cloudformation_json(
cls, original_resource, new_resource_name, cloudformation_json, region_name cls, original_resource, new_resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] if original_resource.name != new_resource_name:
if original_resource.name != properties["ClusterName"]:
ecs_backend = ecs_backends[region_name] ecs_backend = ecs_backends[region_name]
ecs_backend.delete_cluster(original_resource.arn) ecs_backend.delete_cluster(original_resource.arn)
return ecs_backend.create_cluster( return ecs_backend.create_cluster(
# ClusterName is optional in CloudFormation, thus create a # ClusterName is optional in CloudFormation, thus create a
# random name if necessary # random name if necessary
cluster_name=properties.get( cluster_name=new_resource_name
"ClusterName", "ecscluster{0}".format(int(random() * 10 ** 6))
)
) )
else: else:
# no-op when nothing changed between old and new resources # no-op when nothing changed between old and new resources
@ -355,14 +343,13 @@ class Service(BaseObject, CloudFormationModel):
task_definition = properties["TaskDefinition"].family task_definition = properties["TaskDefinition"].family
else: else:
task_definition = properties["TaskDefinition"] task_definition = properties["TaskDefinition"]
service_name = "{0}Service{1}".format(cluster, int(random() * 10 ** 6))
desired_count = properties["DesiredCount"] desired_count = properties["DesiredCount"]
# TODO: LoadBalancers # TODO: LoadBalancers
# TODO: Role # TODO: Role
ecs_backend = ecs_backends[region_name] ecs_backend = ecs_backends[region_name]
return ecs_backend.create_service( return ecs_backend.create_service(
cluster, service_name, desired_count, task_definition_str=task_definition cluster, resource_name, desired_count, task_definition_str=task_definition
) )
@classmethod @classmethod
@ -386,12 +373,9 @@ class Service(BaseObject, CloudFormationModel):
# TODO: LoadBalancers # TODO: LoadBalancers
# TODO: Role # TODO: Role
ecs_backend.delete_service(cluster_name, service_name) ecs_backend.delete_service(cluster_name, service_name)
new_service_name = "{0}Service{1}".format(
cluster_name, int(random() * 10 ** 6)
)
return ecs_backend.create_service( return ecs_backend.create_service(
cluster_name, cluster_name,
new_service_name, new_resource_name,
desired_count, desired_count,
task_definition_str=task_definition, task_definition_str=task_definition,
) )

View File

@ -160,7 +160,6 @@ class FakeTargetGroup(CloudFormationModel):
elbv2_backend = elbv2_backends[region_name] elbv2_backend = elbv2_backends[region_name]
name = properties.get("Name")
vpc_id = properties.get("VpcId") vpc_id = properties.get("VpcId")
protocol = properties.get("Protocol") protocol = properties.get("Protocol")
port = properties.get("Port") port = properties.get("Port")
@ -175,7 +174,7 @@ class FakeTargetGroup(CloudFormationModel):
target_type = properties.get("TargetType") target_type = properties.get("TargetType")
target_group = elbv2_backend.create_target_group( target_group = elbv2_backend.create_target_group(
name=name, name=resource_name,
vpc_id=vpc_id, vpc_id=vpc_id,
protocol=protocol, protocol=protocol,
port=port, port=port,
@ -437,13 +436,12 @@ class FakeLoadBalancer(CloudFormationModel):
elbv2_backend = elbv2_backends[region_name] elbv2_backend = elbv2_backends[region_name]
name = properties.get("Name", resource_name)
security_groups = properties.get("SecurityGroups") security_groups = properties.get("SecurityGroups")
subnet_ids = properties.get("Subnets") subnet_ids = properties.get("Subnets")
scheme = properties.get("Scheme", "internet-facing") scheme = properties.get("Scheme", "internet-facing")
load_balancer = elbv2_backend.create_load_balancer( load_balancer = elbv2_backend.create_load_balancer(
name, security_groups, subnet_ids, scheme=scheme resource_name, security_groups, subnet_ids, scheme=scheme
) )
return load_balancer return load_balancer

View File

@ -88,7 +88,7 @@ class Rule(CloudFormationModel):
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
event_backend = events_backends[region_name] event_backend = events_backends[region_name]
event_name = properties.get("Name") or resource_name event_name = resource_name
return event_backend.put_rule(name=event_name, **properties) return event_backend.put_rule(name=event_name, **properties)
@classmethod @classmethod
@ -104,9 +104,8 @@ class Rule(CloudFormationModel):
def delete_from_cloudformation_json( def delete_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"]
event_backend = events_backends[region_name] event_backend = events_backends[region_name]
event_name = properties.get("Name") or resource_name event_name = resource_name
event_backend.delete_rule(name=event_name) event_backend.delete_rule(name=event_name)
@ -176,7 +175,7 @@ class EventBus(CloudFormationModel):
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
event_backend = events_backends[region_name] event_backend = events_backends[region_name]
event_name = properties["Name"] event_name = resource_name
event_source_name = properties.get("EventSourceName") event_source_name = properties.get("EventSourceName")
return event_backend.create_event_bus( return event_backend.create_event_bus(
name=event_name, event_source_name=event_source_name name=event_name, event_source_name=event_source_name
@ -195,9 +194,8 @@ class EventBus(CloudFormationModel):
def delete_from_cloudformation_json( def delete_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"]
event_backend = events_backends[region_name] event_backend = events_backends[region_name]
event_bus_name = properties["Name"] event_bus_name = resource_name
event_backend.delete_event_bus(event_bus_name) event_backend.delete_event_bus(event_bus_name)

View File

@ -12,7 +12,6 @@ import re
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from six.moves.urllib.parse import urlparse from six.moves.urllib.parse import urlparse
from uuid import uuid4
from moto.core.exceptions import RESTError from moto.core.exceptions import RESTError
from moto.core import BaseBackend, BaseModel, ACCOUNT_ID, CloudFormationModel from moto.core import BaseBackend, BaseModel, ACCOUNT_ID, CloudFormationModel
@ -84,7 +83,11 @@ class VirtualMfaDevice(object):
return iso_8601_datetime_without_milliseconds(self.enable_date) return iso_8601_datetime_without_milliseconds(self.enable_date)
class Policy(BaseModel): class Policy(CloudFormationModel):
# Note: This class does not implement the CloudFormation support for AWS::IAM::Policy, as that CF resource
# is for creating *inline* policies. That is done in class InlinePolicy.
is_attachable = False is_attachable = False
def __init__( def __init__(
@ -295,8 +298,149 @@ aws_managed_policies = [
] ]
class InlinePolicy(Policy): class InlinePolicy(CloudFormationModel):
"""TODO: is this needed?""" # Represents an Inline Policy created by CloudFormation
def __init__(
self,
resource_name,
policy_name,
policy_document,
group_names,
role_names,
user_names,
):
self.name = resource_name
self.policy_name = None
self.policy_document = None
self.group_names = None
self.role_names = None
self.user_names = None
self.update(policy_name, policy_document, group_names, role_names, user_names)
def update(
self, policy_name, policy_document, group_names, role_names, user_names,
):
self.policy_name = policy_name
self.policy_document = (
json.dumps(policy_document)
if isinstance(policy_document, dict)
else policy_document
)
self.group_names = group_names
self.role_names = role_names
self.user_names = user_names
@staticmethod
def cloudformation_name_type():
return None # Resource never gets named after by template PolicyName!
@staticmethod
def cloudformation_type():
return "AWS::IAM::Policy"
@classmethod
def create_from_cloudformation_json(
cls, resource_physical_name, cloudformation_json, region_name
):
properties = cloudformation_json.get("Properties", {})
policy_document = properties.get("PolicyDocument")
policy_name = properties.get("PolicyName")
user_names = properties.get("Users")
role_names = properties.get("Roles")
group_names = properties.get("Groups")
return iam_backend.create_inline_policy(
resource_physical_name,
policy_name,
policy_document,
group_names,
role_names,
user_names,
)
@classmethod
def update_from_cloudformation_json(
cls, original_resource, new_resource_name, cloudformation_json, region_name,
):
properties = cloudformation_json["Properties"]
if cls.is_replacement_update(properties):
resource_name_property = cls.cloudformation_name_type()
if resource_name_property not in properties:
properties[resource_name_property] = new_resource_name
new_resource = cls.create_from_cloudformation_json(
properties[resource_name_property], cloudformation_json, region_name
)
properties[resource_name_property] = original_resource.name
cls.delete_from_cloudformation_json(
original_resource.name, cloudformation_json, region_name
)
return new_resource
else: # No Interruption
properties = cloudformation_json.get("Properties", {})
policy_document = properties.get("PolicyDocument")
policy_name = properties.get("PolicyName", original_resource.name)
user_names = properties.get("Users")
role_names = properties.get("Roles")
group_names = properties.get("Groups")
return iam_backend.update_inline_policy(
original_resource.name,
policy_name,
policy_document,
group_names,
role_names,
user_names,
)
@classmethod
def delete_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
):
iam_backend.delete_inline_policy(resource_name)
@staticmethod
def is_replacement_update(properties):
properties_requiring_replacement_update = []
return any(
[
property_requiring_replacement in properties
for property_requiring_replacement in properties_requiring_replacement_update
]
)
@property
def physical_resource_id(self):
return self.name
def apply_policy(self, backend):
if self.user_names:
for user_name in self.user_names:
backend.put_user_policy(
user_name, self.policy_name, self.policy_document
)
if self.role_names:
for role_name in self.role_names:
backend.put_role_policy(
role_name, self.policy_name, self.policy_document
)
if self.group_names:
for group_name in self.group_names:
backend.put_group_policy(
group_name, self.policy_name, self.policy_document
)
def unapply_policy(self, backend):
if self.user_names:
for user_name in self.user_names:
backend.delete_user_policy(user_name, self.policy_name)
if self.role_names:
for role_name in self.role_names:
backend.delete_role_policy(role_name, self.policy_name)
if self.group_names:
for group_name in self.group_names:
backend.delete_group_policy(group_name, self.policy_name)
class Role(CloudFormationModel): class Role(CloudFormationModel):
@ -338,11 +482,13 @@ class Role(CloudFormationModel):
@classmethod @classmethod
def create_from_cloudformation_json( def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name cls, resource_physical_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
role_name = ( role_name = (
properties["RoleName"] if "RoleName" in properties else str(uuid4())[0:5] properties["RoleName"]
if "RoleName" in properties
else resource_physical_name
) )
role = iam_backend.create_role( role = iam_backend.create_role(
@ -416,13 +562,15 @@ class InstanceProfile(CloudFormationModel):
@classmethod @classmethod
def create_from_cloudformation_json( def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name cls, resource_physical_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
role_ids = properties["Roles"] role_ids = properties["Roles"]
return iam_backend.create_instance_profile( return iam_backend.create_instance_profile(
name=resource_name, path=properties.get("Path", "/"), role_ids=role_ids name=resource_physical_name,
path=properties.get("Path", "/"),
role_ids=role_ids,
) )
@property @property
@ -475,12 +623,12 @@ class SigningCertificate(BaseModel):
return iso_8601_datetime_without_milliseconds(self.upload_date) return iso_8601_datetime_without_milliseconds(self.upload_date)
class AccessKey(BaseModel): class AccessKey(CloudFormationModel):
def __init__(self, user_name): def __init__(self, user_name, status="Active"):
self.user_name = user_name self.user_name = user_name
self.access_key_id = "AKIA" + random_access_key() self.access_key_id = "AKIA" + random_access_key()
self.secret_access_key = random_alphanumeric(40) self.secret_access_key = random_alphanumeric(40)
self.status = "Active" self.status = status
self.create_date = datetime.utcnow() self.create_date = datetime.utcnow()
self.last_used = None self.last_used = None
@ -499,6 +647,66 @@ class AccessKey(BaseModel):
return self.secret_access_key return self.secret_access_key
raise UnformattedGetAttTemplateException() raise UnformattedGetAttTemplateException()
@staticmethod
def cloudformation_name_type():
return None # Resource never gets named after by template PolicyName!
@staticmethod
def cloudformation_type():
return "AWS::IAM::AccessKey"
@classmethod
def create_from_cloudformation_json(
cls, resource_physical_name, cloudformation_json, region_name
):
properties = cloudformation_json.get("Properties", {})
user_name = properties.get("UserName")
status = properties.get("Status", "Active")
return iam_backend.create_access_key(user_name, status=status,)
@classmethod
def update_from_cloudformation_json(
cls, original_resource, new_resource_name, cloudformation_json, region_name,
):
properties = cloudformation_json["Properties"]
if cls.is_replacement_update(properties):
new_resource = cls.create_from_cloudformation_json(
new_resource_name, cloudformation_json, region_name
)
cls.delete_from_cloudformation_json(
original_resource.physical_resource_id, cloudformation_json, region_name
)
return new_resource
else: # No Interruption
properties = cloudformation_json.get("Properties", {})
status = properties.get("Status")
return iam_backend.update_access_key(
original_resource.user_name, original_resource.access_key_id, status
)
@classmethod
def delete_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
):
iam_backend.delete_access_key_by_name(resource_name)
@staticmethod
def is_replacement_update(properties):
properties_requiring_replacement_update = ["Serial", "UserName"]
return any(
[
property_requiring_replacement in properties
for property_requiring_replacement in properties_requiring_replacement_update
]
)
@property
def physical_resource_id(self):
return self.access_key_id
class SshPublicKey(BaseModel): class SshPublicKey(BaseModel):
def __init__(self, user_name, ssh_public_key_body): def __init__(self, user_name, ssh_public_key_body):
@ -564,8 +772,14 @@ class Group(BaseModel):
def list_policies(self): def list_policies(self):
return self.policies.keys() return self.policies.keys()
def delete_policy(self, policy_name):
if policy_name not in self.policies:
raise IAMNotFoundException("Policy {0} not found".format(policy_name))
class User(BaseModel): del self.policies[policy_name]
class User(CloudFormationModel):
def __init__(self, name, path=None, tags=None): def __init__(self, name, path=None, tags=None):
self.name = name self.name = name
self.id = random_resource_id() self.id = random_resource_id()
@ -614,8 +828,8 @@ class User(BaseModel):
del self.policies[policy_name] del self.policies[policy_name]
def create_access_key(self): def create_access_key(self, status="Active"):
access_key = AccessKey(self.name) access_key = AccessKey(self.name, status)
self.access_keys.append(access_key) self.access_keys.append(access_key)
return access_key return access_key
@ -633,9 +847,11 @@ class User(BaseModel):
key = self.get_access_key_by_id(access_key_id) key = self.get_access_key_by_id(access_key_id)
self.access_keys.remove(key) self.access_keys.remove(key)
def update_access_key(self, access_key_id, status): def update_access_key(self, access_key_id, status=None):
key = self.get_access_key_by_id(access_key_id) key = self.get_access_key_by_id(access_key_id)
key.status = status if status is not None:
key.status = status
return key
def get_access_key_by_id(self, access_key_id): def get_access_key_by_id(self, access_key_id):
for key in self.access_keys: for key in self.access_keys:
@ -646,6 +862,15 @@ class User(BaseModel):
"The Access Key with id {0} cannot be found".format(access_key_id) "The Access Key with id {0} cannot be found".format(access_key_id)
) )
def has_access_key(self, access_key_id):
return any(
[
access_key
for access_key in self.access_keys
if access_key.access_key_id == access_key_id
]
)
def upload_ssh_public_key(self, ssh_public_key_body): def upload_ssh_public_key(self, ssh_public_key_body):
pubkey = SshPublicKey(self.name, ssh_public_key_body) pubkey = SshPublicKey(self.name, ssh_public_key_body)
self.ssh_public_keys.append(pubkey) self.ssh_public_keys.append(pubkey)
@ -677,7 +902,7 @@ class User(BaseModel):
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
if attribute_name == "Arn": if attribute_name == "Arn":
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') return self.arn
raise UnformattedGetAttTemplateException() raise UnformattedGetAttTemplateException()
def to_csv(self): def to_csv(self):
@ -752,6 +977,66 @@ class User(BaseModel):
access_key_2_last_used, access_key_2_last_used,
) )
@staticmethod
def cloudformation_name_type():
return "UserName"
@staticmethod
def cloudformation_type():
return "AWS::IAM::User"
@classmethod
def create_from_cloudformation_json(
cls, resource_physical_name, cloudformation_json, region_name
):
properties = cloudformation_json.get("Properties", {})
path = properties.get("Path")
return iam_backend.create_user(resource_physical_name, path)
@classmethod
def update_from_cloudformation_json(
cls, original_resource, new_resource_name, cloudformation_json, region_name,
):
properties = cloudformation_json["Properties"]
if cls.is_replacement_update(properties):
resource_name_property = cls.cloudformation_name_type()
if resource_name_property not in properties:
properties[resource_name_property] = new_resource_name
new_resource = cls.create_from_cloudformation_json(
properties[resource_name_property], cloudformation_json, region_name
)
properties[resource_name_property] = original_resource.name
cls.delete_from_cloudformation_json(
original_resource.name, cloudformation_json, region_name
)
return new_resource
else: # No Interruption
if "Path" in properties:
original_resource.path = properties["Path"]
return original_resource
@classmethod
def delete_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
):
iam_backend.delete_user(resource_name)
@staticmethod
def is_replacement_update(properties):
properties_requiring_replacement_update = ["UserName"]
return any(
[
property_requiring_replacement in properties
for property_requiring_replacement in properties_requiring_replacement_update
]
)
@property
def physical_resource_id(self):
return self.name
class AccountPasswordPolicy(BaseModel): class AccountPasswordPolicy(BaseModel):
def __init__( def __init__(
@ -984,6 +1269,8 @@ class IAMBackend(BaseBackend):
self.virtual_mfa_devices = {} self.virtual_mfa_devices = {}
self.account_password_policy = None self.account_password_policy = None
self.account_summary = AccountSummary(self) self.account_summary = AccountSummary(self)
self.inline_policies = {}
self.access_keys = {}
super(IAMBackend, self).__init__() super(IAMBackend, self).__init__()
def _init_managed_policies(self): def _init_managed_policies(self):
@ -1478,6 +1765,10 @@ class IAMBackend(BaseBackend):
group = self.get_group(group_name) group = self.get_group(group_name)
return group.list_policies() return group.list_policies()
def delete_group_policy(self, group_name, policy_name):
group = self.get_group(group_name)
group.delete_policy(policy_name)
def get_group_policy(self, group_name, policy_name): def get_group_policy(self, group_name, policy_name):
group = self.get_group(group_name) group = self.get_group(group_name)
return group.get_policy(policy_name) return group.get_policy(policy_name)
@ -1674,14 +1965,15 @@ class IAMBackend(BaseBackend):
def delete_policy(self, policy_arn): def delete_policy(self, policy_arn):
del self.managed_policies[policy_arn] del self.managed_policies[policy_arn]
def create_access_key(self, user_name=None): def create_access_key(self, user_name=None, status="Active"):
user = self.get_user(user_name) user = self.get_user(user_name)
key = user.create_access_key() key = user.create_access_key(status)
self.access_keys[key.physical_resource_id] = key
return key return key
def update_access_key(self, user_name, access_key_id, status): def update_access_key(self, user_name, access_key_id, status=None):
user = self.get_user(user_name) user = self.get_user(user_name)
user.update_access_key(access_key_id, status) return user.update_access_key(access_key_id, status)
def get_access_key_last_used(self, access_key_id): def get_access_key_last_used(self, access_key_id):
access_keys_list = self.get_all_access_keys_for_all_users() access_keys_list = self.get_all_access_keys_for_all_users()
@ -1706,7 +1998,17 @@ class IAMBackend(BaseBackend):
def delete_access_key(self, access_key_id, user_name): def delete_access_key(self, access_key_id, user_name):
user = self.get_user(user_name) user = self.get_user(user_name)
user.delete_access_key(access_key_id) access_key = user.get_access_key_by_id(access_key_id)
self.delete_access_key_by_name(access_key.access_key_id)
def delete_access_key_by_name(self, name):
key = self.access_keys[name]
try: # User may have been deleted before their access key...
user = self.get_user(key.user_name)
user.delete_access_key(key.access_key_id)
except IAMNotFoundException:
pass
del self.access_keys[name]
def upload_ssh_public_key(self, user_name, ssh_public_key_body): def upload_ssh_public_key(self, user_name, ssh_public_key_body):
user = self.get_user(user_name) user = self.get_user(user_name)
@ -2017,5 +2319,62 @@ class IAMBackend(BaseBackend):
def get_account_summary(self): def get_account_summary(self):
return self.account_summary return self.account_summary
def create_inline_policy(
self,
resource_name,
policy_name,
policy_document,
group_names,
role_names,
user_names,
):
if resource_name in self.inline_policies:
raise IAMConflictException(
"EntityAlreadyExists",
"Inline Policy {0} already exists".format(resource_name),
)
inline_policy = InlinePolicy(
resource_name,
policy_name,
policy_document,
group_names,
role_names,
user_names,
)
self.inline_policies[resource_name] = inline_policy
inline_policy.apply_policy(self)
return inline_policy
def get_inline_policy(self, policy_id):
inline_policy = None
try:
inline_policy = self.inline_policies[policy_id]
except KeyError:
raise IAMNotFoundException("Inline policy {0} not found".format(policy_id))
return inline_policy
def update_inline_policy(
self,
resource_name,
policy_name,
policy_document,
group_names,
role_names,
user_names,
):
inline_policy = self.get_inline_policy(resource_name)
inline_policy.unapply_policy(self)
inline_policy.update(
policy_name, policy_document, group_names, role_names, user_names,
)
inline_policy.apply_policy(self)
return inline_policy
def delete_inline_policy(self, policy_id):
inline_policy = self.get_inline_policy(policy_id)
inline_policy.unapply_policy(self)
del self.inline_policies[policy_id]
iam_backend = IAMBackend() iam_backend = IAMBackend()

View File

@ -135,7 +135,7 @@ class Shard(BaseModel):
class Stream(CloudFormationModel): class Stream(CloudFormationModel):
def __init__(self, stream_name, shard_count, region_name): def __init__(self, stream_name, shard_count, retention_period_hours, region_name):
self.stream_name = stream_name self.stream_name = stream_name
self.creation_datetime = datetime.datetime.now() self.creation_datetime = datetime.datetime.now()
self.region = region_name self.region = region_name
@ -145,6 +145,7 @@ class Stream(CloudFormationModel):
self.status = "ACTIVE" self.status = "ACTIVE"
self.shard_count = None self.shard_count = None
self.update_shard_count(shard_count) self.update_shard_count(shard_count)
self.retention_period_hours = retention_period_hours
def update_shard_count(self, shard_count): def update_shard_count(self, shard_count):
# ToDo: This was extracted from init. It's only accurate for new streams. # ToDo: This was extracted from init. It's only accurate for new streams.
@ -213,6 +214,7 @@ class Stream(CloudFormationModel):
"StreamName": self.stream_name, "StreamName": self.stream_name,
"StreamStatus": self.status, "StreamStatus": self.status,
"HasMoreShards": False, "HasMoreShards": False,
"RetentionPeriodHours": self.retention_period_hours,
"Shards": [shard.to_json() for shard in self.shards.values()], "Shards": [shard.to_json() for shard in self.shards.values()],
} }
} }
@ -243,9 +245,19 @@ class Stream(CloudFormationModel):
): ):
properties = cloudformation_json.get("Properties", {}) properties = cloudformation_json.get("Properties", {})
shard_count = properties.get("ShardCount", 1) shard_count = properties.get("ShardCount", 1)
name = properties.get("Name", resource_name) retention_period_hours = properties.get("RetentionPeriodHours", resource_name)
tags = {
tag_item["Key"]: tag_item["Value"]
for tag_item in properties.get("Tags", [])
}
backend = kinesis_backends[region_name] backend = kinesis_backends[region_name]
return backend.create_stream(name, shard_count, region_name) stream = backend.create_stream(
resource_name, shard_count, retention_period_hours, region_name
)
if any(tags):
backend.add_tags_to_stream(stream.stream_name, tags)
return stream
@classmethod @classmethod
def update_from_cloudformation_json( def update_from_cloudformation_json(
@ -269,6 +281,15 @@ class Stream(CloudFormationModel):
else: # No Interruption else: # No Interruption
if "ShardCount" in properties: if "ShardCount" in properties:
original_resource.update_shard_count(properties["ShardCount"]) original_resource.update_shard_count(properties["ShardCount"])
if "RetentionPeriodHours" in properties:
original_resource.retention_period_hours = properties[
"RetentionPeriodHours"
]
if "Tags" in properties:
original_resource.tags = {
tag_item["Key"]: tag_item["Value"]
for tag_item in properties.get("Tags", [])
}
return original_resource return original_resource
@classmethod @classmethod
@ -276,9 +297,7 @@ class Stream(CloudFormationModel):
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
backend = kinesis_backends[region_name] backend = kinesis_backends[region_name]
properties = cloudformation_json.get("Properties", {}) backend.delete_stream(resource_name)
stream_name = properties.get(cls.cloudformation_name_type(), resource_name)
backend.delete_stream(stream_name)
@staticmethod @staticmethod
def is_replacement_update(properties): def is_replacement_update(properties):
@ -398,10 +417,12 @@ class KinesisBackend(BaseBackend):
self.streams = OrderedDict() self.streams = OrderedDict()
self.delivery_streams = {} self.delivery_streams = {}
def create_stream(self, stream_name, shard_count, region_name): def create_stream(
self, stream_name, shard_count, retention_period_hours, region_name
):
if stream_name in self.streams: if stream_name in self.streams:
raise ResourceInUseError(stream_name) raise ResourceInUseError(stream_name)
stream = Stream(stream_name, shard_count, region_name) stream = Stream(stream_name, shard_count, retention_period_hours, region_name)
self.streams[stream_name] = stream self.streams[stream_name] = stream
return stream return stream

View File

@ -25,7 +25,10 @@ class KinesisResponse(BaseResponse):
def create_stream(self): def create_stream(self):
stream_name = self.parameters.get("StreamName") stream_name = self.parameters.get("StreamName")
shard_count = self.parameters.get("ShardCount") shard_count = self.parameters.get("ShardCount")
self.kinesis_backend.create_stream(stream_name, shard_count, self.region) retention_period_hours = self.parameters.get("RetentionPeriodHours")
self.kinesis_backend.create_stream(
stream_name, shard_count, retention_period_hours, self.region
)
return "" return ""
def describe_stream(self): def describe_stream(self):

View File

@ -4,7 +4,6 @@ import boto.rds
from jinja2 import Template from jinja2 import Template
from moto.core import BaseBackend, CloudFormationModel from moto.core import BaseBackend, CloudFormationModel
from moto.core.utils import get_random_hex
from moto.ec2.models import ec2_backends from moto.ec2.models import ec2_backends
from moto.rds.exceptions import UnformattedGetAttTemplateException from moto.rds.exceptions import UnformattedGetAttTemplateException
from moto.rds2.models import rds2_backends from moto.rds2.models import rds2_backends
@ -33,9 +32,6 @@ class Database(CloudFormationModel):
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
db_instance_identifier = properties.get(cls.cloudformation_name_type())
if not db_instance_identifier:
db_instance_identifier = resource_name.lower() + get_random_hex(12)
db_security_groups = properties.get("DBSecurityGroups") db_security_groups = properties.get("DBSecurityGroups")
if not db_security_groups: if not db_security_groups:
db_security_groups = [] db_security_groups = []
@ -48,7 +44,7 @@ class Database(CloudFormationModel):
"availability_zone": properties.get("AvailabilityZone"), "availability_zone": properties.get("AvailabilityZone"),
"backup_retention_period": properties.get("BackupRetentionPeriod"), "backup_retention_period": properties.get("BackupRetentionPeriod"),
"db_instance_class": properties.get("DBInstanceClass"), "db_instance_class": properties.get("DBInstanceClass"),
"db_instance_identifier": db_instance_identifier, "db_instance_identifier": resource_name,
"db_name": properties.get("DBName"), "db_name": properties.get("DBName"),
"db_subnet_group_name": db_subnet_group_name, "db_subnet_group_name": db_subnet_group_name,
"engine": properties.get("Engine"), "engine": properties.get("Engine"),
@ -229,7 +225,7 @@ class SecurityGroup(CloudFormationModel):
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
group_name = resource_name.lower() + get_random_hex(12) group_name = resource_name.lower()
description = properties["GroupDescription"] description = properties["GroupDescription"]
security_group_ingress_rules = properties.get("DBSecurityGroupIngress", []) security_group_ingress_rules = properties.get("DBSecurityGroupIngress", [])
tags = properties.get("Tags") tags = properties.get("Tags")
@ -303,9 +299,7 @@ class SubnetGroup(CloudFormationModel):
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
subnet_name = properties.get(cls.cloudformation_name_type()) subnet_name = resource_name.lower()
if not subnet_name:
subnet_name = resource_name.lower() + get_random_hex(12)
description = properties["DBSubnetGroupDescription"] description = properties["DBSubnetGroupDescription"]
subnet_ids = properties["SubnetIds"] subnet_ids = properties["SubnetIds"]
tags = properties.get("Tags") tags = properties.get("Tags")

View File

@ -10,7 +10,6 @@ from jinja2 import Template
from re import compile as re_compile from re import compile as re_compile
from moto.compat import OrderedDict from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import get_random_hex
from moto.core.utils import iso_8601_datetime_with_milliseconds from moto.core.utils import iso_8601_datetime_with_milliseconds
from moto.ec2.models import ec2_backends from moto.ec2.models import ec2_backends
from .exceptions import ( from .exceptions import (
@ -371,9 +370,6 @@ class Database(CloudFormationModel):
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
db_instance_identifier = properties.get(cls.cloudformation_name_type())
if not db_instance_identifier:
db_instance_identifier = resource_name.lower() + get_random_hex(12)
db_security_groups = properties.get("DBSecurityGroups") db_security_groups = properties.get("DBSecurityGroups")
if not db_security_groups: if not db_security_groups:
db_security_groups = [] db_security_groups = []
@ -386,7 +382,7 @@ class Database(CloudFormationModel):
"availability_zone": properties.get("AvailabilityZone"), "availability_zone": properties.get("AvailabilityZone"),
"backup_retention_period": properties.get("BackupRetentionPeriod"), "backup_retention_period": properties.get("BackupRetentionPeriod"),
"db_instance_class": properties.get("DBInstanceClass"), "db_instance_class": properties.get("DBInstanceClass"),
"db_instance_identifier": db_instance_identifier, "db_instance_identifier": resource_name,
"db_name": properties.get("DBName"), "db_name": properties.get("DBName"),
"db_subnet_group_name": db_subnet_group_name, "db_subnet_group_name": db_subnet_group_name,
"engine": properties.get("Engine"), "engine": properties.get("Engine"),
@ -650,7 +646,7 @@ class SecurityGroup(CloudFormationModel):
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
group_name = resource_name.lower() + get_random_hex(12) group_name = resource_name.lower()
description = properties["GroupDescription"] description = properties["GroupDescription"]
security_group_ingress_rules = properties.get("DBSecurityGroupIngress", []) security_group_ingress_rules = properties.get("DBSecurityGroupIngress", [])
tags = properties.get("Tags") tags = properties.get("Tags")
@ -759,9 +755,6 @@ class SubnetGroup(CloudFormationModel):
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
subnet_name = properties.get(cls.cloudformation_name_type())
if not subnet_name:
subnet_name = resource_name.lower() + get_random_hex(12)
description = properties["DBSubnetGroupDescription"] description = properties["DBSubnetGroupDescription"]
subnet_ids = properties["SubnetIds"] subnet_ids = properties["SubnetIds"]
tags = properties.get("Tags") tags = properties.get("Tags")
@ -770,7 +763,7 @@ class SubnetGroup(CloudFormationModel):
subnets = [ec2_backend.get_subnet(subnet_id) for subnet_id in subnet_ids] subnets = [ec2_backend.get_subnet(subnet_id) for subnet_id in subnet_ids]
rds2_backend = rds2_backends[region_name] rds2_backend = rds2_backends[region_name]
subnet_group = rds2_backend.create_subnet_group( subnet_group = rds2_backend.create_subnet_group(
subnet_name, description, subnets, tags resource_name, description, subnets, tags
) )
return subnet_group return subnet_group

View File

@ -298,10 +298,9 @@ class FakeZone(CloudFormationModel):
def create_from_cloudformation_json( def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] hosted_zone = route53_backend.create_hosted_zone(
name = properties["Name"] resource_name, private_zone=False
)
hosted_zone = route53_backend.create_hosted_zone(name, private_zone=False)
return hosted_zone return hosted_zone

View File

@ -1086,7 +1086,7 @@ class FakeBucket(CloudFormationModel):
): ):
bucket = s3_backend.create_bucket(resource_name, region_name) bucket = s3_backend.create_bucket(resource_name, region_name)
properties = cloudformation_json["Properties"] properties = cloudformation_json.get("Properties", {})
if "BucketEncryption" in properties: if "BucketEncryption" in properties:
bucket_encryption = cfn_to_api_encryption(properties["BucketEncryption"]) bucket_encryption = cfn_to_api_encryption(properties["BucketEncryption"])
@ -1129,9 +1129,7 @@ class FakeBucket(CloudFormationModel):
def delete_from_cloudformation_json( def delete_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] s3_backend.delete_bucket(resource_name)
bucket_name = properties[cls.cloudformation_name_type()]
s3_backend.delete_bucket(bucket_name)
def to_config_dict(self): def to_config_dict(self):
"""Return the AWS Config JSON format of this S3 bucket. """Return the AWS Config JSON format of this S3 bucket.

View File

@ -104,7 +104,7 @@ class Topic(CloudFormationModel):
sns_backend = sns_backends[region_name] sns_backend = sns_backends[region_name]
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
topic = sns_backend.create_topic(properties.get(cls.cloudformation_name_type())) topic = sns_backend.create_topic(resource_name)
for subscription in properties.get("Subscription", []): for subscription in properties.get("Subscription", []):
sns_backend.subscribe( sns_backend.subscribe(
topic.arn, subscription["Endpoint"], subscription["Protocol"] topic.arn, subscription["Endpoint"], subscription["Protocol"]

View File

@ -374,10 +374,7 @@ class Queue(CloudFormationModel):
sqs_backend = sqs_backends[region_name] sqs_backend = sqs_backends[region_name]
return sqs_backend.create_queue( return sqs_backend.create_queue(
name=properties["QueueName"], name=resource_name, tags=tags_dict, region=region_name, **properties
tags=tags_dict,
region=region_name,
**properties
) )
@classmethod @classmethod
@ -385,7 +382,7 @@ class Queue(CloudFormationModel):
cls, original_resource, new_resource_name, cloudformation_json, region_name cls, original_resource, new_resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
queue_name = properties["QueueName"] queue_name = original_resource.name
sqs_backend = sqs_backends[region_name] sqs_backend = sqs_backends[region_name]
queue = sqs_backend.get_queue(queue_name) queue = sqs_backend.get_queue(queue_name)
@ -402,10 +399,8 @@ class Queue(CloudFormationModel):
def delete_from_cloudformation_json( def delete_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"]
queue_name = properties["QueueName"]
sqs_backend = sqs_backends[region_name] sqs_backend = sqs_backends[region_name]
sqs_backend.delete_queue(queue_name) sqs_backend.delete_queue(resource_name)
@property @property
def approximate_number_of_messages_delayed(self): def approximate_number_of_messages_delayed(self):

View File

@ -592,7 +592,7 @@ def test_boto3_create_stack_set_with_yaml():
@mock_cloudformation @mock_cloudformation
@mock_s3 @mock_s3
def test_create_stack_set_from_s3_url(): def test_create_stack_set_from_s3_url():
s3 = boto3.client("s3") s3 = boto3.client("s3", region_name="us-east-1")
s3_conn = boto3.resource("s3", region_name="us-east-1") s3_conn = boto3.resource("s3", region_name="us-east-1")
s3_conn.create_bucket(Bucket="foobar") s3_conn.create_bucket(Bucket="foobar")
@ -704,7 +704,7 @@ def test_boto3_create_stack_with_short_form_func_yaml():
@mock_s3 @mock_s3
@mock_cloudformation @mock_cloudformation
def test_get_template_summary(): def test_get_template_summary():
s3 = boto3.client("s3") s3 = boto3.client("s3", region_name="us-east-1")
s3_conn = boto3.resource("s3", region_name="us-east-1") s3_conn = boto3.resource("s3", region_name="us-east-1")
conn = boto3.client("cloudformation", region_name="us-east-1") conn = boto3.client("cloudformation", region_name="us-east-1")
@ -802,7 +802,7 @@ def test_create_stack_with_role_arn():
@mock_cloudformation @mock_cloudformation
@mock_s3 @mock_s3
def test_create_stack_from_s3_url(): def test_create_stack_from_s3_url():
s3 = boto3.client("s3") s3 = boto3.client("s3", region_name="us-east-1")
s3_conn = boto3.resource("s3", region_name="us-east-1") s3_conn = boto3.resource("s3", region_name="us-east-1")
s3_conn.create_bucket(Bucket="foobar") s3_conn.create_bucket(Bucket="foobar")
@ -857,7 +857,7 @@ def test_update_stack_with_previous_value():
@mock_s3 @mock_s3
@mock_ec2 @mock_ec2
def test_update_stack_from_s3_url(): def test_update_stack_from_s3_url():
s3 = boto3.client("s3") s3 = boto3.client("s3", region_name="us-east-1")
s3_conn = boto3.resource("s3", region_name="us-east-1") s3_conn = boto3.resource("s3", region_name="us-east-1")
cf_conn = boto3.client("cloudformation", region_name="us-east-1") cf_conn = boto3.client("cloudformation", region_name="us-east-1")
@ -886,7 +886,7 @@ def test_update_stack_from_s3_url():
@mock_cloudformation @mock_cloudformation
@mock_s3 @mock_s3
def test_create_change_set_from_s3_url(): def test_create_change_set_from_s3_url():
s3 = boto3.client("s3") s3 = boto3.client("s3", region_name="us-east-1")
s3_conn = boto3.resource("s3", region_name="us-east-1") s3_conn = boto3.resource("s3", region_name="us-east-1")
s3_conn.create_bucket(Bucket="foobar") s3_conn.create_bucket(Bucket="foobar")

View File

@ -118,7 +118,7 @@ def test_boto3_yaml_validate_successful():
@mock_cloudformation @mock_cloudformation
@mock_s3 @mock_s3
def test_boto3_yaml_validate_template_url_successful(): def test_boto3_yaml_validate_template_url_successful():
s3 = boto3.client("s3") s3 = boto3.client("s3", region_name="us-east-1")
s3_conn = boto3.resource("s3", region_name="us-east-1") s3_conn = boto3.resource("s3", region_name="us-east-1")
s3_conn.create_bucket(Bucket="foobar") s3_conn.create_bucket(Bucket="foobar")

View File

@ -5,12 +5,9 @@ import json
import boto import boto
import boto3 import boto3
import csv import csv
import os
import sure # noqa import sure # noqa
import sys
from boto.exception import BotoServerError from boto.exception import BotoServerError
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from dateutil.tz import tzutc
from moto import mock_iam, mock_iam_deprecated, settings from moto import mock_iam, mock_iam_deprecated, settings
from moto.core import ACCOUNT_ID from moto.core import ACCOUNT_ID

File diff suppressed because it is too large Load Diff

View File

@ -73,6 +73,12 @@ Resources:
Properties: Properties:
Name: MyStream Name: MyStream
ShardCount: 4 ShardCount: 4
RetentionPeriodHours: 48
Tags:
- Key: TagKey1
Value: TagValue1
- Key: TagKey2
Value: TagValue2
""".strip() """.strip()
cf_conn.create_stack(StackName=stack_name, TemplateBody=template) cf_conn.create_stack(StackName=stack_name, TemplateBody=template)
@ -83,6 +89,14 @@ Resources:
stream_description = kinesis_conn.describe_stream(StreamName="MyStream")[ stream_description = kinesis_conn.describe_stream(StreamName="MyStream")[
"StreamDescription" "StreamDescription"
] ]
stream_description["RetentionPeriodHours"].should.equal(48)
tags = kinesis_conn.list_tags_for_stream(StreamName="MyStream")["Tags"]
tag1_value = [tag for tag in tags if tag["Key"] == "TagKey1"][0]["Value"]
tag2_value = [tag for tag in tags if tag["Key"] == "TagKey2"][0]["Value"]
tag1_value.should.equal("TagValue1")
tag2_value.should.equal("TagValue2")
shards_provisioned = len( shards_provisioned = len(
[ [
shard shard
@ -98,12 +112,27 @@ Resources:
Type: AWS::Kinesis::Stream Type: AWS::Kinesis::Stream
Properties: Properties:
ShardCount: 6 ShardCount: 6
RetentionPeriodHours: 24
Tags:
- Key: TagKey1
Value: TagValue1a
- Key: TagKey2
Value: TagValue2a
""".strip() """.strip()
cf_conn.update_stack(StackName=stack_name, TemplateBody=template) cf_conn.update_stack(StackName=stack_name, TemplateBody=template)
stream_description = kinesis_conn.describe_stream(StreamName="MyStream")[ stream_description = kinesis_conn.describe_stream(StreamName="MyStream")[
"StreamDescription" "StreamDescription"
] ]
stream_description["RetentionPeriodHours"].should.equal(24)
tags = kinesis_conn.list_tags_for_stream(StreamName="MyStream")["Tags"]
tag1_value = [tag for tag in tags if tag["Key"] == "TagKey1"][0]["Value"]
tag2_value = [tag for tag in tags if tag["Key"] == "TagKey2"][0]["Value"]
tag1_value.should.equal("TagValue1a")
tag2_value.should.equal("TagValue2a")
shards_provisioned = len( shards_provisioned = len(
[ [
shard shard

View File

@ -2,7 +2,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
import os
import sys import sys
from boto3 import Session from boto3 import Session
@ -11,7 +10,6 @@ from six.moves.urllib.error import HTTPError
from functools import wraps from functools import wraps
from gzip import GzipFile from gzip import GzipFile
from io import BytesIO from io import BytesIO
import mimetypes
import zlib import zlib
import pickle import pickle
import uuid import uuid
@ -36,7 +34,7 @@ from nose.tools import assert_raises
import sure # noqa import sure # noqa
from moto import settings, mock_s3, mock_s3_deprecated, mock_config, mock_cloudformation from moto import settings, mock_s3, mock_s3_deprecated, mock_config
import moto.s3.models as s3model import moto.s3.models as s3model
from moto.core.exceptions import InvalidNextTokenException from moto.core.exceptions import InvalidNextTokenException
from moto.core.utils import py2_strip_unicode_keys from moto.core.utils import py2_strip_unicode_keys
@ -4686,142 +4684,3 @@ def test_presigned_put_url_with_custom_headers():
s3.delete_object(Bucket=bucket, Key=key) s3.delete_object(Bucket=bucket, Key=key)
s3.delete_bucket(Bucket=bucket) s3.delete_bucket(Bucket=bucket)
@mock_s3
@mock_cloudformation
def test_s3_bucket_cloudformation_basic():
s3 = boto3.client("s3", region_name="us-east-1")
cf = boto3.client("cloudformation", region_name="us-east-1")
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {"testInstance": {"Type": "AWS::S3::Bucket", "Properties": {},}},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
stack_id = cf.create_stack(StackName="test_stack", TemplateBody=template_json)[
"StackId"
]
stack_description = cf.describe_stacks(StackName="test_stack")["Stacks"][0]
s3.head_bucket(Bucket=stack_description["Outputs"][0]["OutputValue"])
@mock_s3
@mock_cloudformation
def test_s3_bucket_cloudformation_with_properties():
s3 = boto3.client("s3", region_name="us-east-1")
cf = boto3.client("cloudformation", region_name="us-east-1")
bucket_name = "MyBucket"
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"testInstance": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": bucket_name,
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
},
},
}
},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
stack_id = cf.create_stack(StackName="test_stack", TemplateBody=template_json)[
"StackId"
]
stack_description = cf.describe_stacks(StackName="test_stack")["Stacks"][0]
s3.head_bucket(Bucket=bucket_name)
encryption = s3.get_bucket_encryption(Bucket=bucket_name)
encryption["ServerSideEncryptionConfiguration"]["Rules"][0][
"ApplyServerSideEncryptionByDefault"
]["SSEAlgorithm"].should.equal("AES256")
@mock_s3
@mock_cloudformation
def test_s3_bucket_cloudformation_update_no_interruption():
s3 = boto3.client("s3", region_name="us-east-1")
cf = boto3.client("cloudformation", region_name="us-east-1")
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {"testInstance": {"Type": "AWS::S3::Bucket"}},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
cf.create_stack(StackName="test_stack", TemplateBody=template_json)
stack_description = cf.describe_stacks(StackName="test_stack")["Stacks"][0]
s3.head_bucket(Bucket=stack_description["Outputs"][0]["OutputValue"])
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"testInstance": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
}
},
}
},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
cf.update_stack(StackName="test_stack", TemplateBody=template_json)
encryption = s3.get_bucket_encryption(
Bucket=stack_description["Outputs"][0]["OutputValue"]
)
encryption["ServerSideEncryptionConfiguration"]["Rules"][0][
"ApplyServerSideEncryptionByDefault"
]["SSEAlgorithm"].should.equal("AES256")
@mock_s3
@mock_cloudformation
def test_s3_bucket_cloudformation_update_replacement():
s3 = boto3.client("s3", region_name="us-east-1")
cf = boto3.client("cloudformation", region_name="us-east-1")
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {"testInstance": {"Type": "AWS::S3::Bucket"}},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
cf.create_stack(StackName="test_stack", TemplateBody=template_json)
stack_description = cf.describe_stacks(StackName="test_stack")["Stacks"][0]
s3.head_bucket(Bucket=stack_description["Outputs"][0]["OutputValue"])
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"testInstance": {
"Type": "AWS::S3::Bucket",
"Properties": {"BucketName": "MyNewBucketName"},
}
},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
cf.update_stack(StackName="test_stack", TemplateBody=template_json)
stack_description = cf.describe_stacks(StackName="test_stack")["Stacks"][0]
s3.head_bucket(Bucket=stack_description["Outputs"][0]["OutputValue"])

View File

@ -0,0 +1,145 @@
import json
import boto3
import sure # noqa
from moto import mock_s3, mock_cloudformation
@mock_s3
@mock_cloudformation
def test_s3_bucket_cloudformation_basic():
s3 = boto3.client("s3", region_name="us-east-1")
cf = boto3.client("cloudformation", region_name="us-east-1")
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {"testInstance": {"Type": "AWS::S3::Bucket", "Properties": {},}},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
stack_id = cf.create_stack(StackName="test_stack", TemplateBody=template_json)[
"StackId"
]
stack_description = cf.describe_stacks(StackName="test_stack")["Stacks"][0]
s3.head_bucket(Bucket=stack_description["Outputs"][0]["OutputValue"])
@mock_s3
@mock_cloudformation
def test_s3_bucket_cloudformation_with_properties():
s3 = boto3.client("s3", region_name="us-east-1")
cf = boto3.client("cloudformation", region_name="us-east-1")
bucket_name = "MyBucket"
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"testInstance": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": bucket_name,
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
},
},
}
},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
stack_id = cf.create_stack(StackName="test_stack", TemplateBody=template_json)[
"StackId"
]
stack_description = cf.describe_stacks(StackName="test_stack")["Stacks"][0]
s3.head_bucket(Bucket=bucket_name)
encryption = s3.get_bucket_encryption(Bucket=bucket_name)
encryption["ServerSideEncryptionConfiguration"]["Rules"][0][
"ApplyServerSideEncryptionByDefault"
]["SSEAlgorithm"].should.equal("AES256")
@mock_s3
@mock_cloudformation
def test_s3_bucket_cloudformation_update_no_interruption():
s3 = boto3.client("s3", region_name="us-east-1")
cf = boto3.client("cloudformation", region_name="us-east-1")
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {"testInstance": {"Type": "AWS::S3::Bucket"}},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
cf.create_stack(StackName="test_stack", TemplateBody=template_json)
stack_description = cf.describe_stacks(StackName="test_stack")["Stacks"][0]
s3.head_bucket(Bucket=stack_description["Outputs"][0]["OutputValue"])
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"testInstance": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
}
},
}
},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
cf.update_stack(StackName="test_stack", TemplateBody=template_json)
encryption = s3.get_bucket_encryption(
Bucket=stack_description["Outputs"][0]["OutputValue"]
)
encryption["ServerSideEncryptionConfiguration"]["Rules"][0][
"ApplyServerSideEncryptionByDefault"
]["SSEAlgorithm"].should.equal("AES256")
@mock_s3
@mock_cloudformation
def test_s3_bucket_cloudformation_update_replacement():
s3 = boto3.client("s3", region_name="us-east-1")
cf = boto3.client("cloudformation", region_name="us-east-1")
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {"testInstance": {"Type": "AWS::S3::Bucket"}},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
cf.create_stack(StackName="test_stack", TemplateBody=template_json)
stack_description = cf.describe_stacks(StackName="test_stack")["Stacks"][0]
s3.head_bucket(Bucket=stack_description["Outputs"][0]["OutputValue"])
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"testInstance": {
"Type": "AWS::S3::Bucket",
"Properties": {"BucketName": "MyNewBucketName"},
}
},
"Outputs": {"Bucket": {"Value": {"Ref": "testInstance"}}},
}
template_json = json.dumps(template)
cf.update_stack(StackName="test_stack", TemplateBody=template_json)
stack_description = cf.describe_stacks(StackName="test_stack")["Stacks"][0]
s3.head_bucket(Bucket=stack_description["Outputs"][0]["OutputValue"])