From 5fb43ee7b6630518cfa14b96a06510b46ebc1c9e Mon Sep 17 00:00:00 2001 From: John Corrales Date: Mon, 14 Jan 2019 22:01:53 -0800 Subject: [PATCH] Operations (#4) Added stop, list operation results, and describe operation --- IMPLEMENTATION_COVERAGE.md | 8 +- moto/cloudformation/models.py | 38 ++++++--- moto/cloudformation/responses.py | 77 ++++++++++++++++++ .../test_cloudformation_stack_crud_boto3.py | 80 +++++++++++++++++++ 4 files changed, 189 insertions(+), 14 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 61bfaf197..98e426e3c 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 ed2ac1a11..bbb1a0fd2 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 c85c86989..86eb3df0a 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 dd8d6cf6c..eb7f6bcc5 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')