diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 61bfaf19..98e426e3 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -470,7 +470,7 @@
- [ ] upgrade_applied_schema
- [ ] upgrade_published_schema
-## cloudformation - 21% implemented
+## cloudformation - 56% implemented
- [ ] cancel_update_stack
- [ ] continue_update_rollback
- [X] create_change_set
@@ -488,7 +488,7 @@
- [ ] describe_stack_resource
- [ ] describe_stack_resources
- [X] describe_stack_set
-- [ ] describe_stack_set_operation
+- [X] describe_stack_set_operation
- [X] describe_stacks
- [ ] estimate_template_cost
- [X] execute_change_set
@@ -500,13 +500,13 @@
- [ ] list_imports
- [X] list_stack_instances
- [X] list_stack_resources
-- [ ] list_stack_set_operation_results
+- [X] list_stack_set_operation_results
- [X] list_stack_set_operations
- [X] list_stack_sets
- [X] list_stacks
- [ ] set_stack_policy
- [ ] signal_resource
-- [ ] stop_stack_set_operation
+- [X] stop_stack_set_operation
- [X] update_stack
- [X] update_stack_instances
- [X] update_stack_set
diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py
index ed2ac1a1..bbb1a0fd 100644
--- a/moto/cloudformation/models.py
+++ b/moto/cloudformation/models.py
@@ -22,7 +22,10 @@ from .exceptions import ValidationError
class FakeStackSet(BaseModel):
- def __init__(self, stackset_id, name, template, region='us-east-1', status='ACTIVE', description=None, parameters=None, tags=None, admin_role=None, execution_role=None):
+ def __init__(self, stackset_id, name, template, region='us-east-1',
+ status='ACTIVE', description=None, parameters=None, tags=None,
+ admin_role='AWSCloudFormationStackSetAdministrationRole',
+ execution_role='AWSCloudFormationStackSetExecutionRole'):
self.id = stackset_id
self.arn = generate_stackset_arn(stackset_id, region)
self.name = name
@@ -34,24 +37,33 @@ class FakeStackSet(BaseModel):
self.execution_role = execution_role
self.status = status
self.instances = FakeStackInstances(parameters, self.id, self.name)
+ self.stack_instances = self.instances.stack_instances
self.operations = []
- @property
- def stack_instances(self):
- return self.instances.stack_instances
-
- def _create_operation(self, operation_id, action, status):
+ def _create_operation(self, operation_id, action, status, accounts=[], regions=[]):
operation = {
'OperationId': str(operation_id),
'Action': action,
'Status': status,
'CreationTimestamp': datetime.now(),
'EndTimestamp': datetime.now() + timedelta(minutes=2),
+ 'Instances': [{account: region} for account in accounts for region in regions],
}
self.operations += [operation]
return operation
+ def get_operation(self, operation_id):
+ for operation in self.operations:
+ if operation_id == operation['OperationId']:
+ return operation
+ raise ValidationError(operation_id)
+
+ def update_operation(self, operation_id, status):
+ operation = self.get_operation(operation_id)
+ operation['Status'] = status
+ return operation_id
+
def delete(self):
self.status = 'DELETED'
@@ -70,7 +82,9 @@ class FakeStackSet(BaseModel):
if accounts and regions:
self.update_instances(accounts, regions, self.parameters)
- operation = self._create_operation(operation_id=operation_id, action='UPDATE', status='SUCCEEDED')
+ operation = self._create_operation(operation_id=operation_id,
+ action='UPDATE', status='SUCCEEDED', accounts=accounts,
+ regions=regions)
return operation
def create_stack_instances(self, accounts, regions, parameters, operation_id=None):
@@ -80,7 +94,8 @@ class FakeStackSet(BaseModel):
parameters = self.parameters
self.instances.create_instances(accounts, regions, parameters, operation_id)
- self._create_operation(operation_id=operation_id, action='CREATE', status='SUCCEEDED')
+ self._create_operation(operation_id=operation_id, action='CREATE',
+ status='SUCCEEDED', accounts=accounts, regions=regions)
def delete_stack_instances(self, accounts, regions, operation_id=None):
if not operation_id:
@@ -88,14 +103,17 @@ class FakeStackSet(BaseModel):
self.instances.delete(accounts, regions)
- self._create_operation(operation_id=operation_id, action='DELETE', status='SUCCEEDED')
+ self._create_operation(operation_id=operation_id, action='DELETE',
+ status='SUCCEEDED', accounts=accounts, regions=regions)
def update_instances(self, accounts, regions, parameters, operation_id=None):
if not operation_id:
operation_id = uuid.uuid4()
self.instances.update(accounts, regions, parameters)
- operation = self._create_operation(operation_id=operation_id, action='UPDATE', status='SUCCEEDED')
+ operation = self._create_operation(operation_id=operation_id,
+ action='UPDATE', status='SUCCEEDED', accounts=accounts,
+ regions=regions)
return operation
diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py
index c85c8698..86eb3df0 100644
--- a/moto/cloudformation/responses.py
+++ b/moto/cloudformation/responses.py
@@ -412,6 +412,30 @@ class CloudFormationResponse(BaseResponse):
template = self.response_template(LIST_STACK_SET_OPERATIONS_RESPONSE_TEMPLATE)
return template.render(stackset=stackset)
+ def stop_stack_set_operation(self):
+ 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')
+ template = self.response_template(STOP_STACK_SET_OPERATION_RESPONSE_TEMPLATE)
+ return template.render()
+
+ def describe_stack_set_operation(self):
+ 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)
+ template = self.response_template(DESCRIBE_STACKSET_OPERATION_RESPONSE_TEMPLATE)
+ return template.render(stackset=stackset, operation=operation)
+
+ def list_stack_set_operation_results(self):
+ 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)
+ template = self.response_template(LIST_STACK_SET_OPERATION_RESULTS_RESPONSE_TEMPLATE)
+ return template.render(operation=operation)
+
def update_stack_set(self):
stackset_name = self._get_param('StackSetName')
operation_id = self._get_param('OperationId')
@@ -878,3 +902,56 @@ LIST_STACK_SET_OPERATIONS_RESPONSE_TEMPLATE = """
"""
+
+STOP_STACK_SET_OPERATION_RESPONSE_TEMPLATE = """
+
+
+ 2188554a-07c6-4396-b2c5-example
+
+"""
+
+DESCRIBE_STACKSET_OPERATION_RESPONSE_TEMPLATE = """
+
+
+ {{ stackset.execution_role }}
+ arn:aws:iam::123456789012:role/{{ stackset.admin_role }}
+ {{ stackset.id }}
+ {{ operation.CreationTimestamp }}
+ {{ operation.OperationId }}
+ {{ operation.Action }}
+
+
+
+ {{ operation.EndTimestamp }}
+ {{ operation.Status }}
+
+
+
+ 2edc27b6-9ce2-486a-a192-example
+
+
+"""
+
+LIST_STACK_SET_OPERATION_RESULTS_RESPONSE_TEMPLATE = """
+
+
+ {% for instance in operation.Instances %}
+ {% for account, region in instance.items() %}
+
+
+ Function not found: arn:aws:lambda:us-west-2:123456789012:function:AWSCloudFormationStackSetAccountGate
+ SKIPPED
+
+ {{ region }}
+ {{ account }}
+ {{ operation.Status }}
+
+ {% endfor %}
+ {% endfor %}
+
+
+
+ ac05a9ce-5f98-4197-a29b-example
+
+
+"""
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
index dd8d6cf6..eb7f6bcc 100644
--- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
@@ -240,6 +240,86 @@ def test_boto3_list_stacksets_contents():
stacksets['Summaries'][0].should.have.key('Status').which.should.equal('ACTIVE')
+@mock_cloudformation
+def test_boto3_stop_stack_set_operation():
+ cf_conn = boto3.client('cloudformation', region_name='us-east-1')
+ cf_conn.create_stack_set(
+ StackSetName="test_stack_set",
+ TemplateBody=dummy_template_json,
+ )
+ cf_conn.create_stack_instances(
+ StackSetName="test_stack_set",
+ Accounts=['123456789012'],
+ Regions=['us-east-1', 'us-west-1', 'us-west-2'],
+ )
+ operation_id = cf_conn.list_stack_set_operations(
+ StackSetName="test_stack_set")['Summaries'][-1]['OperationId']
+ cf_conn.stop_stack_set_operation(
+ StackSetName="test_stack_set",
+ OperationId=operation_id
+ )
+ list_operation = cf_conn.list_stack_set_operations(
+ StackSetName="test_stack_set"
+ )
+ list_operation['Summaries'][-1]['Status'].should.equal('STOPPED')
+
+
+@mock_cloudformation
+def test_boto3_describe_stack_set_operation():
+ cf_conn = boto3.client('cloudformation', region_name='us-east-1')
+ cf_conn.create_stack_set(
+ StackSetName="test_stack_set",
+ TemplateBody=dummy_template_json,
+ )
+ cf_conn.create_stack_instances(
+ StackSetName="test_stack_set",
+ Accounts=['123456789012'],
+ Regions=['us-east-1', 'us-west-1', 'us-west-2'],
+ )
+ operation_id = cf_conn.list_stack_set_operations(
+ StackSetName="test_stack_set")['Summaries'][-1]['OperationId']
+ cf_conn.stop_stack_set_operation(
+ StackSetName="test_stack_set",
+ OperationId=operation_id
+ )
+ response = cf_conn.describe_stack_set_operation(
+ StackSetName="test_stack_set",
+ OperationId=operation_id,
+ )
+
+ response['StackSetOperation']['Status'].should.equal('STOPPED')
+ response['StackSetOperation']['Action'].should.equal('CREATE')
+
+
+@mock_cloudformation
+def test_boto3_list_stack_set_operation_results():
+ cf_conn = boto3.client('cloudformation', region_name='us-east-1')
+ cf_conn.create_stack_set(
+ StackSetName="test_stack_set",
+ TemplateBody=dummy_template_json,
+ )
+ cf_conn.create_stack_instances(
+ StackSetName="test_stack_set",
+ Accounts=['123456789012'],
+ Regions=['us-east-1', 'us-west-1', 'us-west-2'],
+ )
+ operation_id = cf_conn.list_stack_set_operations(
+ StackSetName="test_stack_set")['Summaries'][-1]['OperationId']
+
+ cf_conn.stop_stack_set_operation(
+ StackSetName="test_stack_set",
+ OperationId=operation_id
+ )
+ response = cf_conn.list_stack_set_operation_results(
+ StackSetName="test_stack_set",
+ OperationId=operation_id,
+ )
+
+ response['Summaries'].should.have.length_of(3)
+ response['Summaries'][0].should.have.key('Account').which.should.equal('123456789012')
+ response['Summaries'][1].should.have.key('Status').which.should.equal('STOPPED')
+
+
@mock_cloudformation
def test_boto3_update_stack_instances():
cf_conn = boto3.client('cloudformation', region_name='us-east-1')