From 0be036124510ad7f7cfb27ec223d01847bc54c42 Mon Sep 17 00:00:00 2001 From: Alec Posney Date: Mon, 22 May 2017 14:59:19 +1000 Subject: [PATCH 1/5] Added Cfn stack tag support to the deploy command --- .../customizations/cloudformation/deploy.py | 52 +++++++++++++++---- .../customizations/cloudformation/deployer.py | 8 +-- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/awscli/customizations/cloudformation/deploy.py b/awscli/customizations/cloudformation/deploy.py index 4ebef072df56..83ceb411525d 100644 --- a/awscli/customizations/cloudformation/deploy.py +++ b/awscli/customizations/cloudformation/deploy.py @@ -216,6 +216,35 @@ class DeployCommand(BasicCommand): 'changes to be made to the stack.' ) } + 'name': 'tags', + 'action': 'store', + 'required': False, + 'default': [], + 'schema': { + "type": "array", + "items": { + "type": "object", + "properties": { + "Key": { + "description": "The tag key.", + "type": "string", + "required": True + }, + "Value": { + "description": "The tag value.", + "type": "string", + "required": True + } + } + } + }, + 'help_text': ( + 'Key-value pairs to associate with the stack,' + ' that is created or updated by the changeset.' + ' AWS CloudFormation also propagates these tags' + ' to resources in the stack.' + ) + }, ] def _run_main(self, parsed_args, parsed_globals): @@ -267,22 +296,25 @@ def _run_main(self, parsed_args, parsed_globals): return self.deploy(deployer, stack_name, template_str, parameters, parsed_args.capabilities, parsed_args.execute_changeset, parsed_args.role_arn, - parsed_args.notification_arns, - s3_uploader, + parsed_args.notification_arns, s3_uploader, + parsed_args.tags, parsed_args.fail_on_empty_changeset) def deploy(self, deployer, stack_name, template_str, parameters, capabilities, execute_changeset, role_arn, - notification_arns, s3_uploader, fail_on_empty_changeset=True): + notification_arns, s3_uploader, tags + fail_on_empty_changeset=True): try: result = deployer.create_and_wait_for_changeset( - stack_name=stack_name, - cfn_template=template_str, - parameter_values=parameters, - capabilities=capabilities, - role_arn=role_arn, - notification_arns=notification_arns, - s3_uploader=s3_uploader) + stack_name=stack_name, + cfn_template=template_str, + parameter_values=parameters, + capabilities=capabilities, + role_arn=role_arn, + notification_arns=notification_arns, + s3_uploader=s3_uploader, + tags=tags + ) except exceptions.ChangeEmptyError as ex: if fail_on_empty_changeset: raise diff --git a/awscli/customizations/cloudformation/deployer.py b/awscli/customizations/cloudformation/deployer.py index 76e41a5fe30a..44b84e8cfc32 100644 --- a/awscli/customizations/cloudformation/deployer.py +++ b/awscli/customizations/cloudformation/deployer.py @@ -73,7 +73,7 @@ def has_stack(self, stack_name): def create_changeset(self, stack_name, cfn_template, parameter_values, capabilities, role_arn, - notification_arns, s3_uploader): + notification_arns, s3_uploader, tags): """ Call Cloudformation to create a changeset and wait for it to complete @@ -81,6 +81,7 @@ def create_changeset(self, stack_name, cfn_template, :param cfn_template: CloudFormation template string :param parameter_values: Template parameters object :param capabilities: Array of capabilities passed to CloudFormation + :param tags: Array of tags passed to CloudFormation :return: """ @@ -208,12 +209,11 @@ def wait_for_execute(self, stack_name, changeset_type): def create_and_wait_for_changeset(self, stack_name, cfn_template, parameter_values, capabilities, role_arn, - notification_arns, s3_uploader): + notification_arns, s3_uploader, tags): result = self.create_changeset( stack_name, cfn_template, parameter_values, capabilities, - role_arn, notification_arns, s3_uploader) - + role_arn, notification_arns, s3_uploader, tags) self.wait_for_changeset(result.changeset_id, stack_name) return result From 914e2e386bbb3150f852a60cfeb49eef01024a37 Mon Sep 17 00:00:00 2001 From: Alec Posney Date: Mon, 22 May 2017 14:59:59 +1000 Subject: [PATCH 2/5] Updated example command to include new tag argument --- awscli/examples/cloudformation/deploy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awscli/examples/cloudformation/deploy.rst b/awscli/examples/cloudformation/deploy.rst index 3cd0f6165f9b..8bb83db45167 100644 --- a/awscli/examples/cloudformation/deploy.rst +++ b/awscli/examples/cloudformation/deploy.rst @@ -2,5 +2,5 @@ Following command deploys template named ``template.json`` to a stack named ``my-new-stack``:: - aws cloudformation deploy --template-file /path_to_template/template.json --stack-name my-new-stack --parameter-overrides Key1=Value1 Key2=Value2 + aws cloudformation deploy --template-file /path_to_template/template.json --stack-name my-new-stack --parameter-overrides Key1=Value1 Key2=Value2 --tags Key=key1,Value=value1,Key=key2,Value=Value2 From f9de9eed4e0090a3b28fae864541c6da9a731434 Mon Sep 17 00:00:00 2001 From: Alec Posney Date: Mon, 22 May 2017 15:22:38 +1000 Subject: [PATCH 3/5] updated tests to handle new parameter --- .../customizations/cloudformation/deploy.py | 5 +-- .../cloudformation/test_deploy.py | 34 +++++++++++++------ .../cloudformation/test_deployer.py | 18 ++++++---- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/awscli/customizations/cloudformation/deploy.py b/awscli/customizations/cloudformation/deploy.py index 83ceb411525d..aa71c2f512a9 100644 --- a/awscli/customizations/cloudformation/deploy.py +++ b/awscli/customizations/cloudformation/deploy.py @@ -215,7 +215,8 @@ class DeployCommand(BasicCommand): 'Causes the CLI to return an exit code of 0 if there are no ' 'changes to be made to the stack.' ) - } + }, + { 'name': 'tags', 'action': 'store', 'required': False, @@ -302,7 +303,7 @@ def _run_main(self, parsed_args, parsed_globals): def deploy(self, deployer, stack_name, template_str, parameters, capabilities, execute_changeset, role_arn, - notification_arns, s3_uploader, tags + notification_arns, s3_uploader, tags, fail_on_empty_changeset=True): try: result = deployer.create_and_wait_for_changeset( diff --git a/tests/unit/customizations/cloudformation/test_deploy.py b/tests/unit/customizations/cloudformation/test_deploy.py index aaf4ea6087dd..8c0f78d38966 100644 --- a/tests/unit/customizations/cloudformation/test_deploy.py +++ b/tests/unit/customizations/cloudformation/test_deploy.py @@ -61,7 +61,8 @@ def setUp(self): s3_bucket=None, s3_prefix="some prefix", kms_key_id="some kms key id", - force_upload=True) + force_upload=True, + tags=[{"Key": "key1", "Value": "val1"}]) self.parsed_globals = FakeArgs(region="us-east-1", endpoint_url=None, verify_ssl=None) self.deploy_command = DeployCommand(self.session) @@ -114,9 +115,12 @@ def test_command_invoked(self, mock_yaml_parse): None, not self.parsed_args.no_execute_changeset, None, - [], + [], None, + mock.Any, True) + [], + mock.ANY) self.deploy_command.parse_parameter_arg.assert_called_once_with( self.parsed_args.parameter_overrides) @@ -158,7 +162,7 @@ def test_s3_upload_required_but_missing_bucket(self, mock_getsize, mock_yaml_par @patch('awscli.customizations.cloudformation.deploy.os.path.getsize') @patch('awscli.customizations.cloudformation.deploy.DeployCommand.deploy') @patch('awscli.customizations.cloudformation.deploy.S3Uploader') - def test_s3_uploader_is_configured_properly(self, s3UploaderMock, + def test_s3_uploader_is_configured_properly(self, s3UploaderMock, deploy_method_mock, mock_getsize, mock_yaml_parse, mock_isfile): """ Tests that large templates are detected prior to deployment @@ -190,12 +194,12 @@ def test_s3_uploader_is_configured_properly(self, s3UploaderMock, None, not self.parsed_args.no_execute_changeset, None, - [], + [], s3UploaderObject, True) - s3UploaderMock.assert_called_once_with(mock.ANY, - bucket_name, + s3UploaderMock.assert_called_once_with(mock.ANY, + bucket_name, mock.ANY, self.parsed_args.s3_prefix, self.parsed_args.kms_key_id, @@ -216,6 +220,7 @@ def test_deploy_success(self): role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None + tags = [{"Key":"key1", "Value": "val1"}] # Set the mock to return this fake changeset_id self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, changeset_type) @@ -228,7 +233,8 @@ def test_deploy_success(self): execute_changeset, role_arn, notification_arns, - s3_uploader) + s3_uploader, + tags) self.assertEqual(rc, 0) @@ -238,7 +244,8 @@ def test_deploy_success(self): capabilities=capabilities, role_arn=role_arn, notification_arns=notification_arns, - s3_uploader=s3_uploader) + s3_uploader=s3_uploader, + tags=tags) # since execute_changeset is set to True, deploy() will execute changeset self.deployer.execute_changeset.assert_called_once_with(changeset_id, stack_name) @@ -255,6 +262,7 @@ def test_deploy_no_execute(self): role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None + tags = [{"Key":"key1", "Value": "val1"}] self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, "CREATE") @@ -266,7 +274,8 @@ def test_deploy_no_execute(self): execute_changeset, role_arn, notification_arns, - s3_uploader) + s3_uploader, + tags) self.assertEqual(rc, 0) self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name, @@ -275,7 +284,8 @@ def test_deploy_no_execute(self): capabilities=capabilities, role_arn=role_arn, notification_arns=notification_arns, - s3_uploader=s3_uploader) + s3_uploader=s3_uploader, + tags=tags) # since execute_changeset is set to True, deploy() will execute changeset self.deployer.execute_changeset.assert_not_called() @@ -291,6 +301,7 @@ def test_deploy_raise_exception(self): role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None + tags = [{"Key":"key1", "Value": "val1"}] self.deployer.wait_for_execute.side_effect = RuntimeError("Some error") with self.assertRaises(RuntimeError): @@ -302,7 +313,8 @@ def test_deploy_raise_exception(self): execute_changeset, role_arn, notification_arns, - s3_uploader) + s3_uploader, + tags) def test_deploy_raises_exception_on_empty_changeset(self): stack_name = "stack_name" diff --git a/tests/unit/customizations/cloudformation/test_deployer.py b/tests/unit/customizations/cloudformation/test_deployer.py index 93087680e407..3d90da055cdd 100644 --- a/tests/unit/customizations/cloudformation/test_deployer.py +++ b/tests/unit/customizations/cloudformation/test_deployer.py @@ -105,6 +105,8 @@ def test_create_changeset_success(self): notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None + tags = [{"Key":"key1", "Value": "val1"}] + # Case 1: Stack DOES NOT exist self.deployer.has_stack = Mock() self.deployer.has_stack.return_value = False @@ -118,7 +120,8 @@ def test_create_changeset_success(self): "Capabilities": capabilities, "Description": botocore.stub.ANY, "RoleARN": role_arn, - "NotificationARNs": notification_arns + "NotificationARNs": notification_arns, + "Tags": tags } response = { @@ -130,7 +133,7 @@ def test_create_changeset_success(self): with self.stub_client: result = self.deployer.create_changeset( stack_name, template, parameters, capabilities, role_arn, - notification_arns, s3_uploader) + notification_arns, s3_uploader, tags) self.assertEquals(response["Id"], result.changeset_id) self.assertEquals("CREATE", result.changeset_type) @@ -143,7 +146,7 @@ def test_create_changeset_success(self): with self.stub_client: result = self.deployer.create_changeset( stack_name, template, parameters, capabilities, role_arn, - notification_arns, s3_uploader) + notification_arns, s3_uploader, tags) self.assertEquals(response["Id"], result.changeset_id) self.assertEquals("UPDATE", result.changeset_type) @@ -225,6 +228,7 @@ def test_create_changeset_exception(self): role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None + tags = [{"Key":"key1", "Value": "val1"}] self.deployer.has_stack = Mock() self.deployer.has_stack.return_value = False @@ -234,7 +238,7 @@ def test_create_changeset_exception(self): with self.stub_client: with self.assertRaises(botocore.exceptions.ClientError): self.deployer.create_changeset(stack_name, template, parameters, - capabilities, role_arn, notification_arns, None) + capabilities, role_arn, notification_arns, None, tags) def test_execute_changeset(self): stack_name = "stack_name" @@ -270,6 +274,7 @@ def test_create_and_wait_for_changeset_successful(self): role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None + tags = [{"Key":"key1", "Value": "val1"}] self.deployer.create_changeset = Mock() self.deployer.create_changeset.return_value = ChangeSetResult(changeset_id, changeset_type) @@ -278,7 +283,7 @@ def test_create_and_wait_for_changeset_successful(self): result = self.deployer.create_and_wait_for_changeset( stack_name, template, parameters, capabilities, role_arn, - notification_arns, s3_uploader) + notification_arns, s3_uploader, tags) self.assertEquals(result.changeset_id, changeset_id) self.assertEquals(result.changeset_type, changeset_type) @@ -293,6 +298,7 @@ def test_create_and_wait_for_changeset_error_waiting_for_changeset(self): role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None + tags = [{"Key":"key1", "Value": "val1"}] self.deployer.create_changeset = Mock() self.deployer.create_changeset.return_value = ChangeSetResult(changeset_id, changeset_type) @@ -303,7 +309,7 @@ def test_create_and_wait_for_changeset_error_waiting_for_changeset(self): with self.assertRaises(RuntimeError): result = self.deployer.create_and_wait_for_changeset( stack_name, template, parameters, capabilities, role_arn, - notification_arns, s3_uploader) + notification_arns, s3_uploader, tags) def test_wait_for_changeset_no_changes(self): stack_name = "stack_name" From 029e5be78f8c06319667f1de83141faa04035841 Mon Sep 17 00:00:00 2001 From: Alec Posney Date: Fri, 27 Oct 2017 12:17:01 +1100 Subject: [PATCH 4/5] fix error where tags are not being passed to create_change_set --- awscli/customizations/cloudformation/deployer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awscli/customizations/cloudformation/deployer.py b/awscli/customizations/cloudformation/deployer.py index 44b84e8cfc32..6569a66a7b9e 100644 --- a/awscli/customizations/cloudformation/deployer.py +++ b/awscli/customizations/cloudformation/deployer.py @@ -108,6 +108,7 @@ def create_changeset(self, stack_name, cfn_template, 'Parameters': parameter_values, 'Capabilities': capabilities, 'Description': description, + 'Tags': tags, } # If an S3 uploader is available, use TemplateURL to deploy rather than From 3bd9e3d1eadb259bdc3a246d2978f6d0e5b13f47 Mon Sep 17 00:00:00 2001 From: stealthycoin Date: Tue, 23 Jan 2018 09:39:46 -0800 Subject: [PATCH 5/5] Fix tests --- .../cloudformation/test_deploy.py | 53 ++++++++++--------- .../cloudformation/test_deployer.py | 7 +-- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/tests/unit/customizations/cloudformation/test_deploy.py b/tests/unit/customizations/cloudformation/test_deploy.py index 8c0f78d38966..4a4693200c8f 100644 --- a/tests/unit/customizations/cloudformation/test_deploy.py +++ b/tests/unit/customizations/cloudformation/test_deploy.py @@ -108,20 +108,18 @@ def test_command_invoked(self, mock_yaml_parse): open_mock.assert_called_once_with(file_path, "r") self.deploy_command.deploy.assert_called_once_with( - mock.ANY, - self.parsed_args.stack_name, - mock.ANY, - fake_parameters, - None, - not self.parsed_args.no_execute_changeset, - None, - [], - None, - mock.Any, - True) - [], - mock.ANY) - + mock.ANY, + 'some_stack_name', + mock.ANY, + fake_parameters, + None, + not self.parsed_args.no_execute_changeset, + None, + [], + None, + mock.ANY, + True + ) self.deploy_command.parse_parameter_arg.assert_called_once_with( self.parsed_args.parameter_overrides) @@ -187,16 +185,18 @@ def test_s3_uploader_is_configured_properly(self, s3UploaderMock, parsed_globals=self.parsed_globals) self.deploy_command.deploy.assert_called_once_with( - mock.ANY, - self.parsed_args.stack_name, - mock.ANY, - mock.ANY, - None, - not self.parsed_args.no_execute_changeset, - None, - [], - s3UploaderObject, - True) + mock.ANY, + self.parsed_args.stack_name, + mock.ANY, + mock.ANY, + None, + not self.parsed_args.no_execute_changeset, + None, + [], + s3UploaderObject, + [{'Key': 'key1', 'Value': 'val1'}], + True + ) s3UploaderMock.assert_called_once_with(mock.ANY, bucket_name, @@ -324,6 +324,7 @@ def test_deploy_raises_exception_on_empty_changeset(self): execute_changeset = True role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] + tags = [] empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name) changeset_func = self.deployer.create_and_wait_for_changeset @@ -332,7 +333,7 @@ def test_deploy_raises_exception_on_empty_changeset(self): self.deploy_command.deploy( self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, - s3_uploader=None) + None, tags) def test_deploy_does_not_raise_exception_on_empty_changeset(self): stack_name = "stack_name" @@ -349,7 +350,7 @@ def test_deploy_does_not_raise_exception_on_empty_changeset(self): self.deploy_command.deploy( self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, - s3_uploader=None, + s3_uploader=None, tags=[], fail_on_empty_changeset=False ) diff --git a/tests/unit/customizations/cloudformation/test_deployer.py b/tests/unit/customizations/cloudformation/test_deployer.py index 3d90da055cdd..73d2f8ce37f6 100644 --- a/tests/unit/customizations/cloudformation/test_deployer.py +++ b/tests/unit/customizations/cloudformation/test_deployer.py @@ -190,6 +190,7 @@ def upload_with_dedup(filename,extension): "Capabilities": capabilities, "Description": botocore.stub.ANY, "RoleARN": role_arn, + "Tags": [], "NotificationARNs": notification_arns } @@ -201,8 +202,8 @@ def upload_with_dedup(filename,extension): expected_params) with self.stub_client: result = self.deployer.create_changeset( - stack_name, template, parameters, capabilities, role_arn, - notification_arns, s3_uploader) + stack_name, template, parameters, capabilities, role_arn, + notification_arns, s3_uploader, []) self.assertEquals(response["Id"], result.changeset_id) self.assertEquals("CREATE", result.changeset_type) @@ -215,7 +216,7 @@ def upload_with_dedup(filename,extension): with self.stub_client: result = self.deployer.create_changeset( stack_name, template, parameters, capabilities, role_arn, - notification_arns, s3_uploader) + notification_arns, s3_uploader, []) self.assertEquals(response["Id"], result.changeset_id) self.assertEquals("UPDATE", result.changeset_type)