From e93897a45687b084cfa69e6a7370c72549c97031 Mon Sep 17 00:00:00 2001 From: Sriram Madapusi Vasudevan <3770774+TheSriram@users.noreply.github.com> Date: Thu, 21 Nov 2019 11:08:43 -0800 Subject: [PATCH] tests: integration tests for `sam deploy` (#1565) - integration tests for `sam deploy` and `sam deploy --guided` - fix for regression tests - `stack-name` is required - additional error cases for when a s3 bucket is needed to be looked up during `sam deploy` - move parameter prompts logic to its own function --- samcli/commands/deploy/command.py | 49 +-- samcli/commands/deploy/deploy_context.py | 3 +- samcli/lib/package/s3_uploader.py | 4 + tests/integration/deploy/deploy_integ_base.py | 6 + .../integration/deploy/test_deploy_command.py | 299 +++++++++++++++++- .../deploy/test_deploy_regression.py | 6 +- 6 files changed, 339 insertions(+), 28 deletions(-) diff --git a/samcli/commands/deploy/command.py b/samcli/commands/deploy/command.py index 33eb24204c..3ce09efa68 100644 --- a/samcli/commands/deploy/command.py +++ b/samcli/commands/deploy/command.py @@ -50,8 +50,7 @@ @template_click_option(include_build=True) @click.option( "--stack-name", - required=False, - default="sam-app", + required=True, help="The name of the AWS CloudFormation stack you're deploying to. " "If you specify an existing stack, the command updates the stack. " "If you specify a new stack, the command creates it.", @@ -288,10 +287,10 @@ def do_cli( def guided_deploy( stack_name, s3_bucket, region, profile, confirm_changeset, parameter_override_keys, parameter_overrides ): + default_stack_name = stack_name or "sam-app" default_region = region or "us-east-1" default_capabilities = ("CAPABILITY_IAM",) input_capabilities = None - input_parameter_overrides = {} color = Colored() start_bold = "\033[1m" @@ -301,25 +300,9 @@ def guided_deploy( color.yellow("\n\tSetting default arguments for 'sam deploy'\n\t=========================================") ) - stack_name = click.prompt(f"\t{start_bold}Stack Name{end_bold}", default=stack_name, type=click.STRING) + stack_name = click.prompt(f"\t{start_bold}Stack Name{end_bold}", default=default_stack_name, type=click.STRING) region = click.prompt(f"\t{start_bold}AWS Region{end_bold}", default=default_region, type=click.STRING) - if parameter_override_keys: - for parameter_key, parameter_properties in parameter_override_keys.items(): - no_echo = parameter_properties.get("NoEcho", False) - if no_echo: - parameter = click.prompt( - f"\t{start_bold}Parameter {parameter_key}{end_bold}", type=click.STRING, hide_input=True - ) - input_parameter_overrides[parameter_key] = {"Value": parameter, "Hidden": True} - else: - parameter = click.prompt( - f"\t{start_bold}Parameter {parameter_key}{end_bold}", - default=parameter_overrides.get( - parameter_key, parameter_properties.get("Default", "No default specified") - ), - type=click.STRING, - ) - input_parameter_overrides[parameter_key] = {"Value": parameter, "Hidden": False} + input_parameter_overrides = prompt_parameters(parameter_override_keys, start_bold, end_bold) click.secho("\t#Shows you resources changes to be deployed and require a 'Y' to initiate deploy") confirm_changeset = click.confirm( @@ -331,7 +314,7 @@ def guided_deploy( if not capabilities_confirm: input_capabilities = click.prompt( f"\t{start_bold}Capabilities{end_bold}", - default=default_capabilities, + default=default_capabilities[0], type=FuncParamType(func=_space_separated_list_func_type), ) @@ -354,6 +337,28 @@ def guided_deploy( ) +def prompt_parameters(parameter_override_keys, start_bold, end_bold): + _prompted_param_overrides = {} + if parameter_override_keys: + for parameter_key, parameter_properties in parameter_override_keys.items(): + no_echo = parameter_properties.get("NoEcho", False) + if no_echo: + parameter = click.prompt( + f"\t{start_bold}Parameter {parameter_key}{end_bold}", type=click.STRING, hide_input=True + ) + _prompted_param_overrides[parameter_key] = {"Value": parameter, "Hidden": True} + else: + parameter = click.prompt( + f"\t{start_bold}Parameter {parameter_key}{end_bold}", + default=_prompted_param_overrides.get( + parameter_key, parameter_properties.get("Default", "No default specified") + ), + type=click.STRING, + ) + _prompted_param_overrides[parameter_key] = {"Value": parameter, "Hidden": False} + return _prompted_param_overrides + + def print_deploy_args(stack_name, s3_bucket, region, capabilities, parameter_overrides, confirm_changeset): _parameters = parameter_overrides.copy() diff --git a/samcli/commands/deploy/deploy_context.py b/samcli/commands/deploy/deploy_context.py index c482c98757..9c246f45cc 100644 --- a/samcli/commands/deploy/deploy_context.py +++ b/samcli/commands/deploy/deploy_context.py @@ -104,6 +104,7 @@ def run(self): session = boto3.Session(profile_name=self.profile if self.profile else None) cloudformation_client = session.client("cloudformation", region_name=self.region if self.region else None) + s3_client = None if self.s3_bucket: s3_client = session.client("s3", region_name=self.region if self.region else None) @@ -111,7 +112,7 @@ def run(self): self.deployer = Deployer(cloudformation_client) - region = s3_client._client_config.region_name # pylint: disable=W0212 + region = s3_client._client_config.region_name if s3_client else self.region # pylint: disable=W0212 return self.deploy( self.stack_name, diff --git a/samcli/lib/package/s3_uploader.py b/samcli/lib/package/s3_uploader.py index d89710c0df..efd7397555 100644 --- a/samcli/lib/package/s3_uploader.py +++ b/samcli/lib/package/s3_uploader.py @@ -91,6 +91,8 @@ def upload(self, file_name, remote_path): additional_args["Metadata"] = self.artifact_metadata print_progress_callback = ProgressPercentage(file_name, remote_path) + if not self.bucket_name: + raise BucketNotSpecifiedError() future = self.transfer_manager.upload( file_name, self.bucket_name, remote_path, additional_args, [print_progress_callback] ) @@ -144,6 +146,8 @@ def file_exists(self, remote_path): return False def make_url(self, obj_path): + if not self.bucket_name: + raise BucketNotSpecifiedError() return "s3://{0}/{1}".format(self.bucket_name, obj_path) def file_checksum(self, file_name): diff --git a/tests/integration/deploy/deploy_integ_base.py b/tests/integration/deploy/deploy_integ_base.py index 1e68f8878f..00165f4adf 100644 --- a/tests/integration/deploy/deploy_integ_base.py +++ b/tests/integration/deploy/deploy_integ_base.py @@ -37,6 +37,7 @@ def get_deploy_command_list( force_upload=False, notification_arns=None, fail_on_empty_changeset=False, + confirm_changeset=False, no_execute_changeset=False, parameter_overrides=None, role_arn=None, @@ -44,9 +45,12 @@ def get_deploy_command_list( tags=None, profile=None, region=None, + guided=False, ): command_list = [self.base_command(), "deploy"] + if guided: + command_list = command_list + ["--guided"] if s3_bucket: command_list = command_list + ["--s3-bucket", str(s3_bucket)] if capabilities: @@ -73,6 +77,8 @@ def get_deploy_command_list( command_list = command_list + ["--force-upload"] if fail_on_empty_changeset: command_list = command_list + ["--fail-on-empty-changeset"] + if confirm_changeset: + command_list = command_list + ["--confirm-changeset"] if tags: command_list = command_list + ["--tags", str(tags)] if region: diff --git a/tests/integration/deploy/test_deploy_command.py b/tests/integration/deploy/test_deploy_command.py index a2bfb79f68..f36872aed7 100644 --- a/tests/integration/deploy/test_deploy_command.py +++ b/tests/integration/deploy/test_deploy_command.py @@ -7,6 +7,8 @@ import boto3 from parameterized import parameterized +from samcli.lib.config.samconfig import DEFAULT_CONFIG_FILE_NAME +from samcli.lib.bootstrap.bootstrap import SAM_CLI_STACK_NAME from tests.integration.deploy.deploy_integ_base import DeployIntegBase from tests.integration.package.package_integ_base import PackageIntegBase from tests.testing_utils import RUNNING_ON_CI, RUNNING_TEST_FOR_MASTER_ON_CI @@ -30,7 +32,7 @@ def tearDown(self): super(TestDeploy, self).tearDown() @parameterized.expand(["aws-serverless-function.yaml"]) - def test_deploy_all_args(self, template_file): + def test_package_and_deploy_no_s3_bucket_all_args(self, template_file): template_path = self.test_data_path.joinpath(template_file) with tempfile.NamedTemporaryFile(delete=False) as output_template_file: # Package necessary artifacts. @@ -71,7 +73,6 @@ def test_deploy_all_args(self, template_file): stack_name=stack_name, capabilities="CAPABILITY_IAM", s3_prefix="integ_deploy", - s3_bucket=self.s3_bucket.name, force_upload=True, notification_arns=self.sns_arn, parameter_overrides="Parameter=Clarity", @@ -82,3 +83,297 @@ def test_deploy_all_args(self, template_file): deploy_process = Popen(deploy_command_list_execute, stdout=PIPE) deploy_process.wait() self.assertEqual(deploy_process.returncode, 0) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_no_package_and_deploy_with_s3_bucket_all_args(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = "a" + str(uuid.uuid4()).replace("-", "")[:10] + self.stack_names.append(stack_name) + + # Package and Deploy in one go without confirming change set. + 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, + 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", + confirm_changeset=False, + ) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE) + deploy_process_execute.wait() + self.assertEqual(deploy_process_execute.returncode, 0) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_no_package_and_deploy_with_s3_bucket_all_args_confirm_changeset(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = "a" + str(uuid.uuid4()).replace("-", "")[:10] + self.stack_names.append(stack_name) + + # Package and Deploy in one go without confirming change set. + 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, + 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", + confirm_changeset=True, + ) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE, stderr=PIPE, stdin=PIPE) + deploy_process_execute.communicate("Y".encode()) + self.assertEqual(deploy_process_execute.returncode, 0) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_deploy_without_s3_bucket(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = "a" + str(uuid.uuid4()).replace("-", "")[:10] + + # Package and Deploy in one go without confirming change set. + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + stack_name=stack_name, + capabilities="CAPABILITY_IAM", + s3_prefix="integ_deploy", + 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", + confirm_changeset=False, + ) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE, stderr=PIPE) + deploy_process_execute.wait() + # Error asking for s3 bucket + self.assertEqual(deploy_process_execute.returncode, 1) + stderr = b"".join(deploy_process_execute.stderr.readlines()).strip() + self.assertIn( + bytes( + f"S3 Bucket not specified, use --s3-bucket to specify a bucket name or run sam deploy --guided", + encoding="utf-8", + ), + stderr, + ) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_deploy_without_stack_name(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + # Package and Deploy in one go without confirming change set. + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + capabilities="CAPABILITY_IAM", + s3_prefix="integ_deploy", + 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", + confirm_changeset=False, + ) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE, stderr=PIPE) + deploy_process_execute.wait() + # Error no stack name present + self.assertEqual(deploy_process_execute.returncode, 1) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_deploy_without_capabilities(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = "a" + str(uuid.uuid4()).replace("-", "")[:10] + + # Package and Deploy in one go without confirming change set. + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + stack_name=stack_name, + s3_prefix="integ_deploy", + 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", + confirm_changeset=False, + ) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE, stderr=PIPE) + deploy_process_execute.wait() + # Error capabilities not specified + self.assertEqual(deploy_process_execute.returncode, 1) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_deploy_without_template_file(self, template_file): + stack_name = "a" + str(uuid.uuid4()).replace("-", "")[:10] + + # Package and Deploy in one go without confirming change set. + deploy_command_list = self.get_deploy_command_list( + stack_name=stack_name, + s3_prefix="integ_deploy", + 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", + confirm_changeset=False, + ) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE, stderr=PIPE) + deploy_process_execute.wait() + # Error template file not specified + self.assertEqual(deploy_process_execute.returncode, 1) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_deploy_with_s3_bucket_switch_region(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = "a" + str(uuid.uuid4()).replace("-", "")[:10] + + # Package and Deploy in one go without confirming change set. + 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.bucket_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", + confirm_changeset=False, + ) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE) + deploy_process_execute.wait() + # Deploy should succeed + self.assertEqual(deploy_process_execute.returncode, 0) + + # Try to deploy to another region. + 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.bucket_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", + confirm_changeset=False, + region="eu-west-2", + ) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE, stderr=PIPE) + deploy_process_execute.wait() + # Deploy should fail, asking for s3 bucket + self.assertEqual(deploy_process_execute.returncode, 1) + stderr = b"".join(deploy_process_execute.stderr.readlines()).strip() + self.assertIn( + bytes( + f"Error: Failed to create/update stack {stack_name} : " + f"deployment s3 bucket is in a different region, try sam deploy --guided", + encoding="utf-8", + ), + stderr, + ) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_deploy_guided(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = "a" + str(uuid.uuid4()).replace("-", "")[:10] + self.stack_names.append(stack_name) + + # Package and Deploy in one go without confirming change set. + deploy_command_list = self.get_deploy_command_list(template_file=template_path, guided=True) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE, stderr=PIPE, stdin=PIPE) + deploy_process_execute.communicate("{}\n\n\n\n\n\n".format(stack_name).encode()) + + # Deploy should succeed with a managed stack + self.assertEqual(deploy_process_execute.returncode, 0) + self.stack_names.append(SAM_CLI_STACK_NAME) + # Remove samconfig.toml + os.remove(self.test_data_path.joinpath(DEFAULT_CONFIG_FILE_NAME)) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_deploy_guided_set_parameter(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = "a" + str(uuid.uuid4()).replace("-", "")[:10] + self.stack_names.append(stack_name) + + # Package and Deploy in one go without confirming change set. + deploy_command_list = self.get_deploy_command_list(template_file=template_path, guided=True) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE, stderr=PIPE, stdin=PIPE) + deploy_process_execute.communicate("{}\n\nSuppliedParameter\n\n\n\n".format(stack_name).encode()) + + # Deploy should succeed with a managed stack + self.assertEqual(deploy_process_execute.returncode, 0) + self.stack_names.append(SAM_CLI_STACK_NAME) + # Remove samconfig.toml + os.remove(self.test_data_path.joinpath(DEFAULT_CONFIG_FILE_NAME)) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_deploy_guided_set_capabilities(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = "a" + str(uuid.uuid4()).replace("-", "")[:10] + self.stack_names.append(stack_name) + + # Package and Deploy in one go without confirming change set. + deploy_command_list = self.get_deploy_command_list(template_file=template_path, guided=True) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE, stderr=PIPE, stdin=PIPE) + deploy_process_execute.communicate( + "{}\n\nSuppliedParameter\n\nn\nCAPABILITY_IAM CAPABILITY_NAMED_IAM\n\n".format(stack_name).encode() + ) + + # Deploy should succeed with a managed stack + self.assertEqual(deploy_process_execute.returncode, 0) + self.stack_names.append(SAM_CLI_STACK_NAME) + # Remove samconfig.toml + os.remove(self.test_data_path.joinpath(DEFAULT_CONFIG_FILE_NAME)) + + @parameterized.expand(["aws-serverless-function.yaml"]) + def test_deploy_guided_set_confirm_changeset(self, template_file): + template_path = self.test_data_path.joinpath(template_file) + + stack_name = "a" + str(uuid.uuid4()).replace("-", "")[:10] + self.stack_names.append(stack_name) + + # Package and Deploy in one go without confirming change set. + deploy_command_list = self.get_deploy_command_list(template_file=template_path, guided=True) + + deploy_process_execute = Popen(deploy_command_list, stdout=PIPE, stderr=PIPE, stdin=PIPE) + deploy_process_execute.communicate("{}\n\nSuppliedParameter\nY\n\n\nY\n".format(stack_name).encode()) + + # Deploy should succeed with a managed stack + self.assertEqual(deploy_process_execute.returncode, 0) + self.stack_names.append(SAM_CLI_STACK_NAME) + # Remove samconfig.toml + os.remove(self.test_data_path.joinpath(DEFAULT_CONFIG_FILE_NAME)) diff --git a/tests/regression/deploy/test_deploy_regression.py b/tests/regression/deploy/test_deploy_regression.py index 8280189b01..9a6fac4134 100644 --- a/tests/regression/deploy/test_deploy_regression.py +++ b/tests/regression/deploy/test_deploy_regression.py @@ -109,7 +109,7 @@ def test_deploy_with_no_capabilities(self, template_file): "tags": "integ=true clarity=yes", } - self.deploy_regression_check(arguments, sam_return_code=2, aws_return_code=255) + self.deploy_regression_check(arguments, sam_return_code=1, aws_return_code=255) def test_deploy_with_no_template_file(self): sam_stack_name = "a" + str(uuid.uuid4()).replace("-", "")[:10] @@ -128,8 +128,8 @@ def test_deploy_with_no_template_file(self): "kms_key_id": self.kms_key, "tags": "integ=true clarity=yes", } - - self.deploy_regression_check(arguments, sam_return_code=2, aws_return_code=2) + # if no template file is specified, sam cli looks for a template.yaml in the current working directory. + self.deploy_regression_check(arguments, sam_return_code=1, aws_return_code=2) @parameterized.expand(["aws-serverless-function.yaml"]) def test_deploy_with_no_changes(self, template_file):