Cloudformation: Various attributes (#5732)

This commit is contained in:
Bert Blommers 2022-12-03 21:21:52 -01:00 committed by GitHub
parent aeb507f091
commit d10a8e9900
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 230 additions and 94 deletions

View File

@ -565,7 +565,7 @@
## cloudformation ## cloudformation
<details> <details>
<summary>34% implemented</summary> <summary>50% implemented</summary>
- [ ] activate_type - [ ] activate_type
- [ ] batch_describe_type_configurations - [ ] batch_describe_type_configurations
@ -586,13 +586,13 @@
- [ ] describe_change_set_hooks - [ ] describe_change_set_hooks
- [ ] describe_publisher - [ ] describe_publisher
- [ ] describe_stack_drift_detection_status - [ ] describe_stack_drift_detection_status
- [ ] describe_stack_events - [X] describe_stack_events
- [X] describe_stack_instance - [X] describe_stack_instance
- [ ] describe_stack_resource - [X] describe_stack_resource
- [ ] describe_stack_resource_drifts - [ ] describe_stack_resource_drifts
- [ ] describe_stack_resources - [X] describe_stack_resources
- [ ] describe_stack_set - [X] describe_stack_set
- [ ] describe_stack_set_operation - [X] describe_stack_set_operation
- [X] describe_stacks - [X] describe_stacks
- [ ] describe_type - [ ] describe_type
- [ ] describe_type_registration - [ ] describe_type_registration
@ -602,7 +602,7 @@
- [ ] estimate_template_cost - [ ] estimate_template_cost
- [X] execute_change_set - [X] execute_change_set
- [X] get_stack_policy - [X] get_stack_policy
- [ ] get_template - [X] get_template
- [ ] get_template_summary - [ ] get_template_summary
- [ ] import_stacks_to_stack_set - [ ] import_stacks_to_stack_set
- [X] list_change_sets - [X] list_change_sets
@ -610,9 +610,9 @@
- [ ] list_imports - [ ] list_imports
- [X] list_stack_instances - [X] list_stack_instances
- [X] list_stack_resources - [X] list_stack_resources
- [ ] list_stack_set_operation_results - [X] list_stack_set_operation_results
- [ ] list_stack_set_operations - [X] list_stack_set_operations
- [ ] list_stack_sets - [X] list_stack_sets
- [X] list_stacks - [X] list_stacks
- [ ] list_type_registrations - [ ] list_type_registrations
- [ ] list_type_versions - [ ] list_type_versions
@ -626,7 +626,7 @@
- [ ] set_type_configuration - [ ] set_type_configuration
- [ ] set_type_default_version - [ ] set_type_default_version
- [ ] signal_resource - [ ] signal_resource
- [ ] stop_stack_set_operation - [X] stop_stack_set_operation
- [ ] test_type - [ ] test_type
- [X] update_stack - [X] update_stack
- [X] update_stack_instances - [X] update_stack_instances

View File

@ -62,13 +62,13 @@ cloudformation
- [ ] describe_change_set_hooks - [ ] describe_change_set_hooks
- [ ] describe_publisher - [ ] describe_publisher
- [ ] describe_stack_drift_detection_status - [ ] describe_stack_drift_detection_status
- [ ] describe_stack_events - [X] describe_stack_events
- [X] describe_stack_instance - [X] describe_stack_instance
- [ ] describe_stack_resource - [X] describe_stack_resource
- [ ] describe_stack_resource_drifts - [ ] describe_stack_resource_drifts
- [ ] describe_stack_resources - [X] describe_stack_resources
- [ ] describe_stack_set - [X] describe_stack_set
- [ ] describe_stack_set_operation - [X] describe_stack_set_operation
- [X] describe_stacks - [X] describe_stacks
- [ ] describe_type - [ ] describe_type
- [ ] describe_type_registration - [ ] describe_type_registration
@ -78,7 +78,7 @@ cloudformation
- [ ] estimate_template_cost - [ ] estimate_template_cost
- [X] execute_change_set - [X] execute_change_set
- [X] get_stack_policy - [X] get_stack_policy
- [ ] get_template - [X] get_template
- [ ] get_template_summary - [ ] get_template_summary
- [ ] import_stacks_to_stack_set - [ ] import_stacks_to_stack_set
- [X] list_change_sets - [X] list_change_sets
@ -91,9 +91,9 @@ cloudformation
- [X] list_stack_resources - [X] list_stack_resources
- [ ] list_stack_set_operation_results - [X] list_stack_set_operation_results
- [ ] list_stack_set_operations - [X] list_stack_set_operations
- [ ] list_stack_sets - [X] list_stack_sets
- [X] list_stacks - [X] list_stacks
- [ ] list_type_registrations - [ ] list_type_registrations
- [ ] list_type_versions - [ ] list_type_versions
@ -111,7 +111,7 @@ cloudformation
- [ ] set_type_configuration - [ ] set_type_configuration
- [ ] set_type_default_version - [ ] set_type_default_version
- [ ] signal_resource - [ ] signal_resource
- [ ] stop_stack_set_operation - [X] stop_stack_set_operation
- [ ] test_type - [ ] test_type
- [X] update_stack - [X] update_stack
- [X] update_stack_instances - [X] update_stack_instances

View File

@ -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): class UnsupportedAttribute(ValidationError):
def __init__(self, resource: str, attr: str): def __init__(self, resource: str, attr: str):
template = Template(ERROR_RESPONSE) template = Template(ERROR_RESPONSE)

