Merge pull request #1909 from markchalloner/cf-list-change-sets
Add CloudFormation methods describe_change_set and list_change_sets.
This commit is contained in:
		
						commit
						536d8c8457
					
				| @ -477,12 +477,12 @@ | ||||
| - [X] create_stack | ||||
| - [ ] create_stack_instances | ||||
| - [ ] create_stack_set | ||||
| - [ ] delete_change_set | ||||
| - [X] delete_change_set | ||||
| - [X] delete_stack | ||||
| - [ ] delete_stack_instances | ||||
| - [ ] delete_stack_set | ||||
| - [ ] describe_account_limits | ||||
| - [ ] describe_change_set | ||||
| - [X] describe_change_set | ||||
| - [ ] describe_stack_events | ||||
| - [ ] describe_stack_instance | ||||
| - [ ] describe_stack_resource | ||||
| @ -495,7 +495,7 @@ | ||||
| - [ ] get_stack_policy | ||||
| - [ ] get_template | ||||
| - [ ] get_template_summary | ||||
| - [ ] list_change_sets | ||||
| - [X] list_change_sets | ||||
| - [X] list_exports | ||||
| - [ ] list_imports | ||||
| - [ ] list_stack_instances | ||||
|  | ||||
| @ -127,6 +127,49 @@ class FakeStack(BaseModel): | ||||
|         self.status = "DELETE_COMPLETE" | ||||
| 
 | ||||
| 
 | ||||
| class FakeChange(BaseModel): | ||||
| 
 | ||||
|     def __init__(self, action, logical_resource_id, resource_type): | ||||
|         self.action = action | ||||
|         self.logical_resource_id = logical_resource_id | ||||
|         self.resource_type = resource_type | ||||
| 
 | ||||
| 
 | ||||
| class FakeChangeSet(FakeStack): | ||||
| 
 | ||||
|     def __init__(self, stack_id, stack_name, stack_template, change_set_id, change_set_name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None, cross_stack_resources=None): | ||||
|         super(FakeChangeSet, self).__init__( | ||||
|             stack_id, | ||||
|             stack_name, | ||||
|             stack_template, | ||||
|             parameters, | ||||
|             region_name, | ||||
|             notification_arns=notification_arns, | ||||
|             tags=tags, | ||||
|             role_arn=role_arn, | ||||
|             cross_stack_resources=cross_stack_resources, | ||||
|             create_change_set=True, | ||||
|         ) | ||||
|         self.stack_name = stack_name | ||||
|         self.change_set_id = change_set_id | ||||
|         self.change_set_name = change_set_name | ||||
|         self.changes = self.diff(template=template, parameters=parameters) | ||||
| 
 | ||||
|     def diff(self, template, parameters=None): | ||||
|         self.template = template | ||||
|         self._parse_template() | ||||
|         changes = [] | ||||
|         resources_by_action = self.resource_map.diff(self.template_dict, parameters) | ||||
|         for action, resources in resources_by_action.items(): | ||||
|             for resource_name, resource in resources.items(): | ||||
|                 changes.append(FakeChange( | ||||
|                     action=action, | ||||
|                     logical_resource_id=resource_name, | ||||
|                     resource_type=resource['ResourceType'], | ||||
|                 )) | ||||
|         return changes | ||||
| 
 | ||||
| 
 | ||||
| class FakeEvent(BaseModel): | ||||
| 
 | ||||
|     def __init__(self, stack_id, stack_name, logical_resource_id, physical_resource_id, resource_type, resource_status, resource_status_reason=None, resource_properties=None): | ||||
| @ -171,24 +214,62 @@ class CloudFormationBackend(BaseBackend): | ||||
|         return new_stack | ||||
| 
 | ||||
|     def create_change_set(self, stack_name, change_set_name, template, parameters, region_name, change_set_type, notification_arns=None, tags=None, role_arn=None): | ||||
|         stack_id = None | ||||
|         stack_template = None | ||||
|         if change_set_type == 'UPDATE': | ||||
|             stacks = self.stacks.values() | ||||
|             stack = None | ||||
|             for s in stacks: | ||||
|                 if s.name == stack_name: | ||||
|                     stack = s | ||||
|                     stack_id = stack.stack_id | ||||
|                     stack_template = stack.template | ||||
|             if stack is None: | ||||
|                 raise ValidationError(stack_name) | ||||
| 
 | ||||
|         else: | ||||
|             stack = self.create_stack(stack_name, template, parameters, | ||||
|                                       region_name, notification_arns, tags, | ||||
|                                       role_arn, create_change_set=True) | ||||
|             stack_id = generate_stack_id(stack_name) | ||||
|             stack_template = template | ||||
| 
 | ||||
|         change_set_id = generate_changeset_id(change_set_name, region_name) | ||||
|         self.stacks[change_set_name] = {'Id': change_set_id, | ||||
|                                         'StackId': stack.stack_id} | ||||
|         self.change_sets[change_set_id] = stack | ||||
|         return change_set_id, stack.stack_id | ||||
|         new_change_set = FakeChangeSet( | ||||
|             stack_id=stack_id, | ||||
|             stack_name=stack_name, | ||||
|             stack_template=stack_template, | ||||
|             change_set_id=change_set_id, | ||||
|             change_set_name=change_set_name, | ||||
|             template=template, | ||||
|             parameters=parameters, | ||||
|             region_name=region_name, | ||||
|             notification_arns=notification_arns, | ||||
|             tags=tags, | ||||
|             role_arn=role_arn, | ||||
|             cross_stack_resources=self.exports | ||||
|         ) | ||||
|         self.change_sets[change_set_id] = new_change_set | ||||
|         self.stacks[stack_id] = new_change_set | ||||
|         return change_set_id, stack_id | ||||
| 
 | ||||
|     def delete_change_set(self, change_set_name, stack_name=None): | ||||
|         if change_set_name in self.change_sets: | ||||
|             # This means arn was passed in | ||||
|             del self.change_sets[change_set_name] | ||||
|         else: | ||||
|             for cs in self.change_sets: | ||||
|                 if self.change_sets[cs].change_set_name == change_set_name: | ||||
|                     del self.change_sets[cs] | ||||
| 
 | ||||
|     def describe_change_set(self, change_set_name, stack_name=None): | ||||
|         change_set = None | ||||
|         if change_set_name in self.change_sets: | ||||
|             # This means arn was passed in | ||||
|             change_set = self.change_sets[change_set_name] | ||||
|         else: | ||||
|             for cs in self.change_sets: | ||||
|                 if self.change_sets[cs].change_set_name == change_set_name: | ||||
|                     change_set = self.change_sets[cs] | ||||
|         if change_set is None: | ||||
|             raise ValidationError(change_set_name) | ||||
|         return change_set | ||||
| 
 | ||||
|     def execute_change_set(self, change_set_name, stack_name=None): | ||||
|         stack = None | ||||
| @ -197,7 +278,7 @@ class CloudFormationBackend(BaseBackend): | ||||
|             stack = self.change_sets[change_set_name] | ||||
|         else: | ||||
|             for cs in self.change_sets: | ||||
|                 if self.change_sets[cs].name == change_set_name: | ||||
|                 if self.change_sets[cs].change_set_name == change_set_name: | ||||
|                     stack = self.change_sets[cs] | ||||
|         if stack is None: | ||||
|             raise ValidationError(stack_name) | ||||
| @ -223,6 +304,9 @@ class CloudFormationBackend(BaseBackend): | ||||
|         else: | ||||
|             return list(stacks) | ||||
| 
 | ||||
|     def list_change_sets(self): | ||||
|         return self.change_sets.values() | ||||
| 
 | ||||
