From 2701428f1b07781402f8bf2ceaccf4a97477c098 Mon Sep 17 00:00:00 2001 From: Tristan Hill Date: Mon, 15 May 2017 17:42:21 +0100 Subject: [PATCH 1/3] don't return non zero with no stack change --- awscli/customizations/cloudformation/deploy.py | 18 +++++++++++------- .../customizations/cloudformation/deployer.py | 2 +- .../cloudformation/test_deployer.py | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/awscli/customizations/cloudformation/deploy.py b/awscli/customizations/cloudformation/deploy.py index 95e0ced1ee73..891f4f261bfa 100644 --- a/awscli/customizations/cloudformation/deploy.py +++ b/awscli/customizations/cloudformation/deploy.py @@ -186,13 +186,17 @@ def _run_main(self, parsed_args, parsed_globals): 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) + 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: + sys.stdout.write("%s\n" % ex) + return 0 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..67ffe4520a5a 100644 --- a/awscli/customizations/cloudformation/deployer.py +++ b/awscli/customizations/cloudformation/deployer.py @@ -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} " diff --git a/tests/unit/customizations/cloudformation/test_deployer.py b/tests/unit/customizations/cloudformation/test_deployer.py index e4299f0c6911..8f9a5ae81cf0 100644 --- a/tests/unit/customizations/cloudformation/test_deployer.py +++ b/tests/unit/customizations/cloudformation/test_deployer.py @@ -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", From 62bb76d585a1c07ffcc0f0cb8572fdbe5c35ed19 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Wed, 25 Oct 2017 15:43:46 -0700 Subject: [PATCH 2/3] Add flag to set rc for empty deploy changeset --- .../customizations/cloudformation/deploy.py | 32 ++++++++++++++- .../cloudformation/test_deploy.py | 40 ++++++++++++++++++- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/awscli/customizations/cloudformation/deploy.py b/awscli/customizations/cloudformation/deploy.py index 891f4f261bfa..bf5ecd35c836 100644 --- a/awscli/customizations/cloudformation/deploy.py +++ b/awscli/customizations/cloudformation/deploy.py @@ -150,6 +150,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.' + ) } ] @@ -181,11 +206,12 @@ 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): + notification_arns, fail_on_empty_changeset=True): try: result = deployer.create_and_wait_for_changeset( stack_name=stack_name, @@ -195,6 +221,8 @@ def deploy(self, deployer, stack_name, template_str, role_arn=role_arn, notification_arns=notification_arns) except exceptions.ChangeEmptyError as ex: + if fail_on_empty_changeset: + raise sys.stdout.write("%s\n" % ex) return 0 diff --git a/tests/unit/customizations/cloudformation/test_deploy.py b/tests/unit/customizations/cloudformation/test_deploy.py index 7dd9dc3982fd..bce6e1e133ee 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=[], + 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) @@ -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) @@ -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): """ From 007e8cac9fbfc17f07d311fe472fd2b76822b33d Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 16 Jan 2018 14:00:34 -0800 Subject: [PATCH 3/3] Unify error writing logic --- awscli/clidriver.py | 6 ++-- .../customizations/cloudformation/deploy.py | 4 ++- awscli/utils.py | 6 ++++ tests/functional/test_utils.py | 29 +++++++++++++++++++ 4 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 tests/functional/test_utils.py diff --git a/awscli/clidriver.py b/awscli/clidriver.py index b0eb2f4ed2f5..c113642c9733 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -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') @@ -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): diff --git a/awscli/customizations/cloudformation/deploy.py b/awscli/customizations/cloudformation/deploy.py index bf5ecd35c836..cb26a7b87641 100644 --- a/awscli/customizations/cloudformation/deploy.py +++ b/awscli/customizations/cloudformation/deploy.py @@ -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__) @@ -223,7 +225,7 @@ def deploy(self, deployer, stack_name, template_str, except exceptions.ChangeEmptyError as ex: if fail_on_empty_changeset: raise - sys.stdout.write("%s\n" % ex) + write_exception(ex, outfile=get_stdout_text_writer()) return 0 if execute_changeset: diff --git a/awscli/utils.py b/awscli/utils.py index 7bf474024186..dfe52f4932df 100644 --- a/awscli/utils.py +++ b/awscli/utils.py @@ -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") diff --git a/tests/functional/test_utils.py b/tests/functional/test_utils.py new file mode 100644 index 000000000000..649c2988c22e --- /dev/null +++ b/tests/functional/test_utils.py @@ -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) +