Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 44 additions & 11 deletions awscli/customizations/cloudformation/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,37 @@ 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,
'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):
Expand Down Expand Up @@ -267,22 +297,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
Expand Down
9 changes: 5 additions & 4 deletions awscli/customizations/cloudformation/deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,15 @@ 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

:param stack_name: Name or ID of stack
: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:
"""

Expand All @@ -107,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
Expand Down Expand Up @@ -208,12 +210,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
2 changes: 1 addition & 1 deletion awscli/examples/cloudformation/deploy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

79 changes: 46 additions & 33 deletions tests/unit/customizations/cloudformation/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -107,17 +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,
True)

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)

Expand Down Expand Up @@ -158,7 +160,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
Expand All @@ -183,19 +185,21 @@ 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)

s3UploaderMock.assert_called_once_with(mock.ANY,
bucket_name,
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,
mock.ANY,
self.parsed_args.s3_prefix,
self.parsed_args.kms_key_id,
Expand All @@ -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)
Expand All @@ -228,7 +233,8 @@ def test_deploy_success(self):
execute_changeset,
role_arn,
notification_arns,
s3_uploader)
s3_uploader,
tags)
self.assertEqual(rc, 0)


Expand All @@ -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)
Expand All @@ -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")
Expand All @@ -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,
Expand All @@ -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()
Expand All @@ -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):
Expand All @@ -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"
Expand All @@ -312,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
Expand All @@ -320,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"
Expand All @@ -337,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
)

Expand Down
Loading