From 060742ffec6c73ef0b50af3e9dc837779a95af7d Mon Sep 17 00:00:00 2001 From: Alec Posney Date: Mon, 22 May 2017 14:59:19 +1000 Subject: [PATCH 1/4] Added Cfn stack tag support to the deploy command --- .../customizations/cloudformation/deploy.py | 39 +++++++++++++++++-- .../customizations/cloudformation/deployer.py | 7 ++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/awscli/customizations/cloudformation/deploy.py b/awscli/customizations/cloudformation/deploy.py index 95e0ced1ee73..726302c1da51 100644 --- a/awscli/customizations/cloudformation/deploy.py +++ b/awscli/customizations/cloudformation/deploy.py @@ -150,7 +150,37 @@ class DeployCommand(BasicCommand): 'Amazon Simple Notification Service topic Amazon Resource Names' ' (ARNs) that AWS CloudFormation associates with 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): @@ -181,18 +211,19 @@ 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) + parsed_args.notification_arns, parsed_args.tags) def deploy(self, deployer, stack_name, template_str, parameters, capabilities, execute_changeset, role_arn, - notification_arns): + notification_arns, tags): 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) + notification_arns=notification_arns, + tags=tags) if execute_changeset: deployer.execute_changeset(result.changeset_id, stack_name) diff --git a/awscli/customizations/cloudformation/deployer.py b/awscli/customizations/cloudformation/deployer.py index 03026d25a0ea..0fa0fc3129a9 100644 --- a/awscli/customizations/cloudformation/deployer.py +++ b/awscli/customizations/cloudformation/deployer.py @@ -71,7 +71,7 @@ def has_stack(self, stack_name): def create_changeset(self, stack_name, cfn_template, parameter_values, capabilities, role_arn, - notification_arns): + notification_arns, tags): """ Call Cloudformation to create a changeset and wait for it to complete @@ -79,6 +79,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: """ @@ -193,11 +194,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): + notification_arns, tags): result = self.create_changeset( stack_name, cfn_template, parameter_values, capabilities, - role_arn, notification_arns) + role_arn, notification_arns, tags) self.wait_for_changeset(result.changeset_id, stack_name) From 0c9fc1c8c24d733a643477d5603371cf82e4c9ed Mon Sep 17 00:00:00 2001 From: Alec Posney Date: Mon, 22 May 2017 14:59:59 +1000 Subject: [PATCH 2/4] 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 a45b8086b4798e8c278706bfb59b8a30c863b913 Mon Sep 17 00:00:00 2001 From: Alec Posney Date: Mon, 22 May 2017 15:22:38 +1000 Subject: [PATCH 3/4] updated tests to handle new parameter --- .../cloudformation/test_deploy.py | 26 ++++++++++++------- .../cloudformation/test_deployer.py | 18 ++++++++----- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/tests/unit/customizations/cloudformation/test_deploy.py b/tests/unit/customizations/cloudformation/test_deploy.py index 7dd9dc3982fd..b9ae3425ac13 100644 --- a/tests/unit/customizations/cloudformation/test_deploy.py +++ b/tests/unit/customizations/cloudformation/test_deploy.py @@ -56,7 +56,8 @@ def setUp(self): execute_changeset=True, capabilities=None, role_arn=None, - notification_arns=[]) + notification_arns=[], + 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) @@ -109,7 +110,8 @@ def test_command_invoked(self, mock_yaml_parse): None, not self.parsed_args.no_execute_changeset, None, - []) + [], + mock.ANY) self.deploy_command.parse_parameter_arg.assert_called_once_with( self.parsed_args.parameter_overrides) @@ -140,7 +142,7 @@ def test_deploy_success(self): changeset_type = "CREATE" role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] - + 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) @@ -152,7 +154,8 @@ def test_deploy_success(self): capabilities, execute_changeset, role_arn, - notification_arns) + notification_arns, + tags) self.assertEqual(rc, 0) @@ -161,7 +164,8 @@ def test_deploy_success(self): parameter_values=parameters, capabilities=capabilities, role_arn=role_arn, - notification_arns=notification_arns) + notification_arns=notification_arns, + 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) @@ -177,6 +181,7 @@ def test_deploy_no_execute(self): execute_changeset = False role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] + tags = [{"Key":"key1", "Value": "val1"}] self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, "CREATE") @@ -187,7 +192,8 @@ def test_deploy_no_execute(self): capabilities, execute_changeset, role_arn, - notification_arns) + notification_arns, + tags) self.assertEqual(rc, 0) self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name, @@ -195,7 +201,8 @@ def test_deploy_no_execute(self): parameter_values=parameters, capabilities=capabilities, role_arn=role_arn, - notification_arns=notification_arns) + notification_arns=notification_arns, + tags=tags) # since execute_changeset is set to True, deploy() will execute changeset self.deployer.execute_changeset.assert_not_called() @@ -210,7 +217,7 @@ def test_deploy_raise_exception(self): execute_changeset = True role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] - + tags = [{"Key":"key1", "Value": "val1"}] self.deployer.wait_for_execute.side_effect = RuntimeError("Some error") with self.assertRaises(RuntimeError): @@ -221,7 +228,8 @@ def test_deploy_raise_exception(self): capabilities, execute_changeset, role_arn, - notification_arns) + notification_arns, + tags) def test_parse_parameter_arg_success(self): diff --git a/tests/unit/customizations/cloudformation/test_deployer.py b/tests/unit/customizations/cloudformation/test_deployer.py index e4299f0c6911..009718792dcc 100644 --- a/tests/unit/customizations/cloudformation/test_deployer.py +++ b/tests/unit/customizations/cloudformation/test_deployer.py @@ -104,6 +104,8 @@ def test_create_changeset_success(self): role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] + tags = [{"Key":"key1", "Value": "val1"}] + # Case 1: Stack DOES NOT exist self.deployer.has_stack = Mock() self.deployer.has_stack.return_value = False @@ -117,7 +119,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 = { @@ -129,7 +132,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) + notification_arns, tags) self.assertEquals(response["Id"], result.changeset_id) self.assertEquals("CREATE", result.changeset_type) @@ -142,7 +145,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) + notification_arns, tags) self.assertEquals(response["Id"], result.changeset_id) self.assertEquals("UPDATE", result.changeset_type) @@ -154,6 +157,7 @@ def test_create_changeset_exception(self): capabilities = ["capabilities"] role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] + tags = [{"Key":"key1", "Value": "val1"}] self.deployer.has_stack = Mock() self.deployer.has_stack.return_value = False @@ -163,7 +167,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) + capabilities, role_arn, notification_arns, tags) def test_execute_changeset(self): stack_name = "stack_name" @@ -198,6 +202,7 @@ def test_create_and_wait_for_changeset_successful(self): changeset_type = "changeset type" role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] + tags = [{"Key":"key1", "Value": "val1"}] self.deployer.create_changeset = Mock() self.deployer.create_changeset.return_value = ChangeSetResult(changeset_id, changeset_type) @@ -206,7 +211,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) + notification_arns, tags) self.assertEquals(result.changeset_id, changeset_id) self.assertEquals(result.changeset_type, changeset_type) @@ -220,6 +225,7 @@ def test_create_and_wait_for_changeset_error_waiting_for_changeset(self): changeset_type = "changeset type" role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] + tags = [{"Key":"key1", "Value": "val1"}] self.deployer.create_changeset = Mock() self.deployer.create_changeset.return_value = ChangeSetResult(changeset_id, changeset_type) @@ -230,7 +236,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) + notification_arns, tags) def test_wait_for_changeset_no_changes(self): stack_name = "stack_name" From 0e43cf766f191723b76b653590d9a24ef371a029 Mon Sep 17 00:00:00 2001 From: Alec Posney Date: Fri, 27 Oct 2017 12:17:01 +1100 Subject: [PATCH 4/4] 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 0fa0fc3129a9..7804963668a9 100644 --- a/awscli/customizations/cloudformation/deployer.py +++ b/awscli/customizations/cloudformation/deployer.py @@ -106,6 +106,7 @@ def create_changeset(self, stack_name, cfn_template, 'Parameters': parameter_values, 'Capabilities': capabilities, 'Description': description, + 'Tags': tags, } # don't set these arguments if not specified to use existing values if role_arn is not None: