From 6f4cb512ac39daa659e1e56f0b5243ea878b8566 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Fri, 17 Mar 2017 23:57:57 +0000 Subject: [PATCH] Allow CloudFormation stack tags to be updated Limitations: * does not update the tags of the resources in the stack. that can be implemented later. * does not support the supposed feature of clearing tags by passing an empty value that boto3 mentions in its documentation. I could not find anything in the request body to indicate when an empty value was passed. --- moto/cloudformation/models.py | 10 ++++++--- moto/cloudformation/responses.py | 11 +++++++++- .../test_cloudformation_stack_crud.py | 22 ++++++++++++++++++- .../test_cloudformation_stack_crud_boto3.py | 6 ++++- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index df9f4a139..b58d1dcf0 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -82,7 +82,7 @@ class FakeStack(BaseModel): def stack_outputs(self): return self.output_map.values() - def update(self, template, role_arn=None, parameters=None): + def update(self, template, role_arn=None, parameters=None, tags=None): self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated") self.template = template self.resource_map.update(json.loads(template), parameters) @@ -90,6 +90,10 @@ class FakeStack(BaseModel): self._add_stack_event("UPDATE_COMPLETE") self.status = "UPDATE_COMPLETE" self.role_arn = role_arn + # only overwrite tags if passed + if tags is not None: + self.tags = tags + # TODO: update tags in the resource map def delete(self): self._add_stack_event("DELETE_IN_PROGRESS", @@ -164,9 +168,9 @@ class CloudFormationBackend(BaseBackend): if stack.name == name_or_stack_id: return stack - def update_stack(self, name, template, role_arn=None, parameters=None): + def update_stack(self, name, template, role_arn=None, parameters=None, tags=None): stack = self.get_stack(name) - stack.update(template, role_arn, parameters=parameters) + stack.update(template, role_arn, parameters=parameters, tags=tags) return stack def list_stack_resources(self, stack_name_or_id): diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index 64923c7e6..f1e6d0415 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -152,6 +152,14 @@ class CloudFormationResponse(BaseResponse): for parameter in self._get_list_prefix("Parameters.member") ]) + # boto3 is supposed to let you clear the tags by passing an empty value, but the request body doesn't + # end up containing anything we can use to differentiate between passing an empty value versus not + # passing anything. so until that changes, moto won't be able to clear tags, only update them. + tags = dict((item['key'], item['value']) + for item in self._get_list_prefix("Tags.member")) + # so that if we don't pass the parameter, we don't clear all the tags accidentally + if not tags: + tags = None stack = self.cloudformation_backend.get_stack(stack_name) if stack.status == 'ROLLBACK_COMPLETE': @@ -162,7 +170,8 @@ class CloudFormationResponse(BaseResponse): name=stack_name, template=stack_body, role_arn=role_arn, - parameters=parameters + parameters=parameters, + tags=tags, ) if self.request_json: stack_body = { diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index 4085e6d8f..eb3798f82 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -424,7 +424,7 @@ def test_update_stack(): @mock_cloudformation_deprecated -def test_update_stack(): +def test_update_stack_with_previous_template(): conn = boto.connect_cloudformation() conn.create_stack( "test_stack", @@ -482,6 +482,26 @@ def test_update_stack_with_parameters(): assert stack.parameters[0].value == "192.168.0.1/16" +@mock_cloudformation_deprecated +def test_update_stack_replace_tags(): + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=dummy_template_json, + tags={"foo": "bar"}, + ) + conn.update_stack( + "test_stack", + template_body=dummy_template_json, + tags={"foo": "baz"}, + ) + + stack = conn.describe_stacks()[0] + stack.stack_status.should.equal("UPDATE_COMPLETE") + # since there is one tag it doesn't come out as a list + dict(stack.tags).should.equal({"foo": "baz"}) + + @mock_cloudformation_deprecated def test_update_stack_when_rolled_back(): conn = boto.connect_cloudformation() diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index c69766209..9a531010f 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -258,12 +258,15 @@ def test_describe_updated_stack(): cf_conn.create_stack( StackName="test_stack", TemplateBody=dummy_template_json, + Tags=[{'Key': 'foo', 'Value': 'bar'}], ) cf_conn.update_stack( StackName="test_stack", RoleARN='arn:aws:iam::123456789012:role/moto', - TemplateBody=dummy_update_template_json) + TemplateBody=dummy_update_template_json, + Tags=[{'Key': 'foo', 'Value': 'baz'}], + ) stack = cf_conn.describe_stacks(StackName="test_stack")['Stacks'][0] stack_id = stack['StackId'] @@ -272,6 +275,7 @@ def test_describe_updated_stack(): stack_by_id['StackName'].should.equal("test_stack") stack_by_id['StackStatus'].should.equal("UPDATE_COMPLETE") stack_by_id['RoleARN'].should.equal('arn:aws:iam::123456789012:role/moto') + stack_by_id['Tags'].should.equal([{'Key': 'foo', 'Value': 'baz'}]) @mock_cloudformation