diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 96eb8d374..4244b4792 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -565,7 +565,7 @@ ## cloudformation
-34% implemented +50% implemented - [ ] activate_type - [ ] batch_describe_type_configurations @@ -586,13 +586,13 @@ - [ ] describe_change_set_hooks - [ ] describe_publisher - [ ] describe_stack_drift_detection_status -- [ ] describe_stack_events +- [X] describe_stack_events - [X] describe_stack_instance -- [ ] describe_stack_resource +- [X] describe_stack_resource - [ ] describe_stack_resource_drifts -- [ ] describe_stack_resources -- [ ] describe_stack_set -- [ ] describe_stack_set_operation +- [X] describe_stack_resources +- [X] describe_stack_set +- [X] describe_stack_set_operation - [X] describe_stacks - [ ] describe_type - [ ] describe_type_registration @@ -602,7 +602,7 @@ - [ ] estimate_template_cost - [X] execute_change_set - [X] get_stack_policy -- [ ] get_template +- [X] get_template - [ ] get_template_summary - [ ] import_stacks_to_stack_set - [X] list_change_sets @@ -610,9 +610,9 @@ - [ ] list_imports - [X] list_stack_instances - [X] list_stack_resources -- [ ] list_stack_set_operation_results -- [ ] list_stack_set_operations -- [ ] list_stack_sets +- [X] list_stack_set_operation_results +- [X] list_stack_set_operations +- [X] list_stack_sets - [X] list_stacks - [ ] list_type_registrations - [ ] list_type_versions @@ -626,7 +626,7 @@ - [ ] set_type_configuration - [ ] set_type_default_version - [ ] signal_resource -- [ ] stop_stack_set_operation +- [X] stop_stack_set_operation - [ ] test_type - [X] update_stack - [X] update_stack_instances diff --git a/docs/docs/services/cloudformation.rst b/docs/docs/services/cloudformation.rst index 4cf8427b2..27f8da9b9 100644 --- a/docs/docs/services/cloudformation.rst +++ b/docs/docs/services/cloudformation.rst @@ -62,13 +62,13 @@ cloudformation - [ ] describe_change_set_hooks - [ ] describe_publisher - [ ] describe_stack_drift_detection_status -- [ ] describe_stack_events +- [X] describe_stack_events - [X] describe_stack_instance -- [ ] describe_stack_resource +- [X] describe_stack_resource - [ ] describe_stack_resource_drifts -- [ ] describe_stack_resources -- [ ] describe_stack_set -- [ ] describe_stack_set_operation +- [X] describe_stack_resources +- [X] describe_stack_set +- [X] describe_stack_set_operation - [X] describe_stacks - [ ] describe_type - [ ] describe_type_registration @@ -78,7 +78,7 @@ cloudformation - [ ] estimate_template_cost - [X] execute_change_set - [X] get_stack_policy -- [ ] get_template +- [X] get_template - [ ] get_template_summary - [ ] import_stacks_to_stack_set - [X] list_change_sets @@ -91,9 +91,9 @@ cloudformation - [X] list_stack_resources -- [ ] list_stack_set_operation_results -- [ ] list_stack_set_operations -- [ ] list_stack_sets +- [X] list_stack_set_operation_results +- [X] list_stack_set_operations +- [X] list_stack_sets - [X] list_stacks - [ ] list_type_registrations - [ ] list_type_versions @@ -111,7 +111,7 @@ cloudformation - [ ] set_type_configuration - [ ] set_type_default_version - [ ] signal_resource -- [ ] stop_stack_set_operation +- [X] stop_stack_set_operation - [ ] test_type - [X] update_stack - [X] update_stack_instances diff --git a/moto/cloudformation/exceptions.py b/moto/cloudformation/exceptions.py index 3573e48e6..e067e0c4f 100644 --- a/moto/cloudformation/exceptions.py +++ b/moto/cloudformation/exceptions.py @@ -48,6 +48,16 @@ class StackSetNotEmpty(RESTError): ) +class StackSetNotFoundException(RESTError): + def __init__(self, name: str): + template = Template(ERROR_RESPONSE) + message = f"StackSet {name} not found" + super().__init__(error_type="StackSetNotFoundException", message=message) + self.description = template.render( + code="StackSetNotFoundException", message=message + ) + + class UnsupportedAttribute(ValidationError): def __init__(self, resource: str, attr: str): template = Template(ERROR_RESPONSE) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 22d19cf6f..c13d60fbd 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -26,7 +26,7 @@ from .utils import ( yaml_tag_constructor, validate_template_cfn_lint, ) -from .exceptions import ValidationError, StackSetNotEmpty +from .exceptions import ValidationError, StackSetNotEmpty, StackSetNotFoundException class FakeStackSet(BaseModel): @@ -40,9 +40,9 @@ class FakeStackSet(BaseModel): description: Optional[str], parameters: Dict[str, str], permission_model: str, - tags: Optional[Dict[str, str]] = None, - admin_role: str = "AWSCloudFormationStackSetAdministrationRole", - execution_role: str = "AWSCloudFormationStackSetExecutionRole", + tags: Optional[Dict[str, str]], + admin_role: Optional[str], + execution_role: Optional[str], ): self.id = stackset_id self.arn = generate_stackset_arn(stackset_id, region, account_id) @@ -53,7 +53,7 @@ class FakeStackSet(BaseModel): self.tags = tags self.admin_role = admin_role self.admin_role_arn = f"arn:aws:iam::{account_id}:role/{self.admin_role}" - self.execution_role = execution_role + self.execution_role = execution_role or "AWSCloudFormationStackSetExecutionRole" self.status = "ACTIVE" self.instances = FakeStackInstances( account_id, template, parameters, self.id, self.name @@ -76,8 +76,10 @@ class FakeStackSet(BaseModel): "OperationId": operation_id, "Action": action, "Status": status, - "CreationTimestamp": datetime.now(), - "EndTimestamp": datetime.now() + timedelta(minutes=2), + "CreationTimestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f"), + "EndTimestamp": (datetime.now() + timedelta(minutes=2)).strftime( + "%Y-%m-%dT%H:%M:%S.%f" + ), "Instances": [ {account: region} for account in accounts for region in regions ], @@ -368,6 +370,8 @@ class FakeStack(BaseModel): role_arn: Optional[str] = None, cross_stack_resources: Optional[Dict[str, Export]] = None, enable_termination_protection: Optional[bool] = False, + timeout_in_mins: Optional[int] = None, + stack_policy_body: Optional[str] = None, ): self.stack_id = stack_id self.name = name @@ -385,7 +389,8 @@ class FakeStack(BaseModel): self.role_arn = role_arn self.tags = tags if tags else {} self.events: List[FakeEvent] = [] - self.policy = "" + self.timeout_in_mins = timeout_in_mins + self.policy = stack_policy_body or "" self.cross_stack_resources: Dict[str, Export] = cross_stack_resources or {} self.enable_termination_protection: bool = ( @@ -716,6 +721,9 @@ class CloudFormationBackend(BaseBackend): parameters: Dict[str, str], tags: Dict[str, str], permission_model: str, + admin_role: Optional[str], + exec_role: Optional[str], + description: Optional[str], ) -> FakeStackSet: """ The following parameters are not yet implemented: StackId, AdministrationRoleARN, AutoDeployment, ExecutionRoleName, CallAs, ClientRequestToken, ManagedExecution @@ -728,21 +736,26 @@ class CloudFormationBackend(BaseBackend): region=self.region_name, template=template, parameters=parameters, - description=None, + description=description, tags=tags, permission_model=permission_model, + admin_role=admin_role, + execution_role=exec_role, ) self.stacksets[stackset_id] = new_stackset return new_stackset - def get_stack_set(self, name: str) -> FakeStackSet: + def describe_stack_set(self, name: str) -> FakeStackSet: stacksets = self.stacksets.keys() - if name in stacksets: + if name in stacksets and self.stacksets[name].status != "DELETED": return self.stacksets[name] for stackset in stacksets: - if self.stacksets[stackset].name == name: + if ( + self.stacksets[stackset].name == name + and self.stacksets[stackset].status != "DELETED" + ): return self.stacksets[stackset] - raise ValidationError(name) + raise StackSetNotFoundException(name) def delete_stack_set(self, name: str) -> None: stackset_to_delete: Optional[FakeStackSet] = None @@ -755,8 +768,33 @@ class CloudFormationBackend(BaseBackend): if stackset_to_delete is not None: if stackset_to_delete.stack_instances: raise StackSetNotEmpty() + # We don't remove StackSets from the list - they still show up when calling list_stack_sets stackset_to_delete.delete() + def list_stack_sets(self) -> Iterable[FakeStackSet]: + return self.stacksets.values() + + def list_stack_set_operations(self, stackset_name: str) -> List[Dict[str, Any]]: + stackset = self.describe_stack_set(stackset_name) + return stackset.operations + + def stop_stack_set_operation(self, stackset_name: str, operation_id: str) -> None: + stackset = self.describe_stack_set(stackset_name) + stackset.update_operation(operation_id, "STOPPED") + + def describe_stack_set_operation( + self, stackset_name: str, operation_id: str + ) -> Tuple[FakeStackSet, Dict[str, Any]]: + stackset = self.describe_stack_set(stackset_name) + operation = stackset.get_operation(operation_id) + return stackset, operation + + def list_stack_set_operation_results( + self, stackset_name: str, operation_id: str + ) -> Dict[str, Any]: + stackset = self.describe_stack_set(stackset_name) + return stackset.get_operation(operation_id) + def create_stack_instances( self, stackset_name: str, @@ -768,7 +806,7 @@ class CloudFormationBackend(BaseBackend): """ The following parameters are not yet implemented: DeploymentTargets.AccountFilterType, DeploymentTargets.AccountsUrl, OperationPreferences, CallAs """ - stackset = self.get_stack_set(stackset_name) + stackset = self.describe_stack_set(stackset_name) operation_id = stackset.create_stack_instances( accounts=accounts, @@ -788,7 +826,7 @@ class CloudFormationBackend(BaseBackend): """ Calling this will update the parameters, but the actual resources are not updated """ - stack_set = self.get_stack_set(stackset_name) + stack_set = self.describe_stack_set(stackset_name) return stack_set.update_instances(accounts, regions, parameters) def update_stack_set( @@ -804,7 +842,7 @@ class CloudFormationBackend(BaseBackend): regions: List[str], operation_id: str, ) -> Dict[str, Any]: - stackset = self.get_stack_set(stackset_name) + stackset = self.describe_stack_set(stackset_name) resolved_parameters = self._resolve_update_parameters( instance=stackset, incoming_params=parameters ) @@ -827,7 +865,7 @@ class CloudFormationBackend(BaseBackend): """ The following parameters are not yet implemented: DeploymentTargets, OperationPreferences, RetainStacks, OperationId, CallAs """ - stackset = self.get_stack_set(stackset_name) + stackset = self.describe_stack_set(stackset_name) stackset.delete_stack_instances(accounts, regions) return stackset @@ -840,6 +878,8 @@ class CloudFormationBackend(BaseBackend): tags: Optional[Dict[str, str]] = None, role_arn: Optional[str] = None, enable_termination_protection: Optional[bool] = False, + timeout_in_mins: Optional[int] = None, + stack_policy_body: Optional[str] = None, ) -> FakeStack: """ The functionality behind EnableTerminationProtection is not yet implemented. @@ -857,6 +897,8 @@ class CloudFormationBackend(BaseBackend): role_arn=role_arn, cross_stack_resources=self.exports, enable_termination_protection=enable_termination_protection, + timeout_in_mins=timeout_in_mins, + stack_policy_body=stack_policy_body, ) self.stacks[stack_id] = new_stack self._validate_export_uniqueness(new_stack) @@ -1012,7 +1054,7 @@ class CloudFormationBackend(BaseBackend): def describe_stack_instance( self, stack_set_name: str, account_id: str, region: str ) -> Dict[str, Any]: - stack_set = self.get_stack_set(stack_set_name) + stack_set = self.describe_stack_set(stack_set_name) return stack_set.instances.get_instance(account_id, region).to_dict() def list_stack_instances(self, stackset_name: str) -> List[Dict[str, Any]]: @@ -1020,7 +1062,7 @@ class CloudFormationBackend(BaseBackend): Pagination is not yet implemented. The parameters StackInstanceAccount/StackInstanceRegion are not yet implemented. """ - stack_set = self.get_stack_set(stackset_name) + stack_set = self.describe_stack_set(stackset_name) return [i.to_dict() for i in stack_set.instances.stack_instances] def list_change_sets(self) -> Iterable[FakeChangeSet]: @@ -1076,6 +1118,26 @@ class CloudFormationBackend(BaseBackend): raise ValidationError(message=f"Stack: {stack_name} does not exist") stack.policy = policy_body + def describe_stack_resource( + self, stack_name: str, logical_resource_id: str + ) -> Tuple[FakeStack, Type[CloudFormationModel]]: + stack = self.get_stack(stack_name) + + for stack_resource in stack.stack_resources: + if stack_resource.logical_resource_id == logical_resource_id: # type: ignore[attr-defined] + return stack, stack_resource + + message = ( + f"Resource {logical_resource_id} does not exist for stack {stack_name}" + ) + raise ValidationError(stack_name, message) + + def describe_stack_resources( + self, stack_name: str + ) -> Tuple[FakeStack, Iterable[Type[CloudFormationModel]]]: + stack = self.get_stack(stack_name) + return stack, stack.stack_resources + def list_stack_resources( self, stack_name_or_id: str ) -> Iterable[Type[CloudFormationModel]]: @@ -1111,6 +1173,12 @@ class CloudFormationBackend(BaseBackend): next_token = str(token + 100) if len(all_exports) > token + 100 else None return exports, next_token + def describe_stack_events(self, stack_name: str) -> List[FakeEvent]: + return self.get_stack(stack_name).events + + def get_template(self, name_or_stack_id: str) -> Union[str, Dict[str, Any]]: + return self.get_stack(name_or_stack_id).template + def validate_template(self, template: str) -> List[Any]: return validate_template_cfn_lint(template) diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index c1f2ae08d..b2daa6141 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -125,6 +125,8 @@ class CloudFormationResponse(BaseResponse): template_url = self._get_param("TemplateURL") role_arn = self._get_param("RoleARN") enable_termination_protection = self._get_param("EnableTerminationProtection") + timeout_in_mins = self._get_param("TimeoutInMinutes") + stack_policy_body = self._get_param("StackPolicyBody") parameters_list = self._get_list_prefix("Parameters.member") tags = dict( (item["key"], item["value"]) @@ -151,6 +153,8 @@ class CloudFormationResponse(BaseResponse): tags=tags, role_arn=role_arn, enable_termination_protection=enable_termination_protection, + timeout_in_mins=timeout_in_mins, + stack_policy_body=stack_policy_body, ) if self.request_json: return json.dumps( @@ -271,37 +275,29 @@ class CloudFormationResponse(BaseResponse): def describe_stack_resource(self) -> str: stack_name = self._get_param("StackName") - stack = self.cloudformation_backend.get_stack(stack_name) logical_resource_id = self._get_param("LogicalResourceId") - - resource = None - for stack_resource in stack.stack_resources: - if stack_resource.logical_resource_id == logical_resource_id: # type: ignore[attr-defined] - resource = stack_resource - break - - if not resource: - message = ( - f"Resource {logical_resource_id} does not exist for stack {stack_name}" - ) - raise ValidationError(stack_name, message) + stack, resource = self.cloudformation_backend.describe_stack_resource( + stack_name, logical_resource_id + ) template = self.response_template(DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE) return template.render(stack=stack, resource=resource) def describe_stack_resources(self) -> str: stack_name = self._get_param("StackName") - stack = self.cloudformation_backend.get_stack(stack_name) + stack, resources = self.cloudformation_backend.describe_stack_resources( + stack_name + ) template = self.response_template(DESCRIBE_STACK_RESOURCES_RESPONSE) - return template.render(stack=stack) + return template.render(stack=stack, resources=resources) def describe_stack_events(self) -> str: stack_name = self._get_param("StackName") - stack = self.cloudformation_backend.get_stack(stack_name) + events = self.cloudformation_backend.describe_stack_events(stack_name) template = self.response_template(DESCRIBE_STACK_EVENTS_RESPONSE) - return template.render(stack=stack) + return template.render(events=events) def list_change_sets(self) -> str: change_sets = self.cloudformation_backend.list_change_sets() @@ -323,14 +319,14 @@ class CloudFormationResponse(BaseResponse): def get_template(self) -> str: name_or_stack_id = self.querystring.get("StackName")[0] # type: ignore[index] - stack = self.cloudformation_backend.get_stack(name_or_stack_id) + stack_template = self.cloudformation_backend.get_template(name_or_stack_id) if self.request_json: return json.dumps( { "GetTemplateResponse": { "GetTemplateResult": { - "TemplateBody": stack.template, + "TemplateBody": stack_template, "ResponseMetadata": { "RequestId": "2d06e36c-ac1d-11e0-a958-f9382b6eb86bEXAMPLE" }, @@ -340,7 +336,7 @@ class CloudFormationResponse(BaseResponse): ) else: template = self.response_template(GET_TEMPLATE_RESPONSE_TEMPLATE) - return template.render(stack=stack) + return template.render(stack_template=stack_template) def get_template_summary(self) -> str: stack_name = self._get_param("StackName") @@ -471,6 +467,9 @@ class CloudFormationResponse(BaseResponse): template_url = self._get_param("TemplateURL") permission_model = self._get_param("PermissionModel") parameters_list = self._get_list_prefix("Parameters.member") + admin_role = self._get_param("AdministrationRoleARN") + exec_role = self._get_param("ExecutionRoleName") + description = self._get_param("Description") tags = dict( (item["key"], item["value"]) for item in self._get_list_prefix("Tags.member") @@ -492,6 +491,9 @@ class CloudFormationResponse(BaseResponse): parameters=parameters, tags=tags, permission_model=permission_model, + admin_role=admin_role, + exec_role=exec_role, + description=description, ) if self.request_json: return json.dumps( @@ -549,7 +551,7 @@ class CloudFormationResponse(BaseResponse): def describe_stack_set(self) -> str: stackset_name = self._get_param("StackSetName") - stackset = self.cloudformation_backend.get_stack_set(stackset_name) + stackset = self.cloudformation_backend.describe_stack_set(stackset_name) if not stackset.admin_role: stackset.admin_role = f"arn:aws:iam::{self.current_account}:role/AWSCloudFormationStackSetAdministrationRole" @@ -572,7 +574,7 @@ class CloudFormationResponse(BaseResponse): return rendered def list_stack_sets(self) -> str: - stacksets = self.cloudformation_backend.stacksets + stacksets = self.cloudformation_backend.list_stack_sets() template = self.response_template(LIST_STACK_SETS_TEMPLATE) return template.render(stacksets=stacksets) @@ -584,31 +586,36 @@ class CloudFormationResponse(BaseResponse): def list_stack_set_operations(self) -> str: stackset_name = self._get_param("StackSetName") - stackset = self.cloudformation_backend.get_stack_set(stackset_name) + operations = self.cloudformation_backend.list_stack_set_operations( + stackset_name + ) template = self.response_template(LIST_STACK_SET_OPERATIONS_RESPONSE_TEMPLATE) - return template.render(stackset=stackset) + return template.render(operations=operations) def stop_stack_set_operation(self) -> str: stackset_name = self._get_param("StackSetName") operation_id = self._get_param("OperationId") - stackset = self.cloudformation_backend.get_stack_set(stackset_name) - stackset.update_operation(operation_id, "STOPPED") + self.cloudformation_backend.stop_stack_set_operation( + stackset_name, operation_id + ) template = self.response_template(STOP_STACK_SET_OPERATION_RESPONSE_TEMPLATE) return template.render() def describe_stack_set_operation(self) -> str: stackset_name = self._get_param("StackSetName") operation_id = self._get_param("OperationId") - stackset = self.cloudformation_backend.get_stack_set(stackset_name) - operation = stackset.get_operation(operation_id) + stackset, operation = self.cloudformation_backend.describe_stack_set_operation( + stackset_name, operation_id + ) template = self.response_template(DESCRIBE_STACKSET_OPERATION_RESPONSE_TEMPLATE) return template.render(stackset=stackset, operation=operation) def list_stack_set_operation_results(self) -> str: stackset_name = self._get_param("StackSetName") operation_id = self._get_param("OperationId") - stackset = self.cloudformation_backend.get_stack_set(stackset_name) - operation = stackset.get_operation(operation_id) + operation = self.cloudformation_backend.list_stack_set_operation_results( + stackset_name, operation_id + ) template = self.response_template( LIST_STACK_SET_OPERATION_RESULTS_RESPONSE_TEMPLATE ) @@ -814,7 +821,7 @@ DESCRIBE_STACKS_TEMPLATE = """ {{ stack.name }} {{ stack.stack_id }} {% if stack.change_set_id %} - {{ stack.change_set_id }} + {{ stack.change_set_id }} {% endif %} {{ stack.creation_time_iso_8601 }} @@ -861,6 +868,9 @@ DESCRIBE_STACKS_TEMPLATE = """ {% endfor %} {{ stack.enable_termination_protection }} + {% if stack.timeout_in_mins %} + {{ stack.timeout_in_mins }} + {% endif %} {% endfor %} @@ -887,7 +897,7 @@ DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE = """ DESCRIBE_STACK_RESOURCES_RESPONSE = """ - {% for resource in stack.stack_resources %} + {% for resource in resources %} {{ stack.stack_id }} {{ stack.name }} @@ -905,7 +915,7 @@ DESCRIBE_STACK_RESOURCES_RESPONSE = """ DESCRIBE_STACK_EVENTS_RESPONSE = """ - {% for event in stack.events[::-1] %} + {% for event in events[::-1] %} {{ event.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ') }} {{ event.resource_status }} @@ -983,7 +993,7 @@ LIST_STACKS_RESOURCES_RESPONSE = """ GET_TEMPLATE_RESPONSE_TEMPLATE = """ - {{ stack.template }} + {{ stack_template }} b9b4b068-3a41-11e5-94eb-example @@ -1054,6 +1064,10 @@ DESCRIBE_STACK_SET_RESPONSE_TEMPLATE = """ - {% for key, value in stacksets.items() %} + {% for stackset in stacksets %} - {{ value.name }} - {{ value.id }} - {{ value.status }} + {{ stackset.name }} + {{ stackset.id }} + {{ stackset.status }} {% endfor %} @@ -1180,7 +1194,7 @@ UPDATE_STACK_SET_RESPONSE_TEMPLATE = """ - {% for operation in stackset.operations %} + {% for operation in operations %} {{ operation.CreationTimestamp }} {{ operation.OperationId }} diff --git a/tests/terraformtests/terraform-tests.success.txt b/tests/terraformtests/terraform-tests.success.txt index 71de52c74..811fa29e8 100644 --- a/tests/terraformtests/terraform-tests.success.txt +++ b/tests/terraformtests/terraform-tests.success.txt @@ -61,6 +61,24 @@ batch: - TestAccBatchJobQueue_ComputeEnvironments_externalOrderUpdate ce: - TestAccCECostCategory +cloudformation: + - TestAccCloudFormationExportDataSource + - TestAccCloudFormationStackDataSource_DataSource + - TestAccCloudFormationStackSet_basic + - TestAccCloudFormationStackSet_templateBody + - TestAccCloudFormationStackSet_templateURL + - TestAccCloudFormationStackSet_description + - TestAccCloudFormationStackSet_operationPreferences + - TestAccCloudFormationStackSet_name + - TestAccCloudFormationStackSet_executionRoleName + - TestAccCloudFormationStackSet_disappears + - TestAccCloudFormationStack_basic + - TestAccCloudFormationStack_disappears + - TestAccCloudFormationStack_onFailure + - TestAccCloudFormationStack_yaml + - TestAccCloudFormationStack_withTransform + - TestAccCloudFormationStack_WithURLWithParams_withYAML + - TestAccCloudFormationStack_WithURL_withParams cloudfront: - TestAccCloudFrontDistributionDataSource_basic - TestAccCloudFrontDistribution_isIPV6Enabled diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index f9f06148c..f481146c3 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -321,19 +321,18 @@ def test_create_stack(): @mock_cloudformation -def test_create_stack_with_termination_protection(): +def test_create_stack_with_additional_properties(): cf_conn = boto3.client("cloudformation", region_name="us-east-1") cf_conn.create_stack( StackName="test_stack", TemplateBody=dummy_template_json, EnableTerminationProtection=True, + TimeoutInMinutes=25, ) stack = cf_conn.describe_stacks()["Stacks"][0] stack.should.have.key("StackName").equal("test_stack") stack.should.have.key("EnableTerminationProtection").equal(True) - - template = cf_conn.get_template(StackName="test_stack")["TemplateBody"] - template.should.equal(dummy_template) + stack.should.have.key("TimeoutInMinutes").equals(25) @mock_cloudformation @@ -765,9 +764,16 @@ def test_delete_stack_set_by_name(): ) cf_conn.delete_stack_set(StackSetName="teststackset") - cf_conn.describe_stack_set(StackSetName="teststackset")["StackSet"][ - "Status" - ].should.equal("DELETED") + stacks = cf_conn.list_stack_sets()["Summaries"] + stacks.should.have.length_of(1) + stacks[0].should.have.key("StackSetName").equals("teststackset") + stacks[0].should.have.key("Status").equals("DELETED") + + with pytest.raises(ClientError) as exc: + cf_conn.describe_stack_set(StackSetName="teststackset") + err = exc.value.response["Error"] + err["Code"].should.equal("StackSetNotFoundException") + err["Message"].should.equal("StackSet teststackset not found") @mock_cloudformation @@ -779,9 +785,10 @@ def test_delete_stack_set_by_id(): stack_set_id = response["StackSetId"] cf_conn.delete_stack_set(StackSetName=stack_set_id) - cf_conn.describe_stack_set(StackSetName="teststackset")["StackSet"][ - "Status" - ].should.equal("DELETED") + stacks = cf_conn.list_stack_sets()["Summaries"] + stacks.should.have.length_of(1) + stacks[0].should.have.key("StackSetName").equals("teststackset") + stacks[0].should.have.key("Status").equals("DELETED") @mock_cloudformation @@ -814,14 +821,20 @@ def test_delete_stack_set__while_instances_are_running(): def test_create_stack_set(): cf_conn = boto3.client("cloudformation", region_name="us-east-1") response = cf_conn.create_stack_set( - StackSetName="teststackset", TemplateBody=dummy_template_json + StackSetName="teststackset", + TemplateBody=dummy_template_json, + Description="desc", + AdministrationRoleARN="admin/role/arn:asdfasdfadsf", ) - - cf_conn.describe_stack_set(StackSetName="teststackset")["StackSet"][ - "TemplateBody" - ].should.equal(dummy_template_json) response["StackSetId"].should_not.equal(None) + stack_set = cf_conn.describe_stack_set(StackSetName="teststackset")["StackSet"] + stack_set["TemplateBody"].should.equal(dummy_template_json) + stack_set.should.have.key("AdministrationRoleARN").should.equal( + "admin/role/arn:asdfasdfadsf" + ) + stack_set.should.have.key("Description").equals("desc") + @mock_cloudformation @pytest.mark.parametrize("name", ["1234", "stack_set", "-set"]) diff --git a/tests/test_cloudformation/test_cloudformation_stack_policies.py b/tests/test_cloudformation/test_cloudformation_stack_policies.py index c5369fa36..067ab17b1 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_policies.py +++ b/tests/test_cloudformation/test_cloudformation_stack_policies.py @@ -71,6 +71,19 @@ def test_set_stack_policy_with_body(): resp.should.have.key("StackPolicyBody").equals(policy) +@mock_cloudformation +def test_set_stack_policy_on_create(): + cf_conn = boto3.client("cloudformation", region_name="us-east-1") + cf_conn.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + StackPolicyBody="stack_policy_body", + ) + + resp = cf_conn.get_stack_policy(StackName="test_stack") + resp.should.have.key("StackPolicyBody").equals("stack_policy_body") + + @mock_cloudformation @mock_s3 def test_set_stack_policy_with_url():