Skip to content

Commit

Permalink
Merge pull request #2922 from JordonPhillips/zero-rc-deploy
Browse files Browse the repository at this point in the history
Add option to return zero rc for empty deploy changeset
  • Loading branch information
JordonPhillips authored Jan 17, 2018
2 parents 0841e1e + 007e8ca commit 4851c13
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 17 deletions.
6 changes: 2 additions & 4 deletions awscli/clidriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from awscli.alias import AliasLoader
from awscli.alias import AliasCommandInjector
from awscli.utils import emit_top_level_args_parsed_event
from awscli.utils import write_exception


LOG = logging.getLogger('awscli.clidriver')
Expand Down Expand Up @@ -229,10 +230,7 @@ def main(self, args=None):
except Exception as e:
LOG.debug("Exception caught in main()", exc_info=True)
LOG.debug("Exiting with rc 255")
err = get_stderr_text_writer()
err.write("\n")
err.write(six.text_type(e))
err.write("\n")
write_exception(e, outfile=get_stderr_text_writer())
return 255

def _emit_session_event(self, parsed_args):
Expand Down
52 changes: 43 additions & 9 deletions awscli/customizations/cloudformation/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from awscli.customizations.cloudformation.yamlhelper import yaml_parse

from awscli.customizations.commands import BasicCommand
from awscli.compat import get_stdout_text_writer
from awscli.utils import write_exception

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -150,6 +152,31 @@ class DeployCommand(BasicCommand):
'Amazon Simple Notification Service topic Amazon Resource Names'
' (ARNs) that AWS CloudFormation associates with the stack.'
)
},
{
'name': 'fail-on-empty-changeset',
'required': False,
'action': 'store_true',
'group_name': 'fail-on-empty-changeset',
'dest': 'fail_on_empty_changeset',
'default': True,
'help_text': (
'Specify if the CLI should return a non-zero exit code if '
'there are no changes to be made to the stack. The default '
'behavior is to return a non-zero exit code.'
)
},
{
'name': 'no-fail-on-empty-changeset',
'required': False,
'action': 'store_false',
'group_name': 'fail-on-empty-changeset',
'dest': 'fail_on_empty_changeset',
'default': True,
'help_text': (
'Causes the CLI to return an exit code of 0 if there are no '
'changes to be made to the stack.'
)
}
]

Expand Down Expand Up @@ -181,18 +208,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)
parsed_args.notification_arns,
parsed_args.fail_on_empty_changeset)

def deploy(self, deployer, stack_name, template_str,
parameters, capabilities, execute_changeset, role_arn,
notification_arns):
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, 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)
except exceptions.ChangeEmptyError as ex:
if fail_on_empty_changeset:
raise
write_exception(ex, outfile=get_stdout_text_writer())
return 0

if execute_changeset:
deployer.execute_changeset(result.changeset_id, stack_name)
Expand Down
2 changes: 1 addition & 1 deletion awscli/customizations/cloudformation/deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def wait_for_changeset(self, changeset_id, stack_name):
reason = resp["StatusReason"]

if status == "FAILED" and \
"No updates are to be performed" in reason:
"The submitted information didn't contain changes." in reason:
raise exceptions.ChangeEmptyError(stack_name=stack_name)

raise RuntimeError("Failed to create the changeset: {0} "
Expand Down
6 changes: 6 additions & 0 deletions awscli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,9 @@ def _get_process_pager_kwargs(self, pager_cmd):
kwargs = get_popen_kwargs_for_pager_cmd(pager_cmd)
kwargs['stdin'] = subprocess.PIPE
return kwargs


def write_exception(ex, outfile):
outfile.write("\n")
outfile.write(six.text_type(ex))
outfile.write("\n")
29 changes: 29 additions & 0 deletions tests/functional/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os
import tempfile
import shutil
import codecs

from awscli.testutils import unittest
from awscli.utils import write_exception


class TestWriteException(unittest.TestCase):
def setUp(self):
self.tempdir = tempfile.mkdtemp()
self.outfile = os.path.join(self.tempdir, 'stdout')

def tearDown(self):
shutil.rmtree(self.tempdir)

def test_write_exception(self):
error_message = "Some error message."
ex = Exception(error_message)
with codecs.open(self.outfile, 'w+', encoding='utf-8') as outfile:
write_exception(ex, outfile)
outfile.seek(0)

expected_output = (
"\n%s\n" % error_message
)
self.assertEqual(outfile.read(), expected_output)

40 changes: 38 additions & 2 deletions tests/unit/customizations/cloudformation/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ def setUp(self):
execute_changeset=True,
capabilities=None,
role_arn=None,
notification_arns=[])
notification_arns=[],
fail_on_empty_changeset=True)
self.parsed_globals = FakeArgs(region="us-east-1", endpoint_url=None,
verify_ssl=None)
self.deploy_command = DeployCommand(self.session)
Expand Down Expand Up @@ -109,7 +110,7 @@ def test_command_invoked(self, mock_yaml_parse):
None,
not self.parsed_args.no_execute_changeset,
None,
[])
[], True)

self.deploy_command.parse_parameter_arg.assert_called_once_with(
self.parsed_args.parameter_overrides)
Expand Down Expand Up @@ -223,6 +224,41 @@ def test_deploy_raise_exception(self):
role_arn,
notification_arns)

def test_deploy_raises_exception_on_empty_changeset(self):
stack_name = "stack_name"
parameters = ["a", "b"]
template = "cloudformation template"
capabilities = ["foo", "bar"]
execute_changeset = True
role_arn = "arn:aws:iam::1234567890:role"
notification_arns = ["arn:aws:sns:region:1234567890:notify"]

empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name)
changeset_func = self.deployer.create_and_wait_for_changeset
changeset_func.side_effect = empty_changeset
with self.assertRaises(exceptions.ChangeEmptyError):
self.deploy_command.deploy(
self.deployer, stack_name, template, parameters, capabilities,
execute_changeset, role_arn, notification_arns
)

def test_deploy_does_not_raise_exception_on_empty_changeset(self):
stack_name = "stack_name"
parameters = ["a", "b"]
template = "cloudformation template"
capabilities = ["foo", "bar"]
execute_changeset = True
role_arn = "arn:aws:iam::1234567890:role"
notification_arns = ["arn:aws:sns:region:1234567890:notify"]

empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name)
changeset_func = self.deployer.create_and_wait_for_changeset
changeset_func.side_effect = empty_changeset
self.deploy_command.deploy(
self.deployer, stack_name, template, parameters, capabilities,
execute_changeset, role_arn, notification_arns,
fail_on_empty_changeset=False
)

def test_parse_parameter_arg_success(self):
"""
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/customizations/cloudformation/test_deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ def test_wait_for_changeset_no_changes(self):

response = {
"Status": "FAILED",
"StatusReason": "No updates are to be performed"
"StatusReason": "The submitted information didn't contain changes."
}

waiter_error = botocore.exceptions.WaiterError(name="name",
Expand Down

0 comments on commit 4851c13

Please sign in to comment.