Skip to content

Commit

Permalink
feat: Add support for CloudFormation's DisableRollback parameter (aws…
Browse files Browse the repository at this point in the history
…#3325)

* add implementation and update unit tests

* update order of guided prompts

* update current integration tests

* add new integration tests

* lint

* remove comment

* fix test typos and remove unnecessary things

* revert boto stubs version bump because of mypy issues

* ux feedback -- change fail message

* change method name for checking stack status not in progress

* move click message to commands package

* replace click with colored log.info

* not necessary to skip test anymore
  • Loading branch information
torresxb1 committed Oct 8, 2021
1 parent 9a3aee3 commit f37b0c3
Show file tree
Hide file tree
Showing 15 changed files with 494 additions and 63 deletions.
2 changes: 1 addition & 1 deletion requirements/base.txt
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions samcli/commands/deploy/command.py
Expand Up @@ -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
Expand Down Expand Up @@ -208,6 +215,7 @@ def cli(
resolve_image_repos,
config_file,
config_env,
disable_rollback,
):
"""
`sam deploy` command entry point
Expand Down Expand Up @@ -241,6 +249,7 @@ def cli(
config_file,
config_env,
resolve_image_repos,
disable_rollback,
) # pragma: no cover


Expand Down Expand Up @@ -272,6 +281,7 @@ def do_cli(
config_file,
config_env,
resolve_image_repos,
disable_rollback,
):
"""
Implementation of the ``cli`` method
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
11 changes: 9 additions & 2 deletions samcli/commands/deploy/deploy_context.py
Expand Up @@ -70,6 +70,7 @@ def __init__(
profile,
confirm_changeset,
signing_profiles,
disable_rollback,
):
self.template_file = template_file
self.stack_name = stack_name
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -165,6 +168,7 @@ def run(self):
region,
self.fail_on_empty_changeset,
self.confirm_changeset,
self.disable_rollback,
)

def deploy(
Expand All @@ -181,6 +185,7 @@ def deploy(
region,
fail_on_empty_changeset=True,
confirm_changeset=False,
disable_rollback=False,
):
"""
Deploy the stack to cloudformation.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions samcli/commands/deploy/guided_context.py
Expand Up @@ -41,6 +41,7 @@


class GuidedContext:
# pylint: disable=too-many-statements
def __init__(
self,
template_file,
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions samcli/commands/deploy/utils.py
Expand Up @@ -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.
Expand All @@ -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'}
Expand All @@ -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()

Expand All @@ -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.
Expand Down
33 changes: 27 additions & 6 deletions samcli/lib/deploy/deployer.py
Expand Up @@ -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__)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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 "
Expand All @@ -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))

Expand Down Expand Up @@ -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
)
3 changes: 3 additions & 0 deletions tests/integration/deploy/deploy_integ_base.py
Expand Up @@ -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"]

Expand Down Expand Up @@ -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

Expand Down

0 comments on commit f37b0c3

Please sign in to comment.