diff --git a/requirements/base.txt b/requirements/base.txt index 1941ec69eb15..a98ba70d76a6 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -2,7 +2,7 @@ chevron~=0.12 click~=7.1 Flask~=1.1.2 #Need to add Schemas latest SDK. -boto3~=1.14 +boto3~=1.18 jmespath~=0.10.0 PyYAML~=5.3 cookiecutter~=1.7.2 diff --git a/samcli/commands/deploy/command.py b/samcli/commands/deploy/command.py index e1e3ae045286..682ec8793829 100644 --- a/samcli/commands/deploy/command.py +++ b/samcli/commands/deploy/command.py @@ -167,6 +167,13 @@ "A companion stack containing ECR repos for each function will be deployed along with the template stack. " "Automatically created image repositories will be deleted if the corresponding functions are removed.", ) +@click.option( + "--disable-rollback/--no-disable-rollback", + default=False, + required=False, + is_flag=True, + help="Preserves the state of previously provisioned resources when an operation fails.", +) @metadata_override_option @notification_arns_override_option @tags_override_option @@ -208,6 +215,7 @@ def cli( resolve_image_repos, config_file, config_env, + disable_rollback, ): """ `sam deploy` command entry point @@ -241,6 +249,7 @@ def cli( config_file, config_env, resolve_image_repos, + disable_rollback, ) # pragma: no cover @@ -272,6 +281,7 @@ def do_cli( config_file, config_env, resolve_image_repos, + disable_rollback, ): """ Implementation of the ``cli`` method @@ -299,6 +309,7 @@ def do_cli( config_section=CONFIG_SECTION, config_env=config_env, config_file=config_file, + disable_rollback=disable_rollback, ) guided_context.run() else: @@ -361,5 +372,6 @@ def do_cli( profile=profile, confirm_changeset=guided_context.confirm_changeset if guided else confirm_changeset, signing_profiles=guided_context.signing_profiles if guided else signing_profiles, + disable_rollback=guided_context.disable_rollback if guided else disable_rollback, ) as deploy_context: deploy_context.run() diff --git a/samcli/commands/deploy/deploy_context.py b/samcli/commands/deploy/deploy_context.py index 3d4e7bc16a36..5ed3d83eb4dc 100644 --- a/samcli/commands/deploy/deploy_context.py +++ b/samcli/commands/deploy/deploy_context.py @@ -70,6 +70,7 @@ def __init__( profile, confirm_changeset, signing_profiles, + disable_rollback, ): self.template_file = template_file self.stack_name = stack_name @@ -97,6 +98,7 @@ def __init__( self.deployer = None self.confirm_changeset = confirm_changeset self.signing_profiles = signing_profiles + self.disable_rollback = disable_rollback def __enter__(self): return self @@ -151,6 +153,7 @@ def run(self): display_parameter_overrides, self.confirm_changeset, self.signing_profiles, + self.disable_rollback, ) return self.deploy( self.stack_name, @@ -165,6 +168,7 @@ def run(self): region, self.fail_on_empty_changeset, self.confirm_changeset, + self.disable_rollback, ) def deploy( @@ -181,6 +185,7 @@ def deploy( region, fail_on_empty_changeset=True, confirm_changeset=False, + disable_rollback=False, ): """ Deploy the stack to cloudformation. @@ -213,6 +218,8 @@ def deploy( Should fail when changeset is empty confirm_changeset : bool Should wait for customer's confirm before executing the changeset + disable_rollback : bool + Preserves the state of previously provisioned resources when an operation fails """ stacks, _ = SamLocalStackProvider.get_stacks( self.template_file, @@ -247,8 +254,8 @@ def deploy( if not click.confirm(f"{self.MSG_CONFIRM_CHANGESET}", default=False): return - self.deployer.execute_changeset(result["Id"], stack_name) - self.deployer.wait_for_execute(stack_name, changeset_type) + self.deployer.execute_changeset(result["Id"], stack_name, disable_rollback) + self.deployer.wait_for_execute(stack_name, changeset_type, disable_rollback) click.echo(self.MSG_EXECUTE_SUCCESS.format(stack_name=stack_name, region=region)) except deploy_exceptions.ChangeEmptyError as ex: diff --git a/samcli/commands/deploy/guided_context.py b/samcli/commands/deploy/guided_context.py index f878c011f976..50ed72f2dda2 100644 --- a/samcli/commands/deploy/guided_context.py +++ b/samcli/commands/deploy/guided_context.py @@ -41,6 +41,7 @@ class GuidedContext: + # pylint: disable=too-many-statements def __init__( self, template_file, @@ -59,6 +60,7 @@ def __init__( config_section=None, config_env=None, config_file=None, + disable_rollback=None, ): self.template_file = template_file self.stack_name = stack_name @@ -89,6 +91,7 @@ def __init__( self.end_bold = "\033[0m" self.color = Colored() self.function_provider = None + self.disable_rollback = disable_rollback @property def guided_capabilities(self): @@ -153,6 +156,9 @@ def guided_prompts(self, parameter_override_keys): type=FuncParamType(func=_space_separated_list_func_type), ) + click.secho("\t#Preserves the state of previously provisioned resources when an operation fails") + disable_rollback = confirm(f"\t{self.start_bold}Disable rollback{self.end_bold}", default=self.disable_rollback) + self.prompt_authorization(stacks) self.prompt_code_signing_settings(stacks) @@ -194,6 +200,7 @@ def guided_prompts(self, parameter_override_keys): self.config_env = config_env if config_env else default_config_env self.config_file = config_file if config_file else default_config_file self.confirm_changeset = confirm_changeset + self.disable_rollback = disable_rollback def prompt_authorization(self, stacks: List[Stack]): auth_required_per_resource = auth_per_resource(stacks) @@ -560,6 +567,7 @@ def run(self): confirm_changeset=self.confirm_changeset, capabilities=self._capabilities, signing_profiles=self.signing_profiles, + disable_rollback=self.disable_rollback, ) @staticmethod diff --git a/samcli/commands/deploy/utils.py b/samcli/commands/deploy/utils.py index c961eb710fae..647572d8b1c6 100644 --- a/samcli/commands/deploy/utils.py +++ b/samcli/commands/deploy/utils.py @@ -18,6 +18,7 @@ def print_deploy_args( parameter_overrides, confirm_changeset, signing_profiles, + disable_rollback, ): """ Print a table of the values that are used during a sam deploy. @@ -30,6 +31,7 @@ def print_deploy_args( Stack name : sam-app Region : us-east-1 Confirm changeset : False + Disable rollback : False Deployment s3 bucket : aws-sam-cli-managed-default-samclisourcebucket-abcdef Capabilities : ["CAPABILITY_IAM"] Parameter overrides : {'MyParamater': '***', 'Parameter2': 'dd'} @@ -43,6 +45,7 @@ def print_deploy_args( :param parameter_overrides: Cloudformation parameter overrides to be supplied based on the stack's template :param confirm_changeset: Prompt for changeset to be confirmed before going ahead with the deploy. :param signing_profiles: Signing profile details which will be used to sign functions/layers + :param disable_rollback: Preserve the state of previously provisioned resources when an operation fails. """ _parameters = parameter_overrides.copy() @@ -63,6 +66,7 @@ def print_deploy_args( click.echo(f"\tStack name : {stack_name}") click.echo(f"\tRegion : {region}") click.echo(f"\tConfirm changeset : {confirm_changeset}") + click.echo(f"\tDisable rollback : {disable_rollback}") if image_repository: msg = "Deployment image repository : " # NOTE(sriram-mv): tab length is 8 spaces. diff --git a/samcli/lib/deploy/deployer.py b/samcli/lib/deploy/deployer.py index 8c1326698f81..ec73af71d708 100644 --- a/samcli/lib/deploy/deployer.py +++ b/samcli/lib/deploy/deployer.py @@ -37,6 +37,7 @@ from samcli.lib.package.local_files_utils import mktempfile, get_uploaded_s3_object_name from samcli.lib.package.s3_uploader import S3Uploader from samcli.lib.utils.time import utc_to_timestamp +from samcli.lib.utils.colors import Colored LOG = logging.getLogger(__name__) @@ -83,6 +84,7 @@ def __init__(self, cloudformation_client, changeset_prefix="samcli-deploy"): # Maximum number of attempts before raising exception back up the chain. self.max_attempts = 3 self.deploy_color = DeployColor() + self._colored = Colored() # pylint: disable=inconsistent-return-statements def has_stack(self, stack_name): @@ -307,16 +309,19 @@ def wait_for_changeset(self, changeset_id, stack_name): stack_name=stack_name, msg="ex: {0} Status: {1}. Reason: {2}".format(ex, status, reason) ) from ex - def execute_changeset(self, changeset_id, stack_name): + def execute_changeset(self, changeset_id, stack_name, disable_rollback): """ Calls CloudFormation to execute changeset :param changeset_id: ID of the changeset :param stack_name: Name or ID of the stack + :param disable_rollback: Preserve the state of previously provisioned resources when an operation fails. :return: Response from execute-change-set call """ try: - return self._client.execute_change_set(ChangeSetName=changeset_id, StackName=stack_name) + return self._client.execute_change_set( + ChangeSetName=changeset_id, StackName=stack_name, DisableRollback=disable_rollback + ) except botocore.exceptions.ClientError as ex: raise DeployFailedError(stack_name=stack_name, msg=str(ex)) from ex @@ -390,7 +395,7 @@ def describe_stack_events(self, stack_name, time_stamp_marker, **kwargs): continue break # reached here only if break from inner loop! - if self._check_stack_complete(stack_status): + if self._check_stack_not_in_progress(stack_status): stack_change_in_progress = False break except botocore.exceptions.ClientError as ex: @@ -402,10 +407,10 @@ def describe_stack_events(self, stack_name, time_stamp_marker, **kwargs): time.sleep(math.pow(self.backoff, retry_attempts)) @staticmethod - def _check_stack_complete(status: str) -> bool: - return "COMPLETE" in status and "CLEANUP" not in status + def _check_stack_not_in_progress(status: str) -> bool: + return "IN_PROGRESS" not in status - def wait_for_execute(self, stack_name, changeset_type): + def wait_for_execute(self, stack_name, changeset_type, disable_rollback): """ Wait for changeset to execute and return when execution completes. If the stack has "Outputs," they will be printed. @@ -416,6 +421,8 @@ def wait_for_execute(self, stack_name, changeset_type): The name of the stack changeset_type : str The type of the changeset, 'CREATE' or 'UPDATE' + disable_rollback : bool + Preserves the state of previously provisioned resources when an operation fails """ sys.stdout.write( "\n{} - Waiting for stack create/update " @@ -441,6 +448,9 @@ def wait_for_execute(self, stack_name, changeset_type): waiter.wait(StackName=stack_name, WaiterConfig=waiter_config) except botocore.exceptions.WaiterError as ex: LOG.debug("Execute changeset waiter exception", exc_info=ex) + if disable_rollback: + msg = self._gen_deploy_failed_with_rollback_disabled_msg(stack_name) + LOG.info(self._colored.red(msg)) raise deploy_exceptions.DeployFailedError(stack_name=stack_name, msg=str(ex)) @@ -501,3 +511,14 @@ def get_stack_outputs(self, stack_name, echo=True): except botocore.exceptions.ClientError as ex: raise DeployStackOutPutFailedError(stack_name=stack_name, msg=str(ex)) from ex + + @staticmethod + def _gen_deploy_failed_with_rollback_disabled_msg(stack_name): + return """\nFailed to deploy. Automatic rollback disabled for this deployment.\n +Actions you can take next +========================= +[*] Fix issues and try deploying again +[*] Roll back stack to the last known stable state: aws cloudformation rollback-stack --stack-name {stack_name} +""".format( + stack_name=stack_name + ) diff --git a/tests/integration/deploy/deploy_integ_base.py b/tests/integration/deploy/deploy_integ_base.py index 870ee7203aff..ef2ba65a3fa1 100644 --- a/tests/integration/deploy/deploy_integ_base.py +++ b/tests/integration/deploy/deploy_integ_base.py @@ -47,6 +47,7 @@ def get_deploy_command_list( config_file=None, signing_profiles=None, resolve_image_repos=False, + disable_rollback=False, ): command_list = [self.base_command(), "deploy"] @@ -106,6 +107,8 @@ def get_deploy_command_list( command_list = command_list + ["--signing-profiles", str(signing_profiles)] if resolve_image_repos: command_list = command_list + ["--resolve-image-repos"] + if disable_rollback: + command_list = command_list + ["--disable-rollback"] return command_list diff --git a/tests/integration/deploy/test_deploy_command.py b/tests/integration/deploy/test_deploy_command.py index b2968e1d94eb..e6a119ca7c9b 100644 --- a/tests/integration/deploy/test_deploy_command.py +++ b/tests/integration/deploy/test_deploy_command.py @@ -586,7 +586,7 @@ def test_deploy_guided_zip(self, template_file): deploy_command_list = self.get_deploy_command_list(template_file=template_path, guided=True) deploy_process_execute = run_command_with_input( - deploy_command_list, "{}\n\n\n\n\n\n\n\n\n".format(stack_name).encode() + deploy_command_list, "{}\n\n\n\n\n\n\n\n\n\n".format(stack_name).encode() ) # Deploy should succeed with a managed stack @@ -606,7 +606,7 @@ def test_deploy_guided_image_auto(self, template_file): deploy_command_list = self.get_deploy_command_list(template_file=template_path, guided=True) deploy_process_execute = run_command_with_input( - deploy_command_list, f"{stack_name}\n\n\n\ny\n\n\ny\n\n\n\n".encode() + deploy_command_list, f"{stack_name}\n\n\n\n\ny\n\n\ny\n\n\n\n".encode() ) # Deploy should succeed with a managed stack @@ -626,7 +626,7 @@ def test_deploy_guided_image_specify(self, template_file): deploy_command_list = self.get_deploy_command_list(template_file=template_path, guided=True) deploy_process_execute = run_command_with_input( - deploy_command_list, f"{stack_name}\n\n\n\ny\n\n\n\nn\n{self.ecr_repo_name}\n\n\n\n".encode() + deploy_command_list, f"{stack_name}\n\n\n\n\ny\n\n\n\nn\n{self.ecr_repo_name}\n\n\n\n".encode() ) # Deploy should succeed with a managed stack @@ -654,7 +654,7 @@ def test_deploy_guided_set_parameter(self, template_file): deploy_command_list = self.get_deploy_command_list(template_file=template_path, guided=True) deploy_process_execute = run_command_with_input( - deploy_command_list, "{}\n\nSuppliedParameter\n\n\n\n\n\n\n".format(stack_name).encode() + deploy_command_list, "{}\n\nSuppliedParameter\n\n\n\n\n\n\n\n".format(stack_name).encode() ) # Deploy should succeed with a managed stack @@ -675,7 +675,7 @@ def test_deploy_guided_set_capabilities(self, template_file): deploy_process_execute = run_command_with_input( deploy_command_list, - "{}\n\nSuppliedParameter\n\nn\nCAPABILITY_IAM CAPABILITY_NAMED_IAM\n\n\n\n".format(stack_name).encode(), + "{}\n\nSuppliedParameter\n\nn\nCAPABILITY_IAM CAPABILITY_NAMED_IAM\n\n\n\n\n".format(stack_name).encode(), ) # Deploy should succeed with a managed stack self.assertEqual(deploy_process_execute.process.returncode, 0) @@ -695,7 +695,7 @@ def test_deploy_guided_capabilities_default(self, template_file): # Set no for Allow SAM CLI IAM role creation, but allow default of ["CAPABILITY_IAM"] by just hitting the return key. deploy_process_execute = run_command_with_input( - deploy_command_list, "{}\n\nSuppliedParameter\n\nn\n\n\n\n\n\n".format(stack_name).encode() + deploy_command_list, "{}\n\nSuppliedParameter\n\nn\n\n\n\n\n\n\n".format(stack_name).encode() ) # Deploy should succeed with a managed stack self.assertEqual(deploy_process_execute.process.returncode, 0) @@ -714,7 +714,7 @@ def test_deploy_guided_set_confirm_changeset(self, template_file): deploy_command_list = self.get_deploy_command_list(template_file=template_path, guided=True) deploy_process_execute = run_command_with_input( - deploy_command_list, "{}\n\nSuppliedParameter\nY\n\nY\n\n\n\n".format(stack_name).encode() + deploy_command_list, "{}\n\nSuppliedParameter\nY\n\n\nY\n\n\n\n".format(stack_name).encode() ) # Deploy should succeed with a managed stack @@ -887,7 +887,7 @@ def test_deploy_guided_sar_with_location_from_map(self, template_file, region, w deploy_command_list = self.get_deploy_command_list(template_file=template_path, guided=True) deploy_process_execute = run_command_with_input( - deploy_command_list, f"{stack_name}\n{region}\n\nN\nCAPABILITY_IAM CAPABILITY_AUTO_EXPAND\nN\n".encode() + deploy_command_list, f"{stack_name}\n{region}\n\nN\nCAPABILITY_IAM CAPABILITY_AUTO_EXPAND\nn\nN\n".encode() ) if will_succeed: @@ -929,6 +929,244 @@ def test_deploy_nested_stacks(self, template_file): # verify child stack ChildStackX's creation self.assertRegex(process_stdout, r"CREATE_COMPLETE.+ChildStackX") + @parameterized.expand(["aws-dynamodb-error.yaml"]) + def test_deploy_create_failed_rollback(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = self._method_to_stack_name(self.id()) + self.stacks.append({"name": stack_name}) + + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + stack_name=stack_name, + capabilities="CAPABILITY_IAM", + s3_prefix="integ_deploy", + s3_bucket=self.s3_bucket.name, + image_repository=self.ecr_repo_name, + force_upload=True, + notification_arns=self.sns_arn, + parameter_overrides="ShardCountParameter=1", + kms_key_id=self.kms_key, + no_execute_changeset=False, + tags="integ=true clarity=yes foo_bar=baz", + confirm_changeset=False, + ) + + deploy_process_execute = run_command(deploy_command_list) + self.assertEqual(deploy_process_execute.process.returncode, 1) + + stderr = deploy_process_execute.stderr.strip() + self.assertIn( + bytes( + f"Error: Failed to create/update the stack: {stack_name}, Waiter StackCreateComplete failed: " + f'Waiter encountered a terminal failure state: For expression "Stacks[].StackStatus" ' + f'we matched expected path: "ROLLBACK_COMPLETE" at least once', + encoding="utf-8", + ), + stderr, + ) + + @parameterized.expand(["aws-dynamodb-error.yaml"]) + def test_deploy_create_failed_disable_rollback(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = self._method_to_stack_name(self.id()) + self.stacks.append({"name": stack_name}) + + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + stack_name=stack_name, + capabilities="CAPABILITY_IAM", + s3_prefix="integ_deploy", + s3_bucket=self.s3_bucket.name, + image_repository=self.ecr_repo_name, + force_upload=True, + notification_arns=self.sns_arn, + parameter_overrides="ShardCountParameter=1", + kms_key_id=self.kms_key, + no_execute_changeset=False, + tags="integ=true clarity=yes foo_bar=baz", + confirm_changeset=False, + disable_rollback=True, + ) + + deploy_process_execute = run_command(deploy_command_list) + self.assertEqual(deploy_process_execute.process.returncode, 1) + + stderr = deploy_process_execute.stderr.strip() + self.assertIn( + bytes( + f"Error: Failed to create/update the stack: {stack_name}, Waiter StackCreateComplete failed: " + f'Waiter encountered a terminal failure state: For expression "Stacks[].StackStatus" ' + f'we matched expected path: "CREATE_FAILED" at least once', + encoding="utf-8", + ), + stderr, + ) + + # Fix template and deploy again + template_path = self.test_data_path.joinpath("aws-dynamodb-error-fixed.yaml") + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + stack_name=stack_name, + capabilities="CAPABILITY_IAM", + s3_prefix="integ_deploy", + s3_bucket=self.s3_bucket.name, + image_repository=self.ecr_repo_name, + force_upload=True, + notification_arns=self.sns_arn, + parameter_overrides="ShardCountParameter=1", + kms_key_id=self.kms_key, + no_execute_changeset=False, + tags="integ=true clarity=yes foo_bar=baz", + confirm_changeset=False, + disable_rollback=True, + ) + + deploy_process_execute = run_command(deploy_command_list) + self.assertEqual(deploy_process_execute.process.returncode, 0) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_deploy_update_failed_rollback(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = self._method_to_stack_name(self.id()) + self.stacks.append({"name": stack_name}) + + # First deploy a simple template that should work + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + stack_name=stack_name, + capabilities="CAPABILITY_IAM", + s3_prefix="integ_deploy", + s3_bucket=self.s3_bucket.name, + image_repository=self.ecr_repo_name, + force_upload=True, + notification_arns=self.sns_arn, + parameter_overrides="Parameter=Clarity", + kms_key_id=self.kms_key, + no_execute_changeset=False, + tags="integ=true clarity=yes foo_bar=baz", + confirm_changeset=False, + ) + + deploy_process_execute = run_command(deploy_command_list) + self.assertEqual(deploy_process_execute.process.returncode, 0) + + # Now update stack with failing template + template_path = self.test_data_path.joinpath("aws-dynamodb-error.yaml") + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + stack_name=stack_name, + capabilities="CAPABILITY_IAM", + s3_prefix="integ_deploy", + s3_bucket=self.s3_bucket.name, + image_repository=self.ecr_repo_name, + force_upload=True, + notification_arns=self.sns_arn, + parameter_overrides="ShardCountParameter=1", + kms_key_id=self.kms_key, + no_execute_changeset=False, + tags="integ=true clarity=yes foo_bar=baz", + confirm_changeset=False, + ) + + deploy_process_execute = run_command(deploy_command_list) + self.assertEqual(deploy_process_execute.process.returncode, 1) + + stderr = deploy_process_execute.stderr.strip() + self.assertIn( + bytes( + f"Error: Failed to create/update the stack: {stack_name}, Waiter StackUpdateComplete failed: " + f'Waiter encountered a terminal failure state: For expression "Stacks[].StackStatus" ' + f'we matched expected path: "UPDATE_ROLLBACK_COMPLETE" at least once', + encoding="utf-8", + ), + stderr, + ) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_deploy_update_failed_disable_rollback(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = self._method_to_stack_name(self.id()) + self.stacks.append({"name": stack_name}) + + # First deploy a simple template that should work + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + stack_name=stack_name, + capabilities="CAPABILITY_IAM", + s3_prefix="integ_deploy", + s3_bucket=self.s3_bucket.name, + image_repository=self.ecr_repo_name, + force_upload=True, + notification_arns=self.sns_arn, + parameter_overrides="Parameter=Clarity", + kms_key_id=self.kms_key, + no_execute_changeset=False, + tags="integ=true clarity=yes foo_bar=baz", + confirm_changeset=False, + ) + + deploy_process_execute = run_command(deploy_command_list) + self.assertEqual(deploy_process_execute.process.returncode, 0) + + # Now update stack with failing template + template_path = self.test_data_path.joinpath("aws-dynamodb-error.yaml") + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + stack_name=stack_name, + capabilities="CAPABILITY_IAM", + s3_prefix="integ_deploy", + s3_bucket=self.s3_bucket.name, + image_repository=self.ecr_repo_name, + force_upload=True, + notification_arns=self.sns_arn, + parameter_overrides="ShardCountParameter=1", + kms_key_id=self.kms_key, + no_execute_changeset=False, + tags="integ=true clarity=yes foo_bar=baz", + confirm_changeset=False, + disable_rollback=True, + ) + + deploy_process_execute = run_command(deploy_command_list) + self.assertEqual(deploy_process_execute.process.returncode, 1) + + stderr = deploy_process_execute.stderr.strip() + self.assertIn( + bytes( + f"Error: Failed to create/update the stack: {stack_name}, Waiter StackUpdateComplete failed: " + f'Waiter encountered a terminal failure state: For expression "Stacks[].StackStatus" ' + f'we matched expected path: "UPDATE_FAILED" at least once', + encoding="utf-8", + ), + stderr, + ) + + # Fix template and deploy again + template_path = self.test_data_path.joinpath("aws-dynamodb-error-fixed.yaml") + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + stack_name=stack_name, + capabilities="CAPABILITY_IAM", + s3_prefix="integ_deploy", + s3_bucket=self.s3_bucket.name, + image_repository=self.ecr_repo_name, + force_upload=True, + notification_arns=self.sns_arn, + parameter_overrides="ShardCountParameter=1", + kms_key_id=self.kms_key, + no_execute_changeset=False, + tags="integ=true clarity=yes foo_bar=baz", + confirm_changeset=False, + disable_rollback=True, + ) + + deploy_process_execute = run_command(deploy_command_list) + self.assertEqual(deploy_process_execute.process.returncode, 0) + def _method_to_stack_name(self, method_name): """Method expects method name which can be a full path. Eg: test.integration.test_deploy_command.method_name""" method_name = method_name.split(".")[-1] diff --git a/tests/integration/testdata/package/aws-dynamodb-error-fixed.yaml b/tests/integration/testdata/package/aws-dynamodb-error-fixed.yaml new file mode 100644 index 000000000000..da6b5182be29 --- /dev/null +++ b/tests/integration/testdata/package/aws-dynamodb-error-fixed.yaml @@ -0,0 +1,46 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: A sample template to fix & remediate +Parameters: + ShardCountParameter: + Type: Number + MinValue: 1 + MaxValue: 10 + Description: The number of shards for the Kinesis stream +Resources: + MyBucket: + Type: AWS::S3::Bucket + MyQueue: + Type: AWS::SQS::Queue + MyStream: + Type: AWS::Kinesis::Stream + Properties: + ShardCount: !Ref ShardCountParameter + MyTable: + Type: AWS::DynamoDB::Table + Properties: + BillingMode: PAY_PER_REQUEST + AttributeDefinitions: + - AttributeName: "ArtistId" + AttributeType: "S" + - AttributeName: "Concert" + AttributeType: "S" + KeySchema: + - AttributeName: "ArtistId" + KeyType: "HASH" + - AttributeName: "Concert" + KeyType: "RANGE" + KinesisStreamSpecification: + StreamArn: !GetAtt MyStream.Arn +Outputs: + BucketName: + Value: !Ref MyBucket + Description: The name of my S3 bucket + QueueName: + Value: !GetAtt MyQueue.QueueName + Description: The name of my SQS queue + StreamName: + Value: !Ref MyStream + Description: The name of my Kinesis stream + TableName: + Value: !Ref MyTable + Description: The name of my DynamoDB table \ No newline at end of file diff --git a/tests/integration/testdata/package/aws-dynamodb-error.yaml b/tests/integration/testdata/package/aws-dynamodb-error.yaml new file mode 100644 index 000000000000..a4f3f5c4b916 --- /dev/null +++ b/tests/integration/testdata/package/aws-dynamodb-error.yaml @@ -0,0 +1,46 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: A sample template to fix & remediate +Parameters: + ShardCountParameter: + Type: Number + Description: The number of shards for the Kinesis stream +Resources: + MyBucket: + Type: AWS::S3::Bucket + MyQueue: + Type: AWS::SQS::Queue + MyStream: + Type: AWS::Kinesis::Stream + Properties: + ShardCount: !Ref ShardCountParameter + MyTable: + Type: AWS::DynamoDB::Table + Properties: + BillingMode: PAY_PER_REQUEST + AttributeDefinitions: + - AttributeName: "ArtistId" + AttributeType: "S" + - AttributeName: "Concert" + AttributeType: "S" + - AttributeName: "TicketSales" + AttributeType: "S" + KeySchema: + - AttributeName: "ArtistId" + KeyType: "HASH" + - AttributeName: "Concert" + KeyType: "RANGE" + KinesisStreamSpecification: + StreamArn: !GetAtt MyStream.Arn +Outputs: + BucketName: + Value: !Ref MyBucket + Description: The name of my S3 bucket + QueueName: + Value: !GetAtt MyQueue.QueueName + Description: The name of my SQS queue + StreamName: + Value: !Ref MyStream + Description: The name of my Kinesis stream + TableName: + Value: !Ref MyTable + Description: The name of my DynamoDB table \ No newline at end of file diff --git a/tests/unit/commands/deploy/test_command.py b/tests/unit/commands/deploy/test_command.py index 46ac917e067a..6a9239e22855 100644 --- a/tests/unit/commands/deploy/test_command.py +++ b/tests/unit/commands/deploy/test_command.py @@ -48,6 +48,7 @@ def setUp(self): self.config_file = "mock-default-filename" self.signing_profiles = None self.resolve_image_repos = False + self.disable_rollback = False MOCK_SAM_CONFIG.reset_mock() self.companion_stack_manager_patch = patch("samcli.commands.deploy.guided_context.CompanionStackManager") @@ -98,6 +99,7 @@ def test_all_args(self, mock_deploy_context, mock_deploy_click, mock_package_con config_env=self.config_env, config_file=self.config_file, resolve_image_repos=self.resolve_image_repos, + disable_rollback=self.disable_rollback, ) mock_deploy_context.assert_called_with( @@ -121,6 +123,7 @@ def test_all_args(self, mock_deploy_context, mock_deploy_click, mock_package_con profile=self.profile, confirm_changeset=self.confirm_changeset, signing_profiles=self.signing_profiles, + disable_rollback=self.disable_rollback, ) context_mock.run.assert_called_with() @@ -159,7 +162,7 @@ def test_all_args_guided_no_to_authorization_confirmation_prompt( context_mock = Mock() mockauth_per_resource.return_value = [("HelloWorldResource1", False), ("HelloWorldResource2", False)] mock_deploy_context.return_value.__enter__.return_value = context_mock - mock_confirm.side_effect = [True, True, True, False] + mock_confirm.side_effect = [True, True, False, True, False] mock_prompt.side_effect = [ "sam-app", "us-east-1", @@ -208,6 +211,7 @@ def test_all_args_guided_no_to_authorization_confirmation_prompt( config_env=self.config_env, config_file=self.config_file, resolve_image_repos=self.resolve_image_repos, + disable_rollback=self.disable_rollback, ) @patch("samcli.commands.package.command.click") @@ -251,7 +255,7 @@ def test_all_args_guided( mock_sam_function_provider.return_value.get_all.return_value = [function_mock] mockauth_per_resource.return_value = [("HelloWorldResource", False)] mock_deploy_context.return_value.__enter__.return_value = context_mock - mock_confirm.side_effect = [True, False, True, True, True, True] + mock_confirm.side_effect = [True, False, True, True, True, True, True] mock_prompt.side_effect = [ "sam-app", "us-east-1", @@ -300,6 +304,7 @@ def test_all_args_guided( config_env=self.config_env, config_file=self.config_file, resolve_image_repos=self.resolve_image_repos, + disable_rollback=self.disable_rollback, ) mock_deploy_context.assert_called_with( @@ -323,6 +328,7 @@ def test_all_args_guided( profile=self.profile, confirm_changeset=True, signing_profiles=self.signing_profiles, + disable_rollback=True, ) context_mock.run.assert_called_with() @@ -342,6 +348,7 @@ def test_all_args_guided( stack_name="sam-app", s3_prefix="sam-app", signing_profiles=self.signing_profiles, + disable_rollback=True, ) mock_managed_stack.assert_called_with(profile=self.profile, region="us-east-1") self.assertEqual(context_mock.run.call_count, 1) @@ -406,7 +413,7 @@ def test_all_args_guided_no_save_echo_param_to_config( "testconfig.toml", "test-env", ] - mock_confirm.side_effect = [True, False, True, True, True, True] + mock_confirm.side_effect = [True, False, True, True, True, True, True] mock_managed_stack.return_value = "managed-s3-bucket" mock_signer_config_per_function.return_value = ({}, {}) @@ -439,6 +446,7 @@ def test_all_args_guided_no_save_echo_param_to_config( config_env=self.config_env, config_file=self.config_file, resolve_image_repos=self.resolve_image_repos, + disable_rollback=self.disable_rollback, ) mock_deploy_context.assert_called_with( @@ -466,13 +474,14 @@ def test_all_args_guided_no_save_echo_param_to_config( profile=self.profile, confirm_changeset=True, signing_profiles=self.signing_profiles, + disable_rollback=True, ) context_mock.run.assert_called_with() mock_managed_stack.assert_called_with(profile=self.profile, region="us-east-1") self.assertEqual(context_mock.run.call_count, 1) - self.assertEqual(MOCK_SAM_CONFIG.put.call_count, 8) + self.assertEqual(MOCK_SAM_CONFIG.put.call_count, 9) self.assertEqual( MOCK_SAM_CONFIG.put.call_args_list, [ @@ -482,6 +491,7 @@ def test_all_args_guided_no_save_echo_param_to_config( call(["deploy"], "parameters", "region", "us-east-1", env="test-env"), call(["deploy"], "parameters", "confirm_changeset", True, env="test-env"), call(["deploy"], "parameters", "capabilities", "CAPABILITY_IAM", env="test-env"), + call(["deploy"], "parameters", "disable_rollback", True, env="test-env"), call( ["deploy"], "parameters", @@ -557,7 +567,7 @@ def test_all_args_guided_no_params_save_config( "testconfig.toml", "test-env", ] - mock_confirm.side_effect = [True, False, True, True, True, True] + mock_confirm.side_effect = [True, False, True, True, True, True, True] mock_get_cmd_names.return_value = ["deploy"] mock_managed_stack.return_value = "managed-s3-bucket" mock_signer_config_per_function.return_value = ({}, {}) @@ -590,6 +600,7 @@ def test_all_args_guided_no_params_save_config( config_file=self.config_file, signing_profiles=self.signing_profiles, resolve_image_repos=self.resolve_image_repos, + disable_rollback=self.disable_rollback, ) mock_deploy_context.assert_called_with( @@ -613,13 +624,14 @@ def test_all_args_guided_no_params_save_config( profile=self.profile, confirm_changeset=True, signing_profiles=self.signing_profiles, + disable_rollback=True, ) context_mock.run.assert_called_with() mock_managed_stack.assert_called_with(profile=self.profile, region="us-east-1") self.assertEqual(context_mock.run.call_count, 1) - self.assertEqual(MOCK_SAM_CONFIG.put.call_count, 8) + self.assertEqual(MOCK_SAM_CONFIG.put.call_count, 9) self.assertEqual( MOCK_SAM_CONFIG.put.call_args_list, [ @@ -629,6 +641,7 @@ def test_all_args_guided_no_params_save_config( call(["deploy"], "parameters", "region", "us-east-1", env="test-env"), call(["deploy"], "parameters", "confirm_changeset", True, env="test-env"), call(["deploy"], "parameters", "capabilities", "CAPABILITY_IAM", env="test-env"), + call(["deploy"], "parameters", "disable_rollback", True, env="test-env"), call(["deploy"], "parameters", "parameter_overrides", 'a="b"', env="test-env"), call( ["deploy"], @@ -687,7 +700,7 @@ def test_all_args_guided_no_params_no_save_config( "us-east-1", ("CAPABILITY_IAM",), ] - mock_confirm.side_effect = [True, False, True, False, True, True] + mock_confirm.side_effect = [True, True, False, True, False, True, True] mock_managed_stack.return_value = "managed-s3-bucket" mock_signer_config_per_function.return_value = ({}, {}) @@ -722,6 +735,7 @@ def test_all_args_guided_no_params_no_save_config( config_env=self.config_env, signing_profiles=self.signing_profiles, resolve_image_repos=self.resolve_image_repos, + disable_rollback=self.disable_rollback, ) mock_deploy_context.assert_called_with( @@ -745,6 +759,7 @@ def test_all_args_guided_no_params_no_save_config( profile=self.profile, confirm_changeset=True, signing_profiles=self.signing_profiles, + disable_rollback=self.disable_rollback, ) context_mock.run.assert_called_with() @@ -792,6 +807,7 @@ def test_all_args_resolve_s3( config_env=self.config_env, signing_profiles=self.signing_profiles, resolve_image_repos=self.resolve_image_repos, + disable_rollback=self.disable_rollback, ) mock_deploy_context.assert_called_with( @@ -815,6 +831,7 @@ def test_all_args_resolve_s3( profile=self.profile, confirm_changeset=self.confirm_changeset, signing_profiles=self.signing_profiles, + disable_rollback=self.disable_rollback, ) context_mock.run.assert_called_with() @@ -850,6 +867,7 @@ def test_resolve_s3_and_s3_bucket_both_set(self): config_env=self.config_env, signing_profiles=self.signing_profiles, resolve_image_repos=self.resolve_image_repos, + disable_rollback=self.disable_rollback, ) @patch("samcli.commands.package.command.click") @@ -899,6 +917,7 @@ def test_all_args_resolve_image_repos( config_env=self.config_env, signing_profiles=self.signing_profiles, resolve_image_repos=True, + disable_rollback=self.disable_rollback, ) mock_deploy_context.assert_called_with( @@ -922,6 +941,7 @@ def test_all_args_resolve_image_repos( profile=self.profile, confirm_changeset=self.confirm_changeset, signing_profiles=self.signing_profiles, + disable_rollback=self.disable_rollback, ) context_mock.run.assert_called_with() diff --git a/tests/unit/commands/deploy/test_deploy_context.py b/tests/unit/commands/deploy/test_deploy_context.py index fdc2b49d7f1b..e659c3fc5781 100644 --- a/tests/unit/commands/deploy/test_deploy_context.py +++ b/tests/unit/commands/deploy/test_deploy_context.py @@ -31,6 +31,7 @@ def setUp(self): profile=None, confirm_changeset=False, signing_profiles=None, + disable_rollback=False, ) def test_template_improper(self): diff --git a/tests/unit/commands/deploy/test_guided_context.py b/tests/unit/commands/deploy/test_guided_context.py index d76012b47bb9..4d42489b72da 100644 --- a/tests/unit/commands/deploy/test_guided_context.py +++ b/tests/unit/commands/deploy/test_guided_context.py @@ -21,6 +21,7 @@ def setUp(self): region="region", image_repository=None, image_repositories={"RandomFunction": "image-repo"}, + disable_rollback=False, ) self.unreferenced_repo_mock = MagicMock() @@ -72,7 +73,7 @@ def test_guided_prompts_check_defaults_non_public_resources_zips( patched_auth_per_resource.return_value = [ ("HelloWorldFunction", True), ] - patched_confirm.side_effect = [True, False, "", True, True, True] + patched_confirm.side_effect = [True, False, False, "", True, True, True] patched_manage_stack.return_value = "managed_s3_stack" patched_signer_config_per_function.return_value = ({}, {}) self.gc.guided_prompts(parameter_override_keys=None) @@ -80,6 +81,7 @@ def test_guided_prompts_check_defaults_non_public_resources_zips( expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call(f"\t{self.gc.start_bold}Save arguments to configuration file{self.gc.end_bold}", default=True), call( f"\t {self.gc.start_bold}Delete the unreferenced repositories listed above when deploying?{self.gc.end_bold}", @@ -121,13 +123,14 @@ def test_guided_prompts_check_defaults_public_resources_zips( patched_get_buildable_stacks.return_value = (Mock(), []) # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, False, True, True] + patched_confirm.side_effect = [True, False, False, True, False, True, True] patched_manage_stack.return_value = "managed_s3_stack" self.gc.guided_prompts(parameter_override_keys=None) # Now to check for all the defaults on confirmations. expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call( f"\t{self.gc.start_bold}HelloWorldFunction may not have authorization defined, Is this okay?{self.gc.end_bold}", default=False, @@ -185,13 +188,14 @@ def test_guided_prompts_check_defaults_public_resources_images( ] # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, False, True, True] + patched_confirm.side_effect = [True, False, False, True, False, True, True] patched_manage_stack.return_value = "managed_s3_stack" self.gc.guided_prompts(parameter_override_keys=None) # Now to check for all the defaults on confirmations. expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call( f"\t{self.gc.start_bold}HelloWorldFunction may not have authorization defined, Is this okay?{self.gc.end_bold}", default=False, @@ -221,6 +225,7 @@ def test_guided_prompts_check_defaults_public_resources_images( expected_click_secho_calls = [ call("\t#Shows you resources changes to be deployed and require a 'Y' to initiate deploy"), call("\t#SAM needs permission to be able to create roles to connect to the resources in your template"), + call("\t#Preserves the state of previously provisioned resources when an operation fails"), ] self.assertEqual(expected_click_secho_calls, patched_click_secho.call_args_list) @@ -257,7 +262,7 @@ def test_guided_prompts_check_defaults_public_resources_images_ecr_url( ] # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, False, True, True] + patched_confirm.side_effect = [True, False, False, True, False, True, True] patched_manage_stack.return_value = "managed_s3_stack" patched_signer_config_per_function.return_value = ({}, {}) self.gc.guided_prompts(parameter_override_keys=None) @@ -265,6 +270,7 @@ def test_guided_prompts_check_defaults_public_resources_images_ecr_url( expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call( f"\t{self.gc.start_bold}HelloWorldFunction may not have authorization defined, Is this okay?{self.gc.end_bold}", default=False, @@ -292,6 +298,7 @@ def test_guided_prompts_check_defaults_public_resources_images_ecr_url( expected_click_secho_calls = [ call("\t#Shows you resources changes to be deployed and require a 'Y' to initiate deploy"), call("\t#SAM needs permission to be able to create roles to connect to the resources in your template"), + call("\t#Preserves the state of previously provisioned resources when an operation fails"), ] self.assertEqual(expected_click_secho_calls, patched_click_secho.call_args_list) @@ -328,7 +335,7 @@ def test_guided_prompts_images_illegal_image_uri( ] # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, False, False, True] + patched_confirm.side_effect = [True, False, False, True, False, False, True] patched_manage_stack.return_value = "managed_s3_stack" patched_signer_config_per_function.return_value = ({}, {}) with self.assertRaises(GuidedDeployFailedError): @@ -371,7 +378,7 @@ def test_guided_prompts_images_missing_repo( ] # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, False, True, True] + patched_confirm.side_effect = [True, False, False, True, False, True, True] patched_manage_stack.return_value = "managed_s3_stack" patched_signer_config_per_function.return_value = ({}, {}) @@ -380,6 +387,7 @@ def test_guided_prompts_images_missing_repo( expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call( f"\t{self.gc.start_bold}HelloWorldFunction may not have authorization defined, Is this okay?{self.gc.end_bold}", default=False, @@ -407,6 +415,7 @@ def test_guided_prompts_images_missing_repo( expected_click_secho_calls = [ call("\t#Shows you resources changes to be deployed and require a 'Y' to initiate deploy"), call("\t#SAM needs permission to be able to create roles to connect to the resources in your template"), + call("\t#Preserves the state of previously provisioned resources when an operation fails"), ] self.assertEqual(expected_click_secho_calls, patched_click_secho.call_args_list) @@ -444,7 +453,7 @@ def test_guided_prompts_images_no_repo( ] # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, False, False, True] + patched_confirm.side_effect = [True, False, False, True, False, False, True] patched_manage_stack.return_value = "managed_s3_stack" patched_signer_config_per_function.return_value = ({}, {}) @@ -453,6 +462,7 @@ def test_guided_prompts_images_no_repo( expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call( f"\t{self.gc.start_bold}HelloWorldFunction may not have authorization defined, Is this okay?{self.gc.end_bold}", default=False, @@ -484,6 +494,7 @@ def test_guided_prompts_images_no_repo( expected_click_secho_calls = [ call("\t#Shows you resources changes to be deployed and require a 'Y' to initiate deploy"), call("\t#SAM needs permission to be able to create roles to connect to the resources in your template"), + call("\t#Preserves the state of previously provisioned resources when an operation fails"), ] self.assertEqual(expected_click_secho_calls, patched_click_secho.call_args_list) @@ -520,7 +531,7 @@ def test_guided_prompts_images_deny_deletion( ] # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, False, True, False] + patched_confirm.side_effect = [True, False, False, True, False, True, False] patched_manage_stack.return_value = "managed_s3_stack" patched_signer_config_per_function.return_value = ({}, {}) with self.assertRaises(GuidedDeployFailedError): @@ -560,7 +571,7 @@ def test_guided_prompts_images_blank_image_repository( ] # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, False, False, True] + patched_confirm.side_effect = [True, False, False, True, False, False, True] patched_manage_stack.return_value = "managed_s3_stack" patched_signer_config_per_function.return_value = ({}, {}) with self.assertRaises(GuidedDeployFailedError): @@ -602,12 +613,13 @@ def test_guided_prompts_with_given_capabilities( patched_get_buildable_stacks.return_value = (Mock(), []) self.gc.capabilities = given_capabilities # Series of inputs to confirmations so that full range of questions are asked. - patched_confirm.side_effect = [True, False, "", True, True, True] + patched_confirm.side_effect = [True, False, False, "", True, True, True] self.gc.guided_prompts(parameter_override_keys=None) # Now to check for all the defaults on confirmations. expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call(f"\t{self.gc.start_bold}Save arguments to configuration file{self.gc.end_bold}", default=True), call( f"\t {self.gc.start_bold}Delete the unreferenced repositories listed above when deploying?{self.gc.end_bold}", @@ -647,13 +659,14 @@ def test_guided_prompts_check_configuration_file_prompt_calls( patched_signer_config_per_function.return_value = ({}, {}) # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, True, True, True] + patched_confirm.side_effect = [True, False, False, True, True, True, True] patched_manage_stack.return_value = "managed_s3_stack" self.gc.guided_prompts(parameter_override_keys=None) # Now to check for all the defaults on confirmations. expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call( f"\t{self.gc.start_bold}HelloWorldFunction may not have authorization defined, Is this okay?{self.gc.end_bold}", default=False, @@ -704,7 +717,7 @@ def test_guided_prompts_check_parameter_from_template( patched_get_buildable_stacks.return_value = (Mock(), []) # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, False, True, True] + patched_confirm.side_effect = [True, False, False, True, False, True, True] patched_manage_stack.return_value = "managed_s3_stack" patched_signer_config_per_function.return_value = ({}, {}) parameter_override_from_template = {"MyTestKey": {"Default": "MyTemplateDefaultVal"}} @@ -714,6 +727,7 @@ def test_guided_prompts_check_parameter_from_template( expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call( f"\t{self.gc.start_bold}HelloWorldFunction may not have authorization defined, Is this okay?{self.gc.end_bold}", default=False, @@ -759,7 +773,7 @@ def test_guided_prompts_check_parameter_from_cmd_or_config( patched_get_buildable_stacks.return_value = (Mock(), []) # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, False, True, True] + patched_confirm.side_effect = [True, False, False, True, False, True, True] patched_signer_config_per_function.return_value = ({}, {}) patched_manage_stack.return_value = "managed_s3_stack" parameter_override_from_template = {"MyTestKey": {"Default": "MyTemplateDefaultVal"}} @@ -769,6 +783,7 @@ def test_guided_prompts_check_parameter_from_cmd_or_config( expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call( f"\t{self.gc.start_bold}HelloWorldFunction may not have authorization defined, Is this okay?{self.gc.end_bold}", default=False, @@ -829,12 +844,13 @@ def test_guided_prompts_with_code_signing( patched_signer_config_per_function.return_value = given_code_signing_configs patched_get_buildable_stacks.return_value = (Mock(), []) # Series of inputs to confirmations so that full range of questions are asked. - patched_confirm.side_effect = [True, False, given_sign_packages_flag, "", True, True, True] + patched_confirm.side_effect = [True, False, False, given_sign_packages_flag, "", True, True, True] self.gc.guided_prompts(parameter_override_keys=None) # Now to check for all the defaults on confirmations. expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call( f"\t{self.gc.start_bold}Do you want to sign your code?{self.gc.end_bold}", default=True, @@ -895,7 +911,7 @@ def test_guided_prompts_check_default_config_region( patched_get_buildable_stacks.return_value = (Mock(), []) # Series of inputs to confirmations so that full range of questions are asked. patchedauth_per_resource.return_value = [("HelloWorldFunction", False)] - patched_confirm.side_effect = [True, False, True, True, True, True] + patched_confirm.side_effect = [True, False, False, True, True, True, True] patched_signer_config_per_function.return_value = ({}, {}) patched_manage_stack.return_value = "managed_s3_stack" patched_get_default_aws_region.return_value = "default_config_region" @@ -906,6 +922,7 @@ def test_guided_prompts_check_default_config_region( expected_confirmation_calls = [ call(f"\t{self.gc.start_bold}Confirm changes before deploy{self.gc.end_bold}", default=True), call(f"\t{self.gc.start_bold}Allow SAM CLI IAM role creation{self.gc.end_bold}", default=True), + call(f"\t{self.gc.start_bold}Disable rollback{self.gc.end_bold}", default=False), call( f"\t{self.gc.start_bold}HelloWorldFunction may not have authorization defined, Is this okay?{self.gc.end_bold}", default=False, diff --git a/tests/unit/commands/samconfig/test_samconfig.py b/tests/unit/commands/samconfig/test_samconfig.py index be7372b81632..8c0d83b5299b 100644 --- a/tests/unit/commands/samconfig/test_samconfig.py +++ b/tests/unit/commands/samconfig/test_samconfig.py @@ -562,6 +562,7 @@ def test_deploy(self, do_cli_mock, get_template_artifacts_format_mock): "confirm_changeset": True, "region": "myregion", "signing_profiles": "function=profile:owner", + "disable_rollback": True, } with samconfig_parameters(["deploy"], self.scratch_dir, **config_values) as config_path: @@ -606,6 +607,7 @@ def test_deploy(self, do_cli_mock, get_template_artifacts_format_mock): "samconfig.toml", "default", False, + True, ) @patch("samcli.commands.deploy.command.do_cli") @@ -671,6 +673,7 @@ def test_deploy_different_parameter_override_format(self, do_cli_mock, get_templ "confirm_changeset": True, "region": "myregion", "signing_profiles": "function=profile:owner", + "disable_rollback": True, } with samconfig_parameters(["deploy"], self.scratch_dir, **config_values) as config_path: @@ -715,6 +718,7 @@ def test_deploy_different_parameter_override_format(self, do_cli_mock, get_templ "samconfig.toml", "default", False, + True, ) @patch("samcli.commands.logs.command.do_cli") diff --git a/tests/unit/lib/deploy/test_deployer.py b/tests/unit/lib/deploy/test_deployer.py index a0b29efe5d79..667e52df9680 100644 --- a/tests/unit/lib/deploy/test_deployer.py +++ b/tests/unit/lib/deploy/test_deployer.py @@ -322,15 +322,17 @@ def test_wait_for_changeset_exception_ChangeEmpty(self): self.deployer.wait_for_changeset("test-id", "test-stack") def test_execute_changeset(self): - self.deployer.execute_changeset("id", "test") - self.deployer._client.execute_change_set.assert_called_with(ChangeSetName="id", StackName="test") + self.deployer.execute_changeset("id", "test", True) + self.deployer._client.execute_change_set.assert_called_with( + ChangeSetName="id", StackName="test", DisableRollback=True + ) def test_execute_changeset_exception(self): self.deployer._client.execute_change_set = MagicMock( side_effect=ClientError(error_response={"Error": {"Message": "Error"}}, operation_name="execute_changeset") ) with self.assertRaises(DeployFailedError): - self.deployer.execute_changeset("id", "test") + self.deployer.execute_changeset("id", "test", True) def test_get_last_event_time(self): timestamp = datetime.utcnow() @@ -593,30 +595,32 @@ def test_describe_stack_events_resume_after_exceptions(self, patched_time, patch self.assertEqual(patched_math.pow.call_args_list, [call(2, 1), call(2, 2), call(2, 3)]) def test_check_stack_status(self): - self.assertEqual(self.deployer._check_stack_complete("CREATE_COMPLETE"), True) - self.assertEqual(self.deployer._check_stack_complete("CREATE_FAILED"), False) - self.assertEqual(self.deployer._check_stack_complete("CREATE_IN_PROGRESS"), False) - self.assertEqual(self.deployer._check_stack_complete("DELETE_COMPLETE"), True) - self.assertEqual(self.deployer._check_stack_complete("DELETE_FAILED"), False) - self.assertEqual(self.deployer._check_stack_complete("DELETE_IN_PROGRESS"), False) - self.assertEqual(self.deployer._check_stack_complete("REVIEW_IN_PROGRESS"), False) - self.assertEqual(self.deployer._check_stack_complete("ROLLBACK_COMPLETE"), True) - self.assertEqual(self.deployer._check_stack_complete("ROLLBACK_IN_PROGRESS"), False) - self.assertEqual(self.deployer._check_stack_complete("UPDATE_COMPLETE"), True) - self.assertEqual(self.deployer._check_stack_complete("UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"), False) - self.assertEqual(self.deployer._check_stack_complete("UPDATE_IN_PROGRESS"), False) - self.assertEqual(self.deployer._check_stack_complete("UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS"), False) - self.assertEqual(self.deployer._check_stack_complete("UPDATE_ROLLBACK_FAILED"), False) - self.assertEqual(self.deployer._check_stack_complete("UPDATE_ROLLBACK_IN_PROGRESS"), False) + self.assertEqual(self.deployer._check_stack_not_in_progress("CREATE_COMPLETE"), True) + self.assertEqual(self.deployer._check_stack_not_in_progress("CREATE_FAILED"), True) + self.assertEqual(self.deployer._check_stack_not_in_progress("CREATE_IN_PROGRESS"), False) + self.assertEqual(self.deployer._check_stack_not_in_progress("DELETE_COMPLETE"), True) + self.assertEqual(self.deployer._check_stack_not_in_progress("DELETE_FAILED"), True) + self.assertEqual(self.deployer._check_stack_not_in_progress("DELETE_IN_PROGRESS"), False) + self.assertEqual(self.deployer._check_stack_not_in_progress("REVIEW_IN_PROGRESS"), False) + self.assertEqual(self.deployer._check_stack_not_in_progress("ROLLBACK_COMPLETE"), True) + self.assertEqual(self.deployer._check_stack_not_in_progress("ROLLBACK_IN_PROGRESS"), False) + self.assertEqual(self.deployer._check_stack_not_in_progress("UPDATE_COMPLETE"), True) + self.assertEqual(self.deployer._check_stack_not_in_progress("UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"), False) + self.assertEqual(self.deployer._check_stack_not_in_progress("UPDATE_IN_PROGRESS"), False) + self.assertEqual( + self.deployer._check_stack_not_in_progress("UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS"), False + ) + self.assertEqual(self.deployer._check_stack_not_in_progress("UPDATE_ROLLBACK_FAILED"), True) + self.assertEqual(self.deployer._check_stack_not_in_progress("UPDATE_ROLLBACK_IN_PROGRESS"), False) @patch("time.sleep") def test_wait_for_execute(self, patched_time): self.deployer.describe_stack_events = MagicMock() self.deployer._client.get_waiter = MagicMock(return_value=MockCreateUpdateWaiter()) - self.deployer.wait_for_execute("test", "CREATE") - self.deployer.wait_for_execute("test", "UPDATE") + self.deployer.wait_for_execute("test", "CREATE", False) + self.deployer.wait_for_execute("test", "UPDATE", True) with self.assertRaises(RuntimeError): - self.deployer.wait_for_execute("test", "DESTRUCT") + self.deployer.wait_for_execute("test", "DESTRUCT", False) self.deployer._client.get_waiter = MagicMock( return_value=MockCreateUpdateWaiter( @@ -628,7 +632,7 @@ def test_wait_for_execute(self, patched_time): ) ) with self.assertRaises(DeployFailedError): - self.deployer.wait_for_execute("test", "CREATE") + self.deployer.wait_for_execute("test", "CREATE", False) def test_create_and_wait_for_changeset(self): self.deployer.create_changeset = MagicMock(return_value=({"Id": "test"}, "create")) @@ -729,7 +733,7 @@ def test_wait_for_execute_no_outputs(self, patched_time): self.deployer._client.get_waiter = MagicMock(return_value=MockCreateUpdateWaiter()) self.deployer._display_stack_outputs = MagicMock() self.deployer.get_stack_outputs = MagicMock(return_value=None) - self.deployer.wait_for_execute("test", "CREATE") + self.deployer.wait_for_execute("test", "CREATE", False) self.assertEqual(self.deployer._display_stack_outputs.call_count, 0) @patch("time.sleep") @@ -748,5 +752,5 @@ def test_wait_for_execute_with_outputs(self, patched_time): self.deployer._client.get_waiter = MagicMock(return_value=MockCreateUpdateWaiter()) self.deployer._display_stack_outputs = MagicMock() self.deployer.get_stack_outputs = MagicMock(return_value=outputs["Stacks"][0]["Outputs"]) - self.deployer.wait_for_execute("test", "CREATE") + self.deployer.wait_for_execute("test", "CREATE", False) self.assertEqual(self.deployer._display_stack_outputs.call_count, 1)