diff --git a/samcli/commands/delete/command.py b/samcli/commands/delete/command.py index 412e4fc76f..266d093a36 100644 --- a/samcli/commands/delete/command.py +++ b/samcli/commands/delete/command.py @@ -5,23 +5,21 @@ import logging import click -from samcli.cli.cli_config_file import TomlProvider, configuration_option from samcli.cli.main import aws_creds_options, common_options, pass_context, print_cmdline_args from samcli.lib.utils.version_checker import check_newer_version -SHORT_HELP = "Delete an AWS SAM application." +SHORT_HELP = "Delete an AWS SAM application and the artifacts created by sam deploy." -HELP_TEXT = """The sam delete command deletes a Cloudformation Stack and deletes all your resources which were created. +HELP_TEXT = """The sam delete command deletes the CloudFormation +stack and all the artifacts which were created using sam deploy. \b -e.g. sam delete --stack-name sam-app --region us-east-1 +e.g. sam delete \b """ -CONFIG_SECTION = "parameters" -CONFIG_COMMAND = "deploy" LOG = logging.getLogger(__name__) @@ -31,12 +29,34 @@ context_settings={"ignore_unknown_options": False, "allow_interspersed_args": True, "allow_extra_args": True}, help=HELP_TEXT, ) -@configuration_option(provider=TomlProvider(section=CONFIG_SECTION, cmd_names=[CONFIG_COMMAND])) @click.option( "--stack-name", required=False, help="The name of the AWS CloudFormation stack you want to delete. ", ) +@click.option( + "--config-file", + help=( + "The path and file name of the configuration file containing default parameter values to use. " + "Its default value is 'samconfig.toml' in project directory. For more information about configuration files, " + "see: " + "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html." + ), + type=click.STRING, + default="samconfig.toml", + show_default=True, +) +@click.option( + "--config-env", + help=( + "The environment name specifying the default parameter values in the configuration file to use. " + "Its default value is 'default'. For more information about configuration files, see: " + "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html." + ), + type=click.STRING, + default="default", + show_default=True, +) @aws_creds_options @common_options @pass_context @@ -44,28 +64,27 @@ @print_cmdline_args def cli( ctx, - stack_name, - config_file, - config_env, + stack_name: str, + config_file: str, + config_env: str, ): """ `sam delete` command entry point """ # All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing - do_cli(stack_name, ctx.region, ctx.profile) # pragma: no cover + do_cli( + stack_name=stack_name, region=ctx.region, config_file=config_file, config_env=config_env, profile=ctx.profile + ) # pragma: no cover -def do_cli(stack_name, region, profile): +def do_cli(stack_name: str, region: str, config_file: str, config_env: str, profile: str): """ Implementation of the ``cli`` method """ from samcli.commands.delete.delete_context import DeleteContext - ctx = click.get_current_context() - s3_bucket = ctx.default_map.get("s3_bucket", None) - s3_prefix = ctx.default_map.get("s3_prefix", None) with DeleteContext( - stack_name=stack_name, region=region, s3_bucket=s3_bucket, s3_prefix=s3_prefix, profile=profile + stack_name=stack_name, region=region, profile=profile, config_file=config_file, config_env=config_env ) as delete_context: delete_context.run() diff --git a/samcli/commands/delete/delete_context.py b/samcli/commands/delete/delete_context.py index 6d470438e1..9b1d5210b5 100644 --- a/samcli/commands/delete/delete_context.py +++ b/samcli/commands/delete/delete_context.py @@ -4,15 +4,16 @@ import boto3 + import docker import click from click import confirm from click import prompt - +from samcli.cli.cli_config_file import TomlProvider from samcli.lib.utils.botoconfig import get_boto_config_with_user_agent from samcli.lib.delete.cf_utils import CfUtils -from samcli.lib.delete.utils import get_cf_template_name from samcli.lib.package.s3_uploader import S3Uploader +from samcli.lib.package.artifact_exporter import mktempfile, get_cf_template_name from samcli.yamlhelper import yaml_parse @@ -20,17 +21,20 @@ from samcli.lib.package.ecr_uploader import ECRUploader from samcli.lib.package.uploaders import Uploaders +CONFIG_COMMAND = "deploy" +CONFIG_SECTION = "parameters" +TEMPLATE_STAGE = "Original" class DeleteContext: - def __init__(self, stack_name, region, s3_bucket, s3_prefix, profile): + def __init__(self, stack_name: str, region: str, profile: str, config_file: str, config_env: str): self.stack_name = stack_name self.region = region self.profile = profile - self.s3_bucket = s3_bucket - self.s3_prefix = s3_prefix + self.config_file = config_file + self.config_env = config_env + self.s3_bucket = None + self.s3_prefix = None self.cf_utils = None - self.start_bold = "\033[1m" - self.end_bold = "\033[0m" self.s3_uploader = None self.uploaders = None self.cf_template_file_name = None @@ -38,26 +42,90 @@ def __init__(self, stack_name, region, s3_bucket, s3_prefix, profile): self.delete_cf_template_file = None def __enter__(self): + self.parse_config_file() + if not self.stack_name: + self.stack_name = prompt( + click.style("\tEnter stack name you want to delete:", bold=True), type=click.STRING + ) + return self def __exit__(self, *args): pass - def run(self): + def parse_config_file(self): """ - Delete the stack based on the argument provided by customers and samconfig.toml. + Read the provided config file if it exists and assign the options values. """ - if not self.stack_name: - self.stack_name = prompt( - f"\t{self.start_bold}Enter stack name you want to delete{self.end_bold}", type=click.STRING + toml_provider = TomlProvider(CONFIG_SECTION, [CONFIG_COMMAND]) + config_options = toml_provider( + config_path=self.config_file, config_env=self.config_env, cmd_names=[CONFIG_COMMAND] + ) + if config_options: + if not self.stack_name: + self.stack_name = config_options.get("stack_name", None) + if self.stack_name == config_options["stack_name"]: + if not self.region: + self.region = config_options.get("region", None) + if not self.profile: + self.profile = config_options.get("profile", None) + self.s3_bucket = config_options.get("s3_bucket", None) + self.s3_prefix = config_options.get("s3_prefix", None) + + def delete(self): + """ + Delete method calls for Cloudformation stacks and S3 and ECR artifacts + """ + template = self.cf_utils.get_stack_template(self.stack_name, TEMPLATE_STAGE) + template_str = template.get("TemplateBody", None) + template_dict = yaml_parse(template_str) + + if self.s3_bucket and self.s3_prefix and template_str: + self.delete_artifacts_folder = confirm( + click.style( + "\tAre you sure you want to delete the folder" + + f" {self.s3_prefix} in S3 which contains the artifacts?", + bold=True, + ), + default=False, ) + if not self.delete_artifacts_folder: + with mktempfile() as temp_file: + self.cf_template_file_name = get_cf_template_name(temp_file, template_str, "template") + self.delete_cf_template_file = confirm( + click.style( + "\tDo you want to delete the template file" + f" {self.cf_template_file_name} in S3?", bold=True + ), + default=False, + ) + + # Delete the primary stack + self.cf_utils.delete_stack(stack_name=self.stack_name) + self.cf_utils.wait_for_delete(self.stack_name) + + click.echo(f"\n\t- Deleting Cloudformation stack {self.stack_name}") + + # Delete the artifacts + template = Template(None, None, self.uploaders, None) + template.delete(template_dict) + + # Delete the CF template file in S3 + if self.delete_cf_template_file: + self.s3_uploader.delete_artifact(remote_path=self.cf_template_file_name) + + # Delete the folder of artifacts if s3_bucket and s3_prefix provided + elif self.delete_artifacts_folder: + self.s3_uploader.delete_prefix_artifacts() - if not self.region: - self.region = prompt( - f"\t{self.start_bold}Enter region you want to delete from{self.end_bold}", type=click.STRING - ) + def run(self): + """ + Delete the stack based on the argument provided by customers and samconfig.toml. + """ delete_stack = confirm( - f"\t{self.start_bold}Are you sure you want to delete the stack {self.stack_name}?{self.end_bold}", + click.style( + f"\tAre you sure you want to delete the stack {self.stack_name}" + f" in the region {self.region} ?", + bold=True, + ), default=False, ) # Fetch the template using the stack-name @@ -76,53 +144,14 @@ def run(self): docker_client = docker.from_env() ecr_uploader = ECRUploader(docker_client, ecr_client, None, None) - + + self.uploaders = Uploaders(self.s3_uploader, ecr_uploader) self.cf_utils = CfUtils(cloudformation_client) - is_deployed = self.cf_utils.has_stack(self.stack_name) + is_deployed = self.cf_utils.has_stack(stack_name=self.stack_name) if is_deployed: - template_str = self.cf_utils.get_stack_template(self.stack_name, "Original") - - template_dict = yaml_parse(template_str) - - if self.s3_bucket and self.s3_prefix: - self.delete_artifacts_folder = confirm( - f"\t{self.start_bold}Are you sure you want to delete the folder" - + f" {self.s3_prefix} in S3 which contains the artifacts?{self.end_bold}", - default=False, - ) - if not self.delete_artifacts_folder: - self.cf_template_file_name = get_cf_template_name(template_str, "template") - self.delete_cf_template_file = confirm( - f"\t{self.start_bold}Do you want to delete the template file" - + f" {self.cf_template_file_name} in S3?{self.end_bold}", - default=False, - ) - - click.echo("\n") - # Delete the primary stack - click.echo("- deleting Cloudformation stack {0}".format(self.stack_name)) - self.cf_utils.delete_stack(self.stack_name) - self.cf_utils.wait_for_delete(self.stack_name) - - - # Delete the artifacts - self.uploaders = Uploaders(self.s3_uploader, ecr_uploader) - template = Template(None, None, self.uploaders, None) - template.delete(template_dict) - - # Delete the CF template file in S3 - if self.delete_cf_template_file: - self.s3_uploader.delete_artifact(self.cf_template_file_name) - - # Delete the folder of artifacts if s3_bucket and s3_prefix provided - elif self.delete_artifacts_folder: - self.s3_uploader.delete_prefix_artifacts() - - # Delete the ECR companion stack - - click.echo("\n") - click.echo("delete complete") + self.delete() + click.echo("\nDeleted successfully") else: - click.echo("Error: The input stack {0} does not exist on Cloudformation".format(self.stack_name)) + click.echo(f"Error: The input stack {self.stack_name} does not exist on Cloudformation") diff --git a/samcli/commands/delete/exceptions.py b/samcli/commands/delete/exceptions.py index 82c56b6bb6..7e2ba5105c 100644 --- a/samcli/commands/delete/exceptions.py +++ b/samcli/commands/delete/exceptions.py @@ -12,3 +12,13 @@ def __init__(self, stack_name, msg): message_fmt = "Failed to delete the stack: {stack_name}, {msg}" super().__init__(message=message_fmt.format(stack_name=self.stack_name, msg=msg)) + + +class FetchTemplateFailedError(UserException): + def __init__(self, stack_name, msg): + self.stack_name = stack_name + self.msg = msg + + message_fmt = "Failed to fetch the template for the stack: {stack_name}, {msg}" + + super().__init__(message=message_fmt.format(stack_name=self.stack_name, msg=msg)) diff --git a/samcli/lib/delete/cf_utils.py b/samcli/lib/delete/cf_utils.py index 8e257a0cd9..40d3b58183 100644 --- a/samcli/lib/delete/cf_utils.py +++ b/samcli/lib/delete/cf_utils.py @@ -4,8 +4,11 @@ import logging + +from typing import Dict from botocore.exceptions import ClientError, BotoCoreError, WaiterError -from samcli.commands.delete.exceptions import DeleteFailedError +from samcli.commands.delete.exceptions import DeleteFailedError, FetchTemplateFailedError + LOG = logging.getLogger(__name__) @@ -14,7 +17,7 @@ class CfUtils: def __init__(self, cloudformation_client): self._client = cloudformation_client - def has_stack(self, stack_name): + def has_stack(self, stack_name: str) -> bool: """ Checks if a CloudFormation stack with given name exists @@ -27,7 +30,11 @@ def has_stack(self, stack_name): return False stack = resp["Stacks"][0] - return stack["StackStatus"] != "REVIEW_IN_PROGRESS" + # Note: Stacks with REVIEW_IN_PROGRESS can be deleted + # using delete_stack but get_template does not return + # the template_str for this stack restricting deletion of + # artifacts. + return bool(stack["StackStatus"] != "REVIEW_IN_PROGRESS") except ClientError as e: # If a stack does not exist, describe_stacks will throw an @@ -37,20 +44,21 @@ def has_stack(self, stack_name): if "Stack with id {0} does not exist".format(stack_name) in str(e): LOG.debug("Stack with id %s does not exist", stack_name) return False + LOG.error("ClientError Exception : %s", str(e)) raise DeleteFailedError(stack_name=stack_name, msg=str(e)) from e except BotoCoreError as e: # If there are credentials, environment errors, # catch that and throw a delete failed error. - LOG.debug("Botocore Exception : %s", str(e)) + LOG.error("Botocore Exception : %s", str(e)) raise DeleteFailedError(stack_name=stack_name, msg=str(e)) from e except Exception as e: # We don't know anything about this exception. Don't handle - LOG.debug("Unable to get stack details.", exc_info=e) + LOG.error("Unable to get stack details.", exc_info=e) raise e - def get_stack_template(self, stack_name, stage): + def get_stack_template(self, stack_name: str, stage: str) -> Dict: """ Return the Cloudformation template of the given stack_name @@ -61,43 +69,40 @@ def get_stack_template(self, stack_name, stage): try: resp = self._client.get_template(StackName=stack_name, TemplateStage=stage) if not resp["TemplateBody"]: - return None - - return resp["TemplateBody"] + return {} + return dict(resp) except (ClientError, BotoCoreError) as e: # If there are credentials, environment errors, # catch that and throw a delete failed error. - LOG.debug("Failed to delete stack : %s", str(e)) - raise DeleteFailedError(stack_name=stack_name, msg=str(e)) from e + LOG.error("Failed to fetch template for the stack : %s", str(e)) + raise FetchTemplateFailedError(stack_name=stack_name, msg=str(e)) from e except Exception as e: # We don't know anything about this exception. Don't handle - LOG.debug("Unable to get stack details.", exc_info=e) + LOG.error("Unable to get stack details.", exc_info=e) raise e - def delete_stack(self, stack_name): + def delete_stack(self, stack_name: str): """ Delete the Cloudformation stack with the given stack_name :param stack_name: Name or ID of the stack - :return: Status of deletion """ try: - resp = self._client.delete_stack(StackName=stack_name) - return resp + self._client.delete_stack(StackName=stack_name) except (ClientError, BotoCoreError) as e: # If there are credentials, environment errors, # catch that and throw a delete failed error. - LOG.debug("Failed to delete stack : %s", str(e)) + LOG.error("Failed to delete stack : %s", str(e)) raise DeleteFailedError(stack_name=stack_name, msg=str(e)) from e except Exception as e: # We don't know anything about this exception. Don't handle - LOG.debug("Unable to get stack details.", exc_info=e) + LOG.error("Failed to delete stack. ", exc_info=e) raise e def wait_for_delete(self, stack_name): diff --git a/samcli/lib/delete/utils.py b/samcli/lib/delete/utils.py deleted file mode 100644 index 497610f776..0000000000 --- a/samcli/lib/delete/utils.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Utilities for Delete -""" - -from samcli.lib.utils.hash import file_checksum -from samcli.lib.package.artifact_exporter import mktempfile - - -def get_cf_template_name(template_str, extension): - with mktempfile() as temp_file: - temp_file.write(template_str) - temp_file.flush() - - filemd5 = file_checksum(temp_file.name) - remote_path = filemd5 + "." + extension - - return remote_path diff --git a/samcli/lib/deploy/deployer.py b/samcli/lib/deploy/deployer.py index 8aae03425e..eeed0fd321 100644 --- a/samcli/lib/deploy/deployer.py +++ b/samcli/lib/deploy/deployer.py @@ -34,7 +34,7 @@ ) from samcli.commands._utils.table_print import pprint_column_names, pprint_columns, newline_per_item, MIN_OFFSET from samcli.commands.deploy import exceptions as deploy_exceptions -from samcli.lib.package.artifact_exporter import mktempfile +from samcli.lib.package.artifact_exporter import mktempfile, get_cf_template_name from samcli.lib.package.s3_uploader import S3Uploader from samcli.lib.utils.time import utc_to_timestamp @@ -174,12 +174,11 @@ def create_changeset( # TemplateBody. This is required for large templates. if s3_uploader: with mktempfile() as temporary_file: - temporary_file.write(kwargs.pop("TemplateBody")) - temporary_file.flush() + remote_path = get_cf_template_name(temporary_file, kwargs.pop("TemplateBody"), "template") # TemplateUrl property requires S3 URL to be in path-style format parts = S3Uploader.parse_s3_url( - s3_uploader.upload_with_dedup(temporary_file.name, "template"), version_property="Version" + s3_uploader.upload(temporary_file.name, remote_path), version_property="Version" ) kwargs["TemplateURL"] = s3_uploader.to_path_style_s3_url(parts["Key"], parts.get("Version", None)) diff --git a/samcli/lib/package/artifact_exporter.py b/samcli/lib/package/artifact_exporter.py index a1b2a98fe4..2a8f484d87 100644 --- a/samcli/lib/package/artifact_exporter.py +++ b/samcli/lib/package/artifact_exporter.py @@ -42,6 +42,7 @@ is_local_file, mktempfile, is_s3_url, + get_cf_template_name, ) from samcli.lib.utils.packagetype import ZIP from samcli.yamlhelper import yaml_parse, yaml_dump @@ -83,10 +84,9 @@ def do_export(self, resource_id, resource_dict, parent_dir): exported_template_str = yaml_dump(exported_template_dict) with mktempfile() as temporary_file: - temporary_file.write(exported_template_str) - temporary_file.flush() - url = self.uploader.upload_with_dedup(temporary_file.name, "template") + remote_path = get_cf_template_name(temporary_file, exported_template_str, "template") + url = self.uploader.upload(temporary_file.name, remote_path) # TemplateUrl property requires S3 URL to be in path-style format parts = S3Uploader.parse_s3_url(url, version_property="Version") diff --git a/samcli/lib/package/s3_uploader.py b/samcli/lib/package/s3_uploader.py index 72ace7ee35..76b7ff1ec7 100644 --- a/samcli/lib/package/s3_uploader.py +++ b/samcli/lib/package/s3_uploader.py @@ -145,14 +145,17 @@ def upload_with_dedup( return self.upload(file_name, remote_path) - def delete_artifact(self, remote_path: str, is_key=False): + def delete_artifact(self, remote_path: str, is_key: bool = False) -> Dict: """ Deletes a given file from S3 :param remote_path: Path to the file that will be deleted :param is_key: If the given remote_path is the key or a file_name + + :return: metadata dict of the deleted object """ try: if not self.bucket_name: + LOG.error("Bucket not specified") raise BucketNotSpecifiedError() key = remote_path @@ -160,13 +163,15 @@ def delete_artifact(self, remote_path: str, is_key=False): key = "{0}/{1}".format(self.prefix, remote_path) # Deleting Specific file with key - click.echo("- deleting S3 file " + key) + click.echo(f"\t- Deleting S3 file {key}") resp = self.s3.delete_object(Bucket=self.bucket_name, Key=key) - return resp["ResponseMetadata"] + LOG.debug("S3 method delete_object is called and returned: %s", resp["ResponseMetadata"]) + return dict(resp["ResponseMetadata"]) except botocore.exceptions.ClientError as ex: error_code = ex.response["Error"]["Code"] if error_code == "NoSuchBucket": + LOG.error("Provided bucket %s does not exist ", self.bucket_name) raise NoSuchBucketError(bucket_name=self.bucket_name) from ex raise ex @@ -175,6 +180,7 @@ def delete_prefix_artifacts(self): Deletes all the files from the prefix in S3 """ if not self.bucket_name: + LOG.error("Bucket not specified") raise BucketNotSpecifiedError() if self.prefix: prefix_files = self.s3.list_objects_v2(Bucket=self.bucket_name, Prefix=self.prefix) diff --git a/samcli/lib/package/utils.py b/samcli/lib/package/utils.py index 6317c35a48..a831518805 100644 --- a/samcli/lib/package/utils.py +++ b/samcli/lib/package/utils.py @@ -19,7 +19,7 @@ from samcli.commands.package.exceptions import ImageNotFoundError from samcli.lib.package.ecr_utils import is_ecr_url from samcli.lib.package.s3_uploader import S3Uploader -from samcli.lib.utils.hash import dir_checksum +from samcli.lib.utils.hash import dir_checksum, file_checksum LOG = logging.getLogger(__name__) @@ -284,3 +284,13 @@ def copy_to_temp_dir(filepath): dst = os.path.join(tmp_dir, os.path.basename(filepath)) shutil.copyfile(filepath, dst) return tmp_dir + + +def get_cf_template_name(temp_file, template_str, extension): + temp_file.write(template_str) + temp_file.flush() + + filemd5 = file_checksum(temp_file.name) + remote_path = filemd5 + "." + extension + + return remote_path diff --git a/tests/unit/commands/delete/test_command.py b/tests/unit/commands/delete/test_command.py index a199c4e960..4e268688ee 100644 --- a/tests/unit/commands/delete/test_command.py +++ b/tests/unit/commands/delete/test_command.py @@ -36,15 +36,17 @@ def test_all_args(self, mock_delete_context, mock_delete_click): do_cli( stack_name=self.stack_name, region=self.region, + config_file=self.config_file, + config_env=self.config_env, profile=self.profile, ) mock_delete_context.assert_called_with( stack_name=self.stack_name, - s3_bucket=mock_delete_click.get_current_context().default_map.get("s3_bucket", None), - s3_prefix=mock_delete_click.get_current_context().default_map.get("s3_prefix", None), region=self.region, profile=self.profile, + config_file=self.config_file, + config_env=self.config_env, ) context_mock.run.assert_called_with() diff --git a/tests/unit/lib/delete/test_cf_utils.py b/tests/unit/lib/delete/test_cf_utils.py index b9bc00faba..90d764a5c4 100644 --- a/tests/unit/lib/delete/test_cf_utils.py +++ b/tests/unit/lib/delete/test_cf_utils.py @@ -1,8 +1,10 @@ from unittest.mock import patch, MagicMock, ANY, call from unittest import TestCase -from samcli.commands.delete.exceptions import DeleteFailedError + +from samcli.commands.delete.exceptions import DeleteFailedError, FetchTemplateFailedError from botocore.exceptions import ClientError, BotoCoreError, WaiterError + from samcli.lib.delete.cf_utils import CfUtils @@ -72,12 +74,12 @@ def test_cf_utils_get_stack_template_exception_client_error(self): operation_name="stack_status", ) ) - with self.assertRaises(DeleteFailedError): + with self.assertRaises(FetchTemplateFailedError): self.cf_utils.get_stack_template("test", "Original") def test_cf_utils_get_stack_template_exception_botocore(self): self.cf_utils._client.get_template = MagicMock(side_effect=BotoCoreError()) - with self.assertRaises(DeleteFailedError): + with self.assertRaises(FetchTemplateFailedError): self.cf_utils.get_stack_template("test", "Original") def test_cf_utils_get_stack_template_exception(self): diff --git a/tests/unit/lib/package/test_artifact_exporter.py b/tests/unit/lib/package/test_artifact_exporter.py index 7cc20f6be7..f7aceafef1 100644 --- a/tests/unit/lib/package/test_artifact_exporter.py +++ b/tests/unit/lib/package/test_artifact_exporter.py @@ -778,7 +778,7 @@ def test_export_cloudformation_stack(self, TemplateMock): TemplateMock.return_value = template_instance_mock template_instance_mock.export.return_value = exported_template_dict - self.s3_uploader_mock.upload_with_dedup.return_value = result_s3_url + self.s3_uploader_mock.upload.return_value = result_s3_url self.s3_uploader_mock.to_path_style_s3_url.return_value = result_path_style_s3_url with tempfile.NamedTemporaryFile() as handle: @@ -792,7 +792,7 @@ def test_export_cloudformation_stack(self, TemplateMock): TemplateMock.assert_called_once_with(template_path, parent_dir, self.uploaders_mock, self.code_signer_mock) template_instance_mock.export.assert_called_once_with() - self.s3_uploader_mock.upload_with_dedup.assert_called_once_with(mock.ANY, "template") + self.s3_uploader_mock.upload.assert_called_once_with(mock.ANY, mock.ANY) self.s3_uploader_mock.to_path_style_s3_url.assert_called_once_with("world", None) def test_export_cloudformation_stack_no_upload_path_is_s3url(self): @@ -805,7 +805,7 @@ def test_export_cloudformation_stack_no_upload_path_is_s3url(self): # Case 1: Path is already S3 url stack_resource.export(resource_id, resource_dict, "dir") self.assertEqual(resource_dict[property_name], s3_url) - self.s3_uploader_mock.upload_with_dedup.assert_not_called() + self.s3_uploader_mock.upload.assert_not_called() def test_export_cloudformation_stack_no_upload_path_is_httpsurl(self): stack_resource = CloudFormationStackResource(self.uploaders_mock, self.code_signer_mock) @@ -817,7 +817,7 @@ def test_export_cloudformation_stack_no_upload_path_is_httpsurl(self): # Case 1: Path is already S3 url stack_resource.export(resource_id, resource_dict, "dir") self.assertEqual(resource_dict[property_name], s3_url) - self.s3_uploader_mock.upload_with_dedup.assert_not_called() + self.s3_uploader_mock.upload.assert_not_called() def test_export_cloudformation_stack_no_upload_path_is_s3_region_httpsurl(self): stack_resource = CloudFormationStackResource(self.uploaders_mock, self.code_signer_mock) @@ -829,7 +829,7 @@ def test_export_cloudformation_stack_no_upload_path_is_s3_region_httpsurl(self): stack_resource.export(resource_id, resource_dict, "dir") self.assertEqual(resource_dict[property_name], s3_url) - self.s3_uploader_mock.upload_with_dedup.assert_not_called() + self.s3_uploader_mock.upload.assert_not_called() def test_export_cloudformation_stack_no_upload_path_is_empty(self): stack_resource = CloudFormationStackResource(self.uploaders_mock, self.code_signer_mock) @@ -842,7 +842,7 @@ def test_export_cloudformation_stack_no_upload_path_is_empty(self): resource_dict = {} stack_resource.export(resource_id, resource_dict, "dir") self.assertEqual(resource_dict, {}) - self.s3_uploader_mock.upload_with_dedup.assert_not_called() + self.s3_uploader_mock.upload.assert_not_called() def test_export_cloudformation_stack_no_upload_path_not_file(self): stack_resource = CloudFormationStackResource(self.uploaders_mock, self.code_signer_mock) @@ -855,7 +855,7 @@ def test_export_cloudformation_stack_no_upload_path_not_file(self): resource_dict = {property_name: dirname} with self.assertRaises(exceptions.ExportFailedError): stack_resource.export(resource_id, resource_dict, "dir") - self.s3_uploader_mock.upload_with_dedup.assert_not_called() + self.s3_uploader_mock.upload.assert_not_called() @patch("samcli.lib.package.artifact_exporter.Template") def test_export_serverless_application(self, TemplateMock): @@ -871,7 +871,7 @@ def test_export_serverless_application(self, TemplateMock): TemplateMock.return_value = template_instance_mock template_instance_mock.export.return_value = exported_template_dict - self.s3_uploader_mock.upload_with_dedup.return_value = result_s3_url + self.s3_uploader_mock.upload.return_value = result_s3_url self.s3_uploader_mock.to_path_style_s3_url.return_value = result_path_style_s3_url with tempfile.NamedTemporaryFile() as handle: @@ -885,7 +885,7 @@ def test_export_serverless_application(self, TemplateMock): TemplateMock.assert_called_once_with(template_path, parent_dir, self.uploaders_mock, self.code_signer_mock) template_instance_mock.export.assert_called_once_with() - self.s3_uploader_mock.upload_with_dedup.assert_called_once_with(mock.ANY, "template") + self.s3_uploader_mock.upload.assert_called_once_with(mock.ANY, mock.ANY) self.s3_uploader_mock.to_path_style_s3_url.assert_called_once_with("world", None) def test_export_serverless_application_no_upload_path_is_s3url(self): @@ -898,7 +898,7 @@ def test_export_serverless_application_no_upload_path_is_s3url(self): # Case 1: Path is already S3 url stack_resource.export(resource_id, resource_dict, "dir") self.assertEqual(resource_dict[property_name], s3_url) - self.s3_uploader_mock.upload_with_dedup.assert_not_called() + self.s3_uploader_mock.upload.assert_not_called() def test_export_serverless_application_no_upload_path_is_httpsurl(self): stack_resource = ServerlessApplicationResource(self.uploaders_mock, self.code_signer_mock) @@ -910,7 +910,7 @@ def test_export_serverless_application_no_upload_path_is_httpsurl(self): # Case 1: Path is already S3 url stack_resource.export(resource_id, resource_dict, "dir") self.assertEqual(resource_dict[property_name], s3_url) - self.s3_uploader_mock.upload_with_dedup.assert_not_called() + self.s3_uploader_mock.upload.assert_not_called() def test_export_serverless_application_no_upload_path_is_empty(self): stack_resource = ServerlessApplicationResource(self.uploaders_mock, self.code_signer_mock) @@ -921,7 +921,7 @@ def test_export_serverless_application_no_upload_path_is_empty(self): resource_dict = {} stack_resource.export(resource_id, resource_dict, "dir") self.assertEqual(resource_dict, {}) - self.s3_uploader_mock.upload_with_dedup.assert_not_called() + self.s3_uploader_mock.upload.assert_not_called() def test_export_serverless_application_no_upload_path_not_file(self): stack_resource = ServerlessApplicationResource(self.uploaders_mock, self.code_signer_mock) @@ -933,7 +933,7 @@ def test_export_serverless_application_no_upload_path_not_file(self): resource_dict = {property_name: dirname} with self.assertRaises(exceptions.ExportFailedError): stack_resource.export(resource_id, resource_dict, "dir") - self.s3_uploader_mock.upload_with_dedup.assert_not_called() + self.s3_uploader_mock.upload.assert_not_called() def test_export_serverless_application_no_upload_path_is_dictionary(self): stack_resource = ServerlessApplicationResource(self.uploaders_mock, self.code_signer_mock) @@ -945,7 +945,7 @@ def test_export_serverless_application_no_upload_path_is_dictionary(self): resource_dict = {property_name: location} stack_resource.export(resource_id, resource_dict, "dir") self.assertEqual(resource_dict[property_name], location) - self.s3_uploader_mock.upload_with_dedup.assert_not_called() + self.s3_uploader_mock.upload.assert_not_called() @patch("samcli.lib.package.artifact_exporter.yaml_parse") def test_template_export_metadata(self, yaml_parse_mock):