|     def list_stacks(self): | ||||
|         return [ | ||||
|             v for v in self.stacks.values() | ||||
|  | ||||
| @ -465,36 +465,70 @@ class ResourceMap(collections.Mapping): | ||||
|                 ec2_models.ec2_backends[self._region_name].create_tags( | ||||
|                     [self[resource].physical_resource_id], self.tags) | ||||
| 
 | ||||
|     def update(self, template, parameters=None): | ||||
|     def diff(self, template, parameters=None): | ||||
|         if parameters: | ||||
|             self.input_parameters = parameters | ||||
|         self.load_mapping() | ||||
|         self.load_parameters() | ||||
|         self.load_conditions() | ||||
| 
 | ||||
|         old_template = self._resource_json_map | ||||
|         new_template = template['Resources'] | ||||
| 
 | ||||
|         resource_names_by_action = { | ||||
|             'Add': set(new_template) - set(old_template), | ||||
|             'Modify': set(name for name in new_template if name in old_template and new_template[ | ||||
|                 name] != old_template[name]), | ||||
|             'Remove': set(old_template) - set(new_template) | ||||
|         } | ||||
|         resources_by_action = { | ||||
|             'Add': {}, | ||||
|             'Modify': {}, | ||||
|             'Remove': {}, | ||||
|         } | ||||
| 
 | ||||
|         for resource_name in resource_names_by_action['Add']: | ||||
|             resources_by_action['Add'][resource_name] = { | ||||
|                 'LogicalResourceId': resource_name, | ||||
|                 'ResourceType': new_template[resource_name]['Type'] | ||||
|             } | ||||
| 
 | ||||
|         for resource_name in resource_names_by_action['Modify']: | ||||
|             resources_by_action['Modify'][resource_name] = { | ||||
|                 'LogicalResourceId': resource_name, | ||||
|                 'ResourceType': new_template[resource_name]['Type'] | ||||
|             } | ||||
| 
 | ||||
|         for resource_name in resource_names_by_action['Remove']: | ||||
|             resources_by_action['Remove'][resource_name] = { | ||||
|                 'LogicalResourceId': resource_name, | ||||
|                 'ResourceType': old_template[resource_name]['Type'] | ||||
|             } | ||||
| 
 | ||||
|         return resources_by_action | ||||
| 
 | ||||
|     def update(self, template, parameters=None): | ||||
|         resources_by_action = self.diff(template, parameters) | ||||
| 
 | ||||
|         old_template = self._resource_json_map | ||||
|         new_template = template['Resources'] | ||||
|         self._resource_json_map = new_template | ||||
| 
 | ||||
|         new_resource_names = set(new_template) - set(old_template) | ||||
|         for resource_name in new_resource_names: | ||||
|         for resource_name, resource in resources_by_action['Add'].items(): | ||||
|             resource_json = new_template[resource_name] | ||||
|             new_resource = parse_and_create_resource( | ||||
|                 resource_name, resource_json, self, self._region_name) | ||||
|             self._parsed_resources[resource_name] = new_resource | ||||
| 
 | ||||
|         removed_resource_names = set(old_template) - set(new_template) | ||||
|         for resource_name in removed_resource_names: | ||||
|         for resource_name, resource in resources_by_action['Remove'].items(): | ||||
|             resource_json = old_template[resource_name] | ||||
|             parse_and_delete_resource( | ||||
|                 resource_name, resource_json, self, self._region_name) | ||||
|             self._parsed_resources.pop(resource_name) | ||||
| 
 | ||||
|         resources_to_update = set(name for name in new_template if name in old_template and new_template[ | ||||
|                                   name] != old_template[name]) | ||||
|         tries = 1 | ||||
|         while resources_to_update and tries < 5: | ||||
|             for resource_name in resources_to_update.copy(): | ||||
|         while resources_by_action['Modify'] and tries < 5: | ||||
|             for resource_name, resource in resources_by_action['Modify'].copy().items(): | ||||
|                 resource_json = new_template[resource_name] | ||||
|                 try: | ||||
|                     changed_resource = parse_and_update_resource( | ||||
| @ -505,7 +539,7 @@ class ResourceMap(collections.Mapping): | ||||
|                     last_exception = e | ||||
|                 else: | ||||
|                     self._parsed_resources[resource_name] = changed_resource | ||||
|                     resources_to_update.remove(resource_name) | ||||
|                     del resources_by_action['Modify'][resource_name] | ||||
|             tries += 1 | ||||
|         if tries == 5: | ||||
|             raise last_exception | ||||
|  | ||||
| @ -120,6 +120,31 @@ class CloudFormationResponse(BaseResponse): | ||||
|             template = self.response_template(CREATE_CHANGE_SET_RESPONSE_TEMPLATE) | ||||
|             return template.render(stack_id=stack_id, change_set_id=change_set_id) | ||||
| 
 | ||||
|     def delete_change_set(self): | ||||
|         stack_name = self._get_param('StackName') | ||||
|         change_set_name = self._get_param('ChangeSetName') | ||||
| 
 | ||||
|         self.cloudformation_backend.delete_change_set(change_set_name=change_set_name, stack_name=stack_name) | ||||
|         if self.request_json: | ||||
|             return json.dumps({ | ||||
|                 'DeleteChangeSetResponse': { | ||||
|                     'DeleteChangeSetResult': {}, | ||||
|                 } | ||||
|             }) | ||||
|         else: | ||||
|             template = self.response_template(DELETE_CHANGE_SET_RESPONSE_TEMPLATE) | ||||
|             return template.render() | ||||
| 
 | ||||
|     def describe_change_set(self): | ||||
|         stack_name = self._get_param('StackName') | ||||
|         change_set_name = self._get_param('ChangeSetName') | ||||
|         change_set = self.cloudformation_backend.describe_change_set( | ||||
|             change_set_name=change_set_name, | ||||
|             stack_name=stack_name, | ||||
|         ) | ||||
|         template = self.response_template(DESCRIBE_CHANGE_SET_RESPONSE_TEMPLATE) | ||||
|         return template.render(change_set=change_set) | ||||
| 
 | ||||
|     @amzn_request_id | ||||
|     def execute_change_set(self): | ||||
|         stack_name = self._get_param('StackName') | ||||
| @ -187,6 +212,11 @@ class CloudFormationResponse(BaseResponse): | ||||
|         template = self.response_template(DESCRIBE_STACK_EVENTS_RESPONSE) | ||||
|         return template.render(stack=stack) | ||||
| 
 | ||||
|     def list_change_sets(self): | ||||
|         change_sets = self.cloudformation_backend.list_change_sets() | ||||
|         template = self.response_template(LIST_CHANGE_SETS_RESPONSE) | ||||
|         return template.render(change_sets=change_sets) | ||||
| 
 | ||||
|     def list_stacks(self): | ||||
|         stacks = self.cloudformation_backend.list_stacks() | ||||
|         template = self.response_template(LIST_STACKS_RESPONSE) | ||||
| @ -354,6 +384,66 @@ CREATE_CHANGE_SET_RESPONSE_TEMPLATE = """<CreateStackResponse> | ||||
| </CreateStackResponse> | ||||
| """ | ||||
| 
 | ||||
| DELETE_CHANGE_SET_RESPONSE_TEMPLATE = """<DeleteChangeSetResponse> | ||||
|   <DeleteChangeSetResult> | ||||
|   </DeleteChangeSetResult> | ||||
|   <ResponseMetadata> | ||||
|     <RequestId>3d3200a1-810e-3023-6cc3-example</RequestId> | ||||
|   </ResponseMetadata> | ||||
| </DeleteChangeSetResponse> | ||||
| """ | ||||
| 
 | ||||
| DESCRIBE_CHANGE_SET_RESPONSE_TEMPLATE = """<DescribeChangeSetResponse> | ||||
|   <DescribeChangeSetResult> | ||||
|     <ChangeSetId>{{ change_set.change_set_id }}</ChangeSetId> | ||||
|     <ChangeSetName>{{ change_set.change_set_name }}</ChangeSetName> | ||||
|     <StackId>{{ change_set.stack_id }}</StackId> | ||||
|     <StackName>{{ change_set.stack_name }}</StackName> | ||||
|     <Description>{{ change_set.description }}</Description> | ||||
|     <Parameters> | ||||
|       {% for param_name, param_value in change_set.stack_parameters.items() %} | ||||
|        <member> | ||||
|           <ParameterKey>{{ param_name }}</ParameterKey> | ||||
|           <ParameterValue>{{ param_value }}</ParameterValue> | ||||
|         </member> | ||||
|       {% endfor %} | ||||
|     </Parameters> | ||||
|     <CreationTime>2011-05-23T15:47:44Z</CreationTime> | ||||
|     <ExecutionStatus>{{ change_set.execution_status }}</ExecutionStatus> | ||||
|     <Status>{{ change_set.status }}</Status> | ||||
|     <StatusReason>{{ change_set.status_reason }}</StatusReason> | ||||
|     {% if change_set.notification_arns %} | ||||
|     <NotificationARNs> | ||||
|       {% for notification_arn in change_set.notification_arns %} | ||||
|       <member>{{ notification_arn }}</member> | ||||
|       {% endfor %} | ||||
|     </NotificationARNs> | ||||
|     {% else %} | ||||
|     <NotificationARNs/> | ||||
|     {% endif %} | ||||
|     {% if change_set.role_arn %} | ||||
|     <RoleARN>{{ change_set.role_arn }}</RoleARN> | ||||
|     {% endif %} | ||||
|     {% if change_set.changes %} | ||||
|     <Changes> | ||||
|       {% for change in change_set.changes %} | ||||
|       <member> | ||||
|         <Type>Resource</Type> | ||||
|         <ResourceChange> | ||||
|             <Action>{{ change.action }}</Action> | ||||
|             <LogicalResourceId>{{ change.logical_resource_id }}</LogicalResourceId> | ||||
|             <ResourceType>{{ change.resource_type }}</ResourceType> | ||||
|         </ResourceChange> | ||||
|       </member> | ||||
|       {% endfor %} | ||||
|     </Changes> | ||||
|     {% endif %} | ||||
|     {% if next_token %} | ||||
|     <NextToken>{{ next_token }}</NextToken> | ||||
|     {% endif %} | ||||
|   </DescribeChangeSetResult> | ||||
| </DescribeChangeSetResponse>""" | ||||
| 
 | ||||
| EXECUTE_CHANGE_SET_RESPONSE_TEMPLATE = """<ExecuteChangeSetResponse> | ||||
|   <ExecuteChangeSetResult> | ||||
|       <ExecuteChangeSetResult/> | ||||
| @ -479,6 +569,27 @@ DESCRIBE_STACK_EVENTS_RESPONSE = """<DescribeStackEventsResponse xmlns="http://c | ||||
| </DescribeStackEventsResponse>""" | ||||
| 
 | ||||
| 
 | ||||
| LIST_CHANGE_SETS_RESPONSE = """<ListChangeSetsResponse> | ||||
|  <ListChangeSetsResult> | ||||
|   <Summaries> | ||||
|     {% for change_set in change_sets %} | ||||
|     <member> | ||||
|         <StackId>{{ change_set.stack_id }}</StackId> | ||||
|         <StackName>{{ change_set.stack_name }}</StackName> | ||||
|         <ChangeSetId>{{ change_set.change_set_id }}</ChangeSetId> | ||||
|         <ChangeSetName>{{ change_set.change_set_name }}</ChangeSetName> | ||||
|         <ExecutionStatus>{{ change_set.execution_status }}</ExecutionStatus> | ||||
|         <Status>{{ change_set.status }}</Status> | ||||
|         <StatusReason>{{ change_set.status_reason }}</StatusReason> | ||||
|         <CreationTime>2011-05-23T15:47:44Z</CreationTime> | ||||
|         <Description>{{ change_set.description }}</Description> | ||||
|     </member> | ||||
|     {% endfor %} | ||||
|   </Summaries> | ||||
|  </ListChangeSetsResult> | ||||
| </ListChangeSetsResponse>""" | ||||
| 
 | ||||
| 
 | ||||
| LIST_STACKS_RESPONSE = """<ListStacksResponse> | ||||
|  <ListStacksResult> | ||||
|   <StackSummaries> | ||||
|  | ||||
| @ -399,6 +399,32 @@ def test_create_change_set_from_s3_url(): | ||||
|     assert 'arn:aws:cloudformation:us-east-1:123456789:stack/NewStack' in response['StackId'] | ||||
| 
 | ||||
| 
 | ||||
| @mock_cloudformation | ||||
| def test_describe_change_set(): | ||||
|     cf_conn = boto3.client('cloudformation', region_name='us-east-1') | ||||
|     cf_conn.create_change_set( | ||||
|         StackName='NewStack', | ||||
|         TemplateBody=dummy_template_json, | ||||
|         ChangeSetName='NewChangeSet', | ||||
|         ChangeSetType='CREATE', | ||||
|     ) | ||||
| 
 | ||||
|     stack = cf_conn.describe_change_set(ChangeSetName="NewChangeSet") | ||||
|     stack['ChangeSetName'].should.equal('NewChangeSet') | ||||
|     stack['StackName'].should.equal('NewStack') | ||||
| 
 | ||||
|     cf_conn.create_change_set( | ||||
|         StackName='NewStack', | ||||
|         TemplateBody=dummy_update_template_json, | ||||
|         ChangeSetName='NewChangeSet2', | ||||
|         ChangeSetType='UPDATE', | ||||
|     ) | ||||
|     stack = cf_conn.describe_change_set(ChangeSetName="NewChangeSet2") | ||||
|     stack['ChangeSetName'].should.equal('NewChangeSet2') | ||||
|     stack['StackName'].should.equal('NewStack') | ||||
|     stack['Changes'].should.have.length_of(2) | ||||
| 
 | ||||
| 
 | ||||
| @mock_cloudformation | ||||
| def test_execute_change_set_w_arn(): | ||||
|     cf_conn = boto3.client('cloudformation', region_name='us-east-1') | ||||
| @ -420,7 +446,7 @@ def test_execute_change_set_w_name(): | ||||
|         ChangeSetName='NewChangeSet', | ||||
|         ChangeSetType='CREATE', | ||||
|     ) | ||||
|     cf_conn.execute_change_set(ChangeSetName='NewStack', StackName='NewStack') | ||||
|     cf_conn.execute_change_set(ChangeSetName='NewChangeSet', StackName='NewStack') | ||||
| 
 | ||||
| 
 | ||||
| @mock_cloudformation | ||||
| @ -489,6 +515,20 @@ def test_describe_stack_by_stack_id(): | ||||
|     stack_by_id['StackName'].should.equal("test_stack") | ||||
| 
 | ||||
| 
 | ||||
| @mock_cloudformation | ||||
| def test_list_change_sets(): | ||||
|     cf_conn = boto3.client('cloudformation', region_name='us-east-1') | ||||
|     cf_conn.create_change_set( | ||||
|         StackName='NewStack2', | ||||
|         TemplateBody=dummy_template_json, | ||||
|         ChangeSetName='NewChangeSet2', | ||||
|         ChangeSetType='CREATE', | ||||
|     ) | ||||
|     change_set = cf_conn.list_change_sets(StackName='NewStack2')['Summaries'][0] | ||||
|     change_set['StackName'].should.equal('NewStack2') | ||||
|     change_set['ChangeSetName'].should.equal('NewChangeSet2') | ||||
| 
 | ||||
| 
 | ||||
| @mock_cloudformation | ||||
| def test_list_stacks(): | ||||
|     cf = boto3.resource('cloudformation', region_name='us-east-1') | ||||
| @ -521,6 +561,22 @@ def test_delete_stack_from_resource(): | ||||
|     list(cf.stacks.all()).should.have.length_of(0) | ||||
| 
 | ||||
| 
 | ||||
| @mock_cloudformation | ||||
| @mock_ec2 | ||||
| def test_delete_change_set(): | ||||
|     cf_conn = boto3.client('cloudformation', region_name='us-east-1') | ||||
|     cf_conn.create_change_set( | ||||
|         StackName='NewStack', | ||||
|         TemplateBody=dummy_template_json, | ||||
|         ChangeSetName='NewChangeSet', | ||||
|         ChangeSetType='CREATE', | ||||
|     ) | ||||
| 
 | ||||
|     cf_conn.list_change_sets(StackName='NewStack')['Summaries'].should.have.length_of(1) | ||||
|     cf_conn.delete_change_set(ChangeSetName='NewChangeSet', StackName='NewStack') | ||||
|     cf_conn.list_change_sets(StackName='NewStack')['Summaries'].should.have.length_of(0) | ||||
| 
 | ||||
| 
 | ||||
| @mock_cloudformation | ||||
| @mock_ec2 | ||||
| def test_delete_stack_by_name(): | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user