View File

@ -26,7 +26,7 @@ from .utils import (
yaml_tag_constructor, yaml_tag_constructor,
validate_template_cfn_lint, validate_template_cfn_lint,
) )
from .exceptions import ValidationError, StackSetNotEmpty from .exceptions import ValidationError, StackSetNotEmpty, StackSetNotFoundException
class FakeStackSet(BaseModel): class FakeStackSet(BaseModel):
@ -40,9 +40,9 @@ class FakeStackSet(BaseModel):
description: Optional[str], description: Optional[str],
parameters: Dict[str, str], parameters: Dict[str, str],
permission_model: str, permission_model: str,
tags: Optional[Dict[str, str]] = None, tags: Optional[Dict[str, str]],
admin_role: str = "AWSCloudFormationStackSetAdministrationRole", admin_role: Optional[str],
execution_role: str = "AWSCloudFormationStackSetExecutionRole", execution_role: Optional[str],
): ):
self.id = stackset_id self.id = stackset_id
self.arn = generate_stackset_arn(stackset_id, region, account_id) self.arn = generate_stackset_arn(stackset_id, region, account_id)
@ -53,7 +53,7 @@ class FakeStackSet(BaseModel):
self.tags = tags self.tags = tags
self.admin_role = admin_role self.admin_role = admin_role
self.admin_role_arn = f"arn:aws:iam::{account_id}:role/{self.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.status = "ACTIVE"
self.instances = FakeStackInstances( self.instances = FakeStackInstances(
account_id, template, parameters, self.id, self.name account_id, template, parameters, self.id, self.name
@ -76,8 +76,10 @@ class FakeStackSet(BaseModel):
"OperationId": operation_id, "OperationId": operation_id,
"Action": action, "Action": action,
"Status": status, "Status": status,
"CreationTimestamp": datetime.now(), "CreationTimestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f"),
"EndTimestamp": datetime.now() + timedelta(minutes=2), "EndTimestamp": (datetime.now() + timedelta(minutes=2)).strftime(
"%Y-%m-%dT%H:%M:%S.%f"
),
"Instances": [ "Instances": [
{account: region} for account in accounts for region in regions {account: region} for account in accounts for region in regions
], ],
@ -368,6 +370,8 @@ class FakeStack(BaseModel):
role_arn: Optional[str] = None, role_arn: Optional[str] = None,
cross_stack_resources: Optional[Dict[str, Export]] = None, cross_stack_resources: Optional[Dict[str, Export]] = None,
enable_termination_protection: Optional[bool] = False, enable_termination_protection: Optional[bool] = False,
timeout_in_mins: Optional[int] = None,
stack_policy_body: Optional[str] = None,
): ):
self.stack_id = stack_id self.stack_id = stack_id
self.name = name self.name = name
@ -385,7 +389,8 @@ class FakeStack(BaseModel):
self.role_arn = role_arn self.role_arn = role_arn
self.tags = tags if tags else {} self.tags = tags if tags else {}
self.events: List[FakeEvent] = [] 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.cross_stack_resources: Dict[str, Export] = cross_stack_resources or {}
self.enable_termination_protection: bool = ( self.enable_termination_protection: bool = (
@ -716,6 +721,9 @@ class CloudFormationBackend(BaseBackend):
parameters: Dict[str, str], parameters: Dict[str, str],
tags: Dict[str, str], tags: Dict[str, str],
permission_model: str, permission_model: str,
admin_role: Optional[str],
exec_role: Optional[str],
description: Optional[str],
) -> FakeStackSet: ) -> FakeStackSet:
""" """
The following parameters are not yet implemented: StackId, AdministrationRoleARN, AutoDeployment, ExecutionRoleName, CallAs, ClientRequestToken, ManagedExecution 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, region=self.region_name,
template=template, template=template,
parameters=parameters, parameters=parameters,
description=None, description=description,
tags=tags, tags=tags,
permission_model=permission_model, permission_model=permission_model,
admin_role=admin_role,
execution_role=exec_role,
) )
self.stacksets[stackset_id] = new_stackset self.stacksets[stackset_id] = new_stackset
return 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() stacksets = self.stacksets.keys()
if name in stacksets: if name in stacksets and self.stacksets[name].status != "DELETED":
return self.stacksets[name] return self.stacksets[name]
for stackset in stacksets: 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] return self.stacksets[stackset]
raise ValidationError(name) raise StackSetNotFoundException(name)
def delete_stack_set(self, name: str) -> None: def delete_stack_set(self, name: str) -> None:
stackset_to_delete: Optional[FakeStackSet] = 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 is not None:
if stackset_to_delete.stack_instances: if stackset_to_delete.stack_instances:
raise StackSetNotEmpty() raise StackSetNotEmpty()
# We don't remove StackSets from the list - they still show up when calling list_stack_sets
stackset_to_delete.delete() 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( def create_stack_instances(
self, self,
stackset_name: str, stackset_name: str,
@ -768,7 +806,7 @@ class CloudFormationBackend(BaseBackend):
""" """
The following parameters are not yet implemented: DeploymentTargets.AccountFilterType, DeploymentTargets.AccountsUrl, OperationPreferences, CallAs 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( operation_id = stackset.create_stack_instances(
accounts=accounts, accounts=accounts,
@ -788,7 +826,7 @@ class CloudFormationBackend(BaseBackend):
""" """
Calling this will update the parameters, but the actual resources are not updated 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) return stack_set.update_instances(accounts, regions, parameters)
def update_stack_set( def update_stack_set(
@ -804,7 +842,7 @@ class CloudFormationBackend(BaseBackend):
regions: List[str], regions: List[str],
operation_id: str, operation_id: str,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
stackset = self.get_stack_set(stackset_name) stackset = self.describe_stack_set(stackset_name)
resolved_parameters = self._resolve_update_parameters( resolved_parameters = self._resolve_update_parameters(
instance=stackset, incoming_params=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 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) stackset.delete_stack_instances(accounts, regions)
return stackset return stackset
@ -840,6 +878,8 @@ class CloudFormationBackend(BaseBackend):
tags: Optional[Dict[str, str]] = None, tags: Optional[Dict[str, str]] = None,
role_arn: Optional[str] = None, role_arn: Optional[str] = None,
enable_termination_protection: Optional[bool] = False, enable_termination_protection: Optional[bool] = False,
timeout_in_mins: Optional[int] = None,
stack_policy_body: Optional[str] = None,
) -> FakeStack: ) -> FakeStack:
""" """
The functionality behind EnableTerminationProtection is not yet implemented. The functionality behind EnableTerminationProtection is not yet implemented.
@ -857,6 +897,8 @@ class CloudFormationBackend(BaseBackend):
role_arn=role_arn, role_arn=role_arn,
cross_stack_resources=self.exports, cross_stack_resources=self.exports,
enable_termination_protection=enable_termination_protection, 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.stacks[stack_id] = new_stack
self._validate_export_uniqueness(new_stack) self._validate_export_uniqueness(new_stack)
@ -1012,7 +1054,7 @@ class CloudFormationBackend(BaseBackend):
def describe_stack_instance( def describe_stack_instance(
self, stack_set_name: str, account_id: str, region: str self, stack_set_name: str, account_id: str, region: str
) -> Dict[str, Any]: ) -> 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() return stack_set.instances.get_instance(account_id, region).to_dict()
def list_stack_instances(self, stackset_name: str) -> List[Dict[str, Any]]: def list_stack_instances(self, stackset_name: str) -> List[Dict[str, Any]]:
@ -1020,7 +1062,7 @@ class CloudFormationBackend(BaseBackend):
Pagination is not yet implemented. Pagination is not yet implemented.
The parameters StackInstanceAccount/StackInstanceRegion are 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] return [i.to_dict() for i in stack_set.instances.stack_instances]
def list_change_sets(self) -> Iterable[FakeChangeSet]: def list_change_sets(self) -> Iterable[FakeChangeSet]:
@ -1076,6 +1118,26 @@ class CloudFormationBackend(BaseBackend):
raise ValidationError(message=f"Stack: {stack_name} does not exist") raise ValidationError(message=f"Stack: {stack_name} does not exist")
stack.policy = policy_body 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( def list_stack_resources(
self, stack_name_or_id: str self, stack_name_or_id: str
) -> Iterable[Type[CloudFormationModel]]: ) -> Iterable[Type[CloudFormationModel]]:
@ -1111,6 +1173,12 @@ class CloudFormationBackend(BaseBackend):
next_token = str(token + 100) if len(all_exports) > token + 100 else None next_token = str(token + 100) if len(all_exports) > token + 100 else None
return exports, next_token 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]: def validate_template(self, template: str) -> List[Any]:
return validate_template_cfn_lint(template) return validate_template_cfn_lint(template)

View File

@ -125,6 +125,8 @@ class CloudFormationResponse(BaseResponse):
template_url = self._get_param("TemplateURL") template_url = self._get_param("TemplateURL")
role_arn = self._get_param("RoleARN") role_arn = self._get_param("RoleARN")
enable_termination_protection = self._get_param("EnableTerminationProtection") 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") parameters_list = self._get_list_prefix("Parameters.member")
tags = dict( tags = dict(
(item["key"], item["value"]) (item["key"], item["value"])
@ -151,6 +153,8 @@ class CloudFormationResponse(BaseResponse):
tags=tags, tags=tags,
role_arn=role_arn, role_arn=role_arn,
enable_termination_protection=enable_termination_protection, enable_termination_protection=enable_termination_protection,
timeout_in_mins=timeout_in_mins,
stack_policy_body=stack_policy_body,
) )
if self.request_json: if self.request_json:
return json.dumps( return json.dumps(
@ -271,37 +275,29 @@ class CloudFormationResponse(BaseResponse):
def describe_stack_resource(self) -> str: def describe_stack_resource(self) -> str:
stack_name = self._get_param("StackName") stack_name = self._get_param("StackName")
stack = self.cloudformation_backend.get_stack(stack_name)
logical_resource_id = self._get_param("LogicalResourceId") logical_resource_id = self._get_param("LogicalResourceId")
stack, resource = self.cloudformation_backend.describe_stack_resource(
resource = None stack_name, logical_resource_id
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)
template = self.response_template(DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE) template = self.response_template(DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE)
return template.render(stack=stack, resource=resource) return template.render(stack=stack, resource=resource)
def describe_stack_resources(self) -> str: def describe_stack_resources(self) -> str:
stack_name = self._get_param("StackName") 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) 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: def describe_stack_events(self) -> str:
stack_name = self._get_param("StackName") 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) template = self.response_template(DESCRIBE_STACK_EVENTS_RESPONSE)
return template.render(stack=stack) return template.render(events=events)
def list_change_sets(self) -> str: def list_change_sets(self) -> str:
change_sets = self.cloudformation_backend.list_change_sets() change_sets = self.cloudformation_backend.list_change_sets()
@ -323,14 +319,14 @@ class CloudFormationResponse(BaseResponse):
def get_template(self) -> str: def get_template(self) -> str:
name_or_stack_id = self.querystring.get("StackName")[0] # type: ignore[index] 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: if self.request_json:
return json.dumps( return json.dumps(
{ {
"GetTemplateResponse": { "GetTemplateResponse": {
"GetTemplateResult": { "GetTemplateResult": {
"TemplateBody": stack.template, "TemplateBody": stack_template,
"ResponseMetadata": { "ResponseMetadata": {
"RequestId": "2d06e36c-ac1d-11e0-a958-f9382b6eb86bEXAMPLE" "RequestId": "2d06e36c-ac1d-11e0-a958-f9382b6eb86bEXAMPLE"
}, },
@ -340,7 +336,7 @@ class CloudFormationResponse(BaseResponse):
) )
else: else:
template = self.response_template(GET_TEMPLATE_RESPONSE_TEMPLATE) 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: def get_template_summary(self) -> str:
stack_name = self._get_param("StackName") stack_name = self._get_param("StackName")
@ -471,6 +467,9 @@ class CloudFormationResponse(BaseResponse):
template_url = self._get_param("TemplateURL") template_url = self._get_param("TemplateURL")
permission_model = self._get_param("PermissionModel") permission_model = self._get_param("PermissionModel")
parameters_list = self._get_list_prefix("Parameters.member") 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( tags = dict(
(item["key"], item["value"]) (item["key"], item["value"])
for item in self._get_list_prefix("Tags.member") for item in self._get_list_prefix("Tags.member")
@ -492,6 +491,9 @@ class CloudFormationResponse(BaseResponse):
parameters=parameters, parameters=parameters,
tags=tags, tags=tags,
permission_model=permission_model, permission_model=permission_model,
admin_role=admin_role,
exec_role=exec_role,
description=description,
) )
if self.request_json: if self.request_json:
return json.dumps( return json.dumps(
@ -549,7 +551,7 @@ class CloudFormationResponse(BaseResponse):
def describe_stack_set(self) -> str: def describe_stack_set(self) -> str:
stackset_name = self._get_param("StackSetName") 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: if not stackset.admin_role:
stackset.admin_role = f"arn:aws:iam::{self.current_account}:role/AWSCloudFormationStackSetAdministrationRole" stackset.admin_role = f"arn:aws:iam::{self.current_account}:role/AWSCloudFormationStackSetAdministrationRole"
@ -572,7 +574,7 @@ class CloudFormationResponse(BaseResponse):
return rendered return rendered
def list_stack_sets(self) -> str: 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) template = self.response_template(LIST_STACK_SETS_TEMPLATE)
return template.render(stacksets=stacksets) return template.render(stacksets=stacksets)
@ -584,31 +586,36 @@ class CloudFormationResponse(BaseResponse):
def list_stack_set_operations(self) -> str: def list_stack_set_operations(self) -> str:
stackset_name = self._get_param("StackSetName") 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) 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: def stop_stack_set_operation(self) -> str:
stackset_name = self._get_param("StackSetName") stackset_name = self._get_param("StackSetName")
operation_id = self._get_param("OperationId") operation_id = self._get_param("OperationId")
stackset = self.cloudformation_backend.get_stack_set(stackset_name) self.cloudformation_backend.stop_stack_set_operation(
stackset.update_operation(operation_id, "STOPPED") stackset_name, operation_id
)
template = self.response_template(STOP_STACK_SET_OPERATION_RESPONSE_TEMPLATE) template = self.response_template(STOP_STACK_SET_OPERATION_RESPONSE_TEMPLATE)
return template.render() return template.render()
def describe_stack_set_operation(self) -> str: def describe_stack_set_operation(self) -> str:
stackset_name = self._get_param("StackSetName") stackset_name = self._get_param("StackSetName")
operation_id = self._get_param("OperationId") operation_id = self._get_param("OperationId")
stackset = self.cloudformation_backend.get_stack_set(stackset_name) stackset, operation = self.cloudformation_backend.describe_stack_set_operation(
operation = stackset.get_operation(operation_id) stackset_name, operation_id
)
template = self.response_template(DESCRIBE_STACKSET_OPERATION_RESPONSE_TEMPLATE) template = self.response_template(DESCRIBE_STACKSET_OPERATION_RESPONSE_TEMPLATE)
return template.render(stackset=stackset, operation=operation) return template.render(stackset=stackset, operation=operation)
def list_stack_set_operation_results(self) -> str: def list_stack_set_operation_results(self) -> str:
stackset_name = self._get_param("StackSetName") stackset_name = self._get_param("StackSetName")
operation_id = self._get_param("OperationId") operation_id = self._get_param("OperationId")
stackset = self.cloudformation_backend.get_stack_set(stackset_name) operation = self.cloudformation_backend.list_stack_set_operation_results(
operation = stackset.get_operation(operation_id) stackset_name, operation_id
)
template = self.response_template( template = self.response_template(
LIST_STACK_SET_OPERATION_RESULTS_RESPONSE_TEMPLATE LIST_STACK_SET_OPERATION_RESULTS_RESPONSE_TEMPLATE
) )
@ -814,7 +821,7 @@ DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResponse>
<StackName>{{ stack.name }}</StackName> <StackName>{{ stack.name }}</StackName>
<StackId>{{ stack.stack_id }}</StackId> <StackId>{{ stack.stack_id }}</StackId>
{% if stack.change_set_id %} {% if stack.change_set_id %}
<ChangeSetId>{{ stack.change_set_id }}</ChangeSetId> <ChangeSetId>{{ stack.change_set_id }}</stack.timeout_in_minsChangeSetId>
{% endif %} {% endif %}
<Description><![CDATA[{{ stack.description }}]]></Description> <Description><![CDATA[{{ stack.description }}]]></Description>
<CreationTime>{{ stack.creation_time_iso_8601 }}</CreationTime> <CreationTime>{{ stack.creation_time_iso_8601 }}</CreationTime>
@ -861,6 +868,9 @@ DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResponse>
{% endfor %} {% endfor %}
</Tags> </Tags>
<EnableTerminationProtection>{{ stack.enable_termination_protection }}</EnableTerminationProtection> <EnableTerminationProtection>{{ stack.enable_termination_protection }}</EnableTerminationProtection>
{% if stack.timeout_in_mins %}
<TimeoutInMinutes>{{ stack.timeout_in_mins }}</TimeoutInMinutes>
{% endif %}
</member> </member>
{% endfor %} {% endfor %}
</Stacks> </Stacks>
@ -887,7 +897,7 @@ DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE = """<DescribeStackResourceResponse>
DESCRIBE_STACK_RESOURCES_RESPONSE = """<DescribeStackResourcesResponse> DESCRIBE_STACK_RESOURCES_RESPONSE = """<DescribeStackResourcesResponse>
<DescribeStackResourcesResult> <DescribeStackResourcesResult>
<StackResources> <StackResources>
{% for resource in stack.stack_resources %} {% for resource in resources %}
<member> <member>
<StackId>{{ stack.stack_id }}</StackId> <StackId>{{ stack.stack_id }}</StackId>
<StackName>{{ stack.name }}</StackName> <StackName>{{ stack.name }}</StackName>
@ -905,7 +915,7 @@ DESCRIBE_STACK_RESOURCES_RESPONSE = """<DescribeStackResourcesResponse>
DESCRIBE_STACK_EVENTS_RESPONSE = """<DescribeStackEventsResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/"> DESCRIBE_STACK_EVENTS_RESPONSE = """<DescribeStackEventsResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
<DescribeStackEventsResult> <DescribeStackEventsResult>
<StackEvents> <StackEvents>
{% for event in stack.events[::-1] %} {% for event in events[::-1] %}
<member> <member>
<Timestamp>{{ event.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ') }}</Timestamp> <Timestamp>{{ event.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ') }}</Timestamp>
<ResourceStatus>{{ event.resource_status }}</ResourceStatus> <ResourceStatus>{{ event.resource_status }}</ResourceStatus>
@ -983,7 +993,7 @@ LIST_STACKS_RESOURCES_RESPONSE = """<ListStackResourcesResponse>
GET_TEMPLATE_RESPONSE_TEMPLATE = """<GetTemplateResponse> GET_TEMPLATE_RESPONSE_TEMPLATE = """<GetTemplateResponse>
<GetTemplateResult> <GetTemplateResult>
<TemplateBody>{{ stack.template }}</TemplateBody> <TemplateBody>{{ stack_template }}</TemplateBody>
</GetTemplateResult> </GetTemplateResult>
<ResponseMetadata> <ResponseMetadata>
<RequestId>b9b4b068-3a41-11e5-94eb-example</RequestId> <RequestId>b9b4b068-3a41-11e5-94eb-example</RequestId>
@ -1054,6 +1064,10 @@ DESCRIBE_STACK_SET_RESPONSE_TEMPLATE = """<DescribeStackSetResponse xmlns="http:
{% endfor %} {% endfor %}
</Tags> </Tags>
<Status>{{ stackset.status }}</Status> <Status>{{ stackset.status }}</Status>
<PermissionModel>{{ stackset.permission_model }}</PermissionModel>
{% if stackset.description %}
<Description>{{ stackset.description }}</Description>
{% endif %}
</StackSet> </StackSet>
</DescribeStackSetResult> </DescribeStackSetResult>
<ResponseMetadata> <ResponseMetadata>
@ -1142,11 +1156,11 @@ DESCRIBE_STACK_INSTANCE_TEMPLATE = """<DescribeStackInstanceResponse xmlns="http
LIST_STACK_SETS_TEMPLATE = """<ListStackSetsResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/"> LIST_STACK_SETS_TEMPLATE = """<ListStackSetsResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<ListStackSetsResult> <ListStackSetsResult>
<Summaries> <Summaries>
{% for key, value in stacksets.items() %} {% for stackset in stacksets %}
<member> <member>
<StackSetName>{{ value.name }}</StackSetName> <StackSetName>{{ stackset.name }}</StackSetName>
<StackSetId>{{ value.id }}</StackSetId> <StackSetId>{{ stackset.id }}</StackSetId>
<Status>{{ value.status }}</Status> <Status>{{ stackset.status }}</Status>
</member> </member>
{% endfor %} {% endfor %}
</Summaries> </Summaries>
@ -1180,7 +1194,7 @@ UPDATE_STACK_SET_RESPONSE_TEMPLATE = """<UpdateStackSetResponse xmlns="http://in
LIST_STACK_SET_OPERATIONS_RESPONSE_TEMPLATE = """<ListStackSetOperationsResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/"> LIST_STACK_SET_OPERATIONS_RESPONSE_TEMPLATE = """<ListStackSetOperationsResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<ListStackSetOperationsResult> <ListStackSetOperationsResult>
<Summaries> <Summaries>
{% for operation in stackset.operations %} {% for operation in operations %}
<member> <member>
<CreationTimestamp>{{ operation.CreationTimestamp }}</CreationTimestamp> <CreationTimestamp>{{ operation.CreationTimestamp }}</CreationTimestamp>
<OperationId>{{ operation.OperationId }}</OperationId> <OperationId>{{ operation.OperationId }}</OperationId>

View File

@ -61,6 +61,24 @@ batch:
- TestAccBatchJobQueue_ComputeEnvironments_externalOrderUpdate - TestAccBatchJobQueue_ComputeEnvironments_externalOrderUpdate
ce: ce:
- TestAccCECostCategory - 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: cloudfront:
- TestAccCloudFrontDistributionDataSource_basic - TestAccCloudFrontDistributionDataSource_basic
- TestAccCloudFrontDistribution_isIPV6Enabled - TestAccCloudFrontDistribution_isIPV6Enabled

View File

@ -321,19 +321,18 @@ def test_create_stack():
@mock_cloudformation @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 = boto3.client("cloudformation", region_name="us-east-1")
cf_conn.create_stack( cf_conn.create_stack(
StackName="test_stack", StackName="test_stack",
TemplateBody=dummy_template_json, TemplateBody=dummy_template_json,
EnableTerminationProtection=True, EnableTerminationProtection=True,
TimeoutInMinutes=25,
) )
stack = cf_conn.describe_stacks()["Stacks"][0] stack = cf_conn.describe_stacks()["Stacks"][0]
stack.should.have.key("StackName").equal("test_stack") stack.should.have.key("StackName").equal("test_stack")
stack.should.have.key("EnableTerminationProtection").equal(True) stack.should.have.key("EnableTerminationProtection").equal(True)
stack.should.have.key("TimeoutInMinutes").equals(25)
template = cf_conn.get_template(StackName="test_stack")["TemplateBody"]
template.should.equal(dummy_template)
@mock_cloudformation @mock_cloudformation
@ -765,9 +764,16 @@ def test_delete_stack_set_by_name():
) )
cf_conn.delete_stack_set(StackSetName="teststackset") cf_conn.delete_stack_set(StackSetName="teststackset")
cf_conn.describe_stack_set(StackSetName="teststackset")["StackSet"][ stacks = cf_conn.list_stack_sets()["Summaries"]
"Status" stacks.should.have.length_of(1)
].should.equal("DELETED") 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 @mock_cloudformation
@ -779,9 +785,10 @@ def test_delete_stack_set_by_id():
stack_set_id = response["StackSetId"] stack_set_id = response["StackSetId"]
cf_conn.delete_stack_set(StackSetName=stack_set_id) cf_conn.delete_stack_set(StackSetName=stack_set_id)
cf_conn.describe_stack_set(StackSetName="teststackset")["StackSet"][ stacks = cf_conn.list_stack_sets()["Summaries"]
"Status" stacks.should.have.length_of(1)
].should.equal("DELETED") stacks[0].should.have.key("StackSetName").equals("teststackset")
stacks[0].should.have.key("Status").equals("DELETED")
@mock_cloudformation @mock_cloudformation
@ -814,14 +821,20 @@ def test_delete_stack_set__while_instances_are_running():
def test_create_stack_set(): def test_create_stack_set():
cf_conn = boto3.client("cloudformation", region_name="us-east-1") cf_conn = boto3.client("cloudformation", region_name="us-east-1")
response = cf_conn.create_stack_set( 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) 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 @mock_cloudformation
@pytest.mark.parametrize("name", ["1234", "stack_set", "-set"]) @pytest.mark.parametrize("name", ["1234", "stack_set", "-set"])

View File

@ -71,6 +71,19 @@ def test_set_stack_policy_with_body():
resp.should.have.key("StackPolicyBody").equals(policy) 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_cloudformation
@mock_s3 @mock_s3
def test_set_stack_policy_with_url(): def test_set_stack_policy_with_url():