From ff04d750da34ac8135fc11eff8b5f4b92e21897a Mon Sep 17 00:00:00 2001 From: Sriram Madapusi Vasudevan <3770774+sriram-mv@users.noreply.github.com> Date: Thu, 2 Jan 2020 12:40:58 -0800 Subject: [PATCH] refactor: `sam deploy` (#1681) * refactor: `sam deploy` - Move guided deploy prompts to its own file - New class for dealing with configuration during deploy - Utilities for printing deploy arguments and sanitizing parameter overrides * fix: save parameter values with spaces into config during guided deploy * fix: add user agent during package and deploy * fix: separate method for adding botoconfig with user-agent --- samcli/commands/deploy/command.py | 278 ++--------- samcli/commands/deploy/deploy_context.py | 10 +- samcli/commands/deploy/guided_config.py | 78 ++++ samcli/commands/deploy/guided_context.py | 162 +++++++ samcli/commands/deploy/utils.py | 59 +++ samcli/commands/package/package_context.py | 13 +- samcli/lib/utils/botoconfig.py | 17 + tests/unit/commands/deploy/test_command.py | 434 +++++++++--------- .../unit/commands/samconfig/test_samconfig.py | 60 +++ 9 files changed, 638 insertions(+), 473 deletions(-) create mode 100644 samcli/commands/deploy/guided_config.py create mode 100644 samcli/commands/deploy/guided_context.py create mode 100644 samcli/commands/deploy/utils.py create mode 100644 samcli/lib/utils/botoconfig.py diff --git a/samcli/commands/deploy/command.py b/samcli/commands/deploy/command.py index bb465a64b1..1c7b6632f4 100644 --- a/samcli/commands/deploy/command.py +++ b/samcli/commands/deploy/command.py @@ -1,32 +1,24 @@ """ CLI command for "deploy" command """ -import json import logging import click -from click.types import FuncParamType -from samcli.lib.utils import osutils -from samcli.cli.cli_config_file import configuration_option, TomlProvider -from samcli.cli.context import get_cmd_names -from samcli.cli.main import pass_context, common_options, aws_creds_options +from samcli.cli.cli_config_file import TomlProvider, configuration_option +from samcli.cli.main import aws_creds_options, common_options, pass_context from samcli.commands._utils.options import ( - parameter_override_option, capabilities_override_option, - tags_override_option, + guided_deploy_stack_name, + metadata_override_option, notification_arns_override_option, + parameter_override_option, + tags_override_option, template_click_option, - metadata_override_option, - _space_separated_list_func_type, - guided_deploy_stack_name, ) -from samcli.commands._utils.template import get_template_parameters -from samcli.commands.deploy.exceptions import GuidedDeployFailedError -from samcli.lib.bootstrap.bootstrap import manage_stack -from samcli.lib.config.samconfig import SamConfig +from samcli.commands.deploy.utils import print_deploy_args, sanitize_parameter_overrides from samcli.lib.telemetry.metrics import track_command -from samcli.lib.utils.colors import Colored +from samcli.lib.utils import osutils SHORT_HELP = "Deploy an AWS SAM application." @@ -211,258 +203,68 @@ def do_cli( ): from samcli.commands.package.package_context import PackageContext from samcli.commands.deploy.deploy_context import DeployContext - - # set capabilities and changeset decision to None, before guided gets input from the user - changeset_decision = None - _capabilities = None - _parameter_overrides = None - guided_stack_name = None - guided_s3_bucket = None - guided_s3_prefix = None - guided_region = None + from samcli.commands.deploy.guided_context import GuidedContext if guided: - - try: - _parameter_override_keys = get_template_parameters(template_file=template_file) - except ValueError as ex: - LOG.debug("Failed to parse SAM template", exc_info=ex) - raise GuidedDeployFailedError(str(ex)) - - read_config_showcase(template_file=template_file) - - ( - guided_stack_name, - guided_s3_bucket, - guided_s3_prefix, - guided_region, - guided_profile, - changeset_decision, - _capabilities, - _parameter_overrides, - save_to_config, - ) = guided_deploy( - stack_name, s3_bucket, region, profile, confirm_changeset, _parameter_override_keys, parameter_overrides + # Allow for a guided deploy to prompt and save those details. + guided_context = GuidedContext( + template_file=template_file, + stack_name=stack_name, + s3_bucket=s3_bucket, + s3_prefix=s3_prefix, + region=region, + profile=profile, + confirm_changeset=confirm_changeset, + capabilities=capabilities, + parameter_overrides=parameter_overrides, + config_section=CONFIG_SECTION, ) - - if save_to_config: - save_config( - template_file, - stack_name=guided_stack_name, - s3_bucket=guided_s3_bucket, - s3_prefix=guided_s3_prefix, - region=guided_region, - profile=guided_profile, - confirm_changeset=changeset_decision, - capabilities=_capabilities, - parameter_overrides=_parameter_overrides, - ) + guided_context.run() print_deploy_args( - stack_name=guided_stack_name if guided else stack_name, - s3_bucket=guided_s3_bucket if guided else s3_bucket, - region=guided_region if guided else region, - capabilities=_capabilities if guided else capabilities, - parameter_overrides=_parameter_overrides if guided else parameter_overrides, - confirm_changeset=changeset_decision if guided else confirm_changeset, + stack_name=guided_context.guided_stack_name if guided else stack_name, + s3_bucket=guided_context.guided_s3_bucket if guided else s3_bucket, + region=guided_context.guided_region if guided else region, + capabilities=guided_context.guided_capabilities if guided else capabilities, + parameter_overrides=guided_context.guided_parameter_overrides if guided else parameter_overrides, + confirm_changeset=guided_context.confirm_changeset if guided else confirm_changeset, ) with osutils.tempfile_platform_independent() as output_template_file: with PackageContext( template_file=template_file, - s3_bucket=guided_s3_bucket if guided else s3_bucket, - s3_prefix=guided_s3_prefix if guided else s3_prefix, + s3_bucket=guided_context.guided_s3_bucket if guided else s3_bucket, + s3_prefix=guided_context.guided_s3_prefix if guided else s3_prefix, output_template_file=output_template_file.name, kms_key_id=kms_key_id, use_json=use_json, force_upload=force_upload, metadata=metadata, on_deploy=True, - region=guided_region if guided else region, + region=guided_context.guided_region if guided else region, profile=profile, ) as package_context: package_context.run() with DeployContext( template_file=output_template_file.name, - stack_name=guided_stack_name if guided else stack_name, - s3_bucket=guided_s3_bucket if guided else s3_bucket, + stack_name=guided_context.guided_stack_name if guided else stack_name, + s3_bucket=guided_context.guided_s3_bucket if guided else s3_bucket, force_upload=force_upload, - s3_prefix=guided_s3_prefix if guided else s3_prefix, + s3_prefix=guided_context.guided_s3_prefix if guided else s3_prefix, kms_key_id=kms_key_id, - parameter_overrides=sanitize_parameter_overrides(_parameter_overrides) if guided else parameter_overrides, - capabilities=_capabilities if guided else capabilities, + parameter_overrides=sanitize_parameter_overrides(guided_context.guided_parameter_overrides) + if guided + else parameter_overrides, + capabilities=guided_context.guided_capabilities if guided else capabilities, no_execute_changeset=no_execute_changeset, role_arn=role_arn, notification_arns=notification_arns, fail_on_empty_changeset=fail_on_empty_changeset, tags=tags, - region=guided_region if guided else region, + region=guided_context.guided_region if guided else region, profile=profile, - confirm_changeset=changeset_decision if guided else confirm_changeset, + confirm_changeset=guided_context.confirm_changeset if guided else confirm_changeset, ) as deploy_context: deploy_context.run() - - -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 - - color = Colored() - start_bold = "\033[1m" - end_bold = "\033[0m" - - click.echo( - color.yellow("\n\tSetting default arguments for 'sam deploy'\n\t=========================================") - ) - - stack_name = click.prompt(f"\t{start_bold}Stack Name{end_bold}", default=default_stack_name, type=click.STRING) - s3_prefix = stack_name - region = click.prompt(f"\t{start_bold}AWS Region{end_bold}", default=default_region, type=click.STRING) - 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( - f"\t{start_bold}Confirm changes before deploy{end_bold}", default=confirm_changeset - ) - click.secho("\t#SAM needs permission to be able to create roles to connect to the resources in your template") - capabilities_confirm = click.confirm(f"\t{start_bold}Allow SAM CLI IAM role creation{end_bold}", default=True) - - if not capabilities_confirm: - input_capabilities = click.prompt( - f"\t{start_bold}Capabilities{end_bold}", - default=default_capabilities[0], - type=FuncParamType(func=_space_separated_list_func_type), - ) - - save_to_config = click.confirm(f"\t{start_bold}Save arguments to samconfig.toml{end_bold}", default=True) - - s3_bucket = manage_stack(profile=profile, region=region) - click.echo(f"\n\t\tManaged S3 bucket: {s3_bucket}") - click.echo("\t\tA different default S3 bucket can be set in samconfig.toml") - - return ( - stack_name, - s3_bucket, - s3_prefix, - region, - profile, - confirm_changeset, - input_capabilities if input_capabilities else default_capabilities, - input_parameter_overrides if input_parameter_overrides else parameter_overrides, - save_to_config, - ) - - -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: - # Make sure the default is casted to a string. - parameter = click.prompt( - f"\t{start_bold}Parameter {parameter_key}{end_bold}", - default=_prompted_param_overrides.get(parameter_key, str(parameter_properties.get("Default", ""))), - 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() - for key, value in _parameters.items(): - if isinstance(value, dict): - _parameters[key] = value.get("Value", value) if not value.get("Hidden") else "*" * len(value.get("Value")) - - capabilities_string = json.dumps(capabilities) - - click.secho("\n\tDeploying with following values\n\t===============================", fg="yellow") - click.echo(f"\tStack name : {stack_name}") - click.echo(f"\tRegion : {region}") - click.echo(f"\tConfirm changeset : {confirm_changeset}") - click.echo(f"\tDeployment s3 bucket : {s3_bucket}") - click.echo(f"\tCapabilities : {capabilities_string}") - click.echo(f"\tParameter overrides : {_parameters}") - - click.secho("\nInitiating deployment\n=====================", fg="yellow") - - -def read_config_showcase(template_file): - _, samconfig = get_config_ctx(template_file) - - status = "Found" if samconfig.exists() else "Not found" - msg = ( - "Syntax invalid in samconfig.toml; save values " - "through sam deploy --guided to overwrite file with a valid set of values." - ) - config_sanity = samconfig.sanity_check() - click.secho("\nConfiguring SAM deploy\n======================", fg="yellow") - click.echo(f"\n\tLooking for samconfig.toml : {status}") - if samconfig.exists(): - click.echo("\tReading default arguments : {}".format("Success" if config_sanity else "Failure")) - - if not config_sanity and samconfig.exists(): - raise GuidedDeployFailedError(msg) - - -def save_config(template_file, parameter_overrides, **kwargs): - - section = CONFIG_SECTION - ctx, samconfig = get_config_ctx(template_file) - - cmd_names = get_cmd_names(ctx.info_name, ctx) - - for key, value in kwargs.items(): - if isinstance(value, (list, tuple)): - value = " ".join(val for val in value) - if value: - samconfig.put(cmd_names, section, key, value) - - if parameter_overrides: - _params = [] - for key, value in parameter_overrides.items(): - if isinstance(value, dict): - if not value.get("Hidden"): - _params.append(f"{key}={value.get('Value')}") - else: - _params.append(f"{key}={value}") - if _params: - samconfig.put(cmd_names, section, "parameter_overrides", " ".join(_params)) - - samconfig.flush() - - click.echo(f"\n\tSaved arguments to config file") - click.echo("\tRunning 'sam deploy' for future deployments will use the parameters saved above.") - click.echo("\tThe above parameters can be changed by modifying samconfig.toml") - click.echo( - "\tLearn more about samconfig.toml syntax at " - "\n\thttps://docs.aws.amazon.com/serverless-application-model/latest/" - "developerguide/serverless-sam-cli-config.html" - ) - - -def get_config_ctx(template_file): - ctx = click.get_current_context() - - samconfig_dir = getattr(ctx, "samconfig_dir", None) - samconfig = SamConfig( - config_dir=samconfig_dir if samconfig_dir else SamConfig.config_dir(template_file_path=template_file) - ) - return ctx, samconfig - - -def sanitize_parameter_overrides(parameter_overrides): - return {key: value.get("Value") if isinstance(value, dict) else value for key, value in parameter_overrides.items()} diff --git a/samcli/commands/deploy/deploy_context.py b/samcli/commands/deploy/deploy_context.py index 741d621019..963fecfb50 100644 --- a/samcli/commands/deploy/deploy_context.py +++ b/samcli/commands/deploy/deploy_context.py @@ -15,16 +15,17 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -import os import logging +import os + import boto3 import click from samcli.commands.deploy import exceptions as deploy_exceptions from samcli.lib.deploy.deployer import Deployer from samcli.lib.package.s3_uploader import S3Uploader +from samcli.lib.utils.botoconfig import get_boto_config_with_user_agent from samcli.yamlhelper import yaml_parse -from samcli.lib.utils.colors import Colored LOG = logging.getLogger(__name__) @@ -102,7 +103,10 @@ def run(self): raise deploy_exceptions.DeployBucketRequiredError() 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) + config = get_boto_config_with_user_agent() + cloudformation_client = session.client( + "cloudformation", region_name=self.region if self.region else None, config=config + ) s3_client = None if self.s3_bucket: diff --git a/samcli/commands/deploy/guided_config.py b/samcli/commands/deploy/guided_config.py new file mode 100644 index 0000000000..00b5e74ce5 --- /dev/null +++ b/samcli/commands/deploy/guided_config.py @@ -0,0 +1,78 @@ +""" +Set of Utilities to deal with reading/writing to configuration file during sam deploy +""" + +import click + +from samcli.cli.context import get_cmd_names +from samcli.commands.deploy.exceptions import GuidedDeployFailedError +from samcli.lib.config.samconfig import SamConfig + + +class GuidedConfig: + def __init__(self, template_file, section): + self.template_file = template_file + self.section = section + + def get_config_ctx(self): + ctx = click.get_current_context() + + samconfig_dir = getattr(ctx, "samconfig_dir", None) + samconfig = SamConfig( + config_dir=samconfig_dir if samconfig_dir else SamConfig.config_dir(template_file_path=self.template_file) + ) + return ctx, samconfig + + def read_config_showcase(self): + _, samconfig = self.get_config_ctx() + + status = "Found" if samconfig.exists() else "Not found" + msg = ( + "Syntax invalid in samconfig.toml; save values " + "through sam deploy --guided to overwrite file with a valid set of values." + ) + config_sanity = samconfig.sanity_check() + click.secho("\nConfiguring SAM deploy\n======================", fg="yellow") + click.echo(f"\n\tLooking for samconfig.toml : {status}") + if samconfig.exists(): + click.echo("\tReading default arguments : {}".format("Success" if config_sanity else "Failure")) + + if not config_sanity and samconfig.exists(): + raise GuidedDeployFailedError(msg) + + def save_config(self, parameter_overrides, **kwargs): + + ctx, samconfig = self.get_config_ctx() + + cmd_names = get_cmd_names(ctx.info_name, ctx) + + for key, value in kwargs.items(): + if isinstance(value, (list, tuple)): + value = " ".join(val for val in value) + if value: + samconfig.put(cmd_names, self.section, key, value) + + if parameter_overrides: + _params = [] + for key, value in parameter_overrides.items(): + if isinstance(value, dict): + if not value.get("Hidden"): + _params.append(f"{key}={self.quote_parameter_values(value.get('Value'))}") + else: + _params.append(f"{key}={self.quote_parameter_values(value)}") + if _params: + samconfig.put(cmd_names, self.section, "parameter_overrides", " ".join(_params)) + + samconfig.flush() + + click.echo(f"\n\tSaved arguments to config file") + click.echo("\tRunning 'sam deploy' for future deployments will use the parameters saved above.") + click.echo("\tThe above parameters can be changed by modifying samconfig.toml") + click.echo( + "\tLearn more about samconfig.toml syntax at " + "\n\thttps://docs.aws.amazon.com/serverless-application-model/latest/" + "developerguide/serverless-sam-cli-config.html" + ) + + def quote_parameter_values(self, parameter_value): + return '"{}"'.format(parameter_value) diff --git a/samcli/commands/deploy/guided_context.py b/samcli/commands/deploy/guided_context.py new file mode 100644 index 0000000000..7b3afc23e7 --- /dev/null +++ b/samcli/commands/deploy/guided_context.py @@ -0,0 +1,162 @@ +""" +Class to manage all the prompts during a guided sam deploy +""" + +import logging + +import click +from click.types import FuncParamType +from click import prompt +from click import confirm + +from samcli.commands._utils.options import _space_separated_list_func_type +from samcli.commands._utils.template import get_template_parameters +from samcli.commands.deploy.exceptions import GuidedDeployFailedError +from samcli.commands.deploy.guided_config import GuidedConfig +from samcli.lib.bootstrap.bootstrap import manage_stack +from samcli.lib.utils.colors import Colored + +LOG = logging.getLogger(__name__) + + +class GuidedContext: + def __init__( + self, + template_file, + stack_name, + s3_bucket, + s3_prefix, + region=None, + profile=None, + confirm_changeset=None, + capabilities=None, + parameter_overrides=None, + save_to_config=True, + config_section=None, + ): + self.template_file = template_file + self.stack_name = stack_name + self.s3_bucket = s3_bucket + self.s3_prefix = s3_prefix + self.region = region + self.profile = profile + self.confirm_changeset = confirm_changeset + self.capabilities = (capabilities,) + self.parameter_overrides = parameter_overrides + self.save_to_config = save_to_config + self.config_section = config_section + self.guided_stack_name = None + self.guided_s3_bucket = None + self.guided_s3_prefix = None + self.guided_region = None + self.guided_profile = None + self._capabilities = None + self._parameter_overrides = None + self.start_bold = "\033[1m" + self.end_bold = "\033[0m" + self.color = Colored() + + @property + def guided_capabilities(self): + return self._capabilities + + @property + def guided_parameter_overrides(self): + return self._parameter_overrides + + def guided_prompts(self, parameter_override_keys): + default_stack_name = self.stack_name or "sam-app" + default_region = self.region or "us-east-1" + default_capabilities = ("CAPABILITY_IAM",) + input_capabilities = None + + click.echo( + self.color.yellow( + "\n\tSetting default arguments for 'sam deploy'\n\t=========================================" + ) + ) + + stack_name = prompt( + f"\t{self.start_bold}Stack Name{self.end_bold}", default=default_stack_name, type=click.STRING + ) + region = prompt(f"\t{self.start_bold}AWS Region{self.end_bold}", default=default_region, type=click.STRING) + input_parameter_overrides = self.prompt_parameters(parameter_override_keys, self.start_bold, self.end_bold) + + click.secho("\t#Shows you resources changes to be deployed and require a 'Y' to initiate deploy") + confirm_changeset = confirm( + f"\t{self.start_bold}Confirm changes before deploy{self.end_bold}", default=self.confirm_changeset + ) + click.secho("\t#SAM needs permission to be able to create roles to connect to the resources in your template") + capabilities_confirm = confirm( + f"\t{self.start_bold}Allow SAM CLI IAM role creation{self.end_bold}", default=True + ) + + if not capabilities_confirm: + input_capabilities = prompt( + f"\t{self.start_bold}Capabilities{self.end_bold}", + default=default_capabilities[0], + type=FuncParamType(func=_space_separated_list_func_type), + ) + + save_to_config = confirm(f"\t{self.start_bold}Save arguments to samconfig.toml{self.end_bold}", default=True) + + s3_bucket = manage_stack(profile=self.profile, region=region) + click.echo(f"\n\t\tManaged S3 bucket: {s3_bucket}") + click.echo("\t\tA different default S3 bucket can be set in samconfig.toml") + + self.guided_stack_name = stack_name + self.guided_s3_bucket = s3_bucket + self.guided_s3_prefix = stack_name + self.guided_region = region + self.guided_profile = self.profile + self._capabilities = input_capabilities if input_capabilities else default_capabilities + self._parameter_overrides = input_parameter_overrides if input_parameter_overrides else self.parameter_overrides + self.save_to_config = save_to_config + self.confirm_changeset = confirm_changeset + + def prompt_parameters(self, 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 = 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: + # Make sure the default is casted to a string. + parameter = prompt( + f"\t{start_bold}Parameter {parameter_key}{end_bold}", + default=_prompted_param_overrides.get( + parameter_key, str(parameter_properties.get("Default", "")) + ), + type=click.STRING, + ) + _prompted_param_overrides[parameter_key] = {"Value": parameter, "Hidden": False} + return _prompted_param_overrides + + def run(self): + + try: + _parameter_override_keys = get_template_parameters(template_file=self.template_file) + except ValueError as ex: + LOG.debug("Failed to parse SAM template", exc_info=ex) + raise GuidedDeployFailedError(str(ex)) + + guided_config = GuidedConfig(template_file=self.template_file, section=self.config_section) + guided_config.read_config_showcase() + + self.guided_prompts(_parameter_override_keys) + + if self.save_to_config: + guided_config.save_config( + self._parameter_overrides, + stack_name=self.guided_stack_name, + s3_bucket=self.guided_s3_bucket, + s3_prefix=self.guided_s3_prefix, + region=self.guided_region, + profile=self.guided_profile, + confirm_changeset=self.confirm_changeset, + capabilities=self._capabilities, + ) diff --git a/samcli/commands/deploy/utils.py b/samcli/commands/deploy/utils.py new file mode 100644 index 0000000000..d475be65f2 --- /dev/null +++ b/samcli/commands/deploy/utils.py @@ -0,0 +1,59 @@ +""" +Utilities for sam deploy command +""" + +import json +import click + + +def print_deploy_args(stack_name, s3_bucket, region, capabilities, parameter_overrides, confirm_changeset): + """ + Print a table of the values that are used during a sam deploy + + Example below: + + Deploying with following values + =============================== + Stack name : sam-app + Region : us-east-1 + Confirm changeset : False + Deployment s3 bucket : aws-sam-cli-managed-default-samclisourcebucket-abcdef + Capabilities : ["CAPABILITY_IAM"] + Parameter overrides : {'MyParamater': '***', 'Parameter2': 'dd'} + + :param stack_name: Name of the stack used during sam deploy + :param s3_bucket: Name of s3 bucket used for packaging code artifacts + :param region: Name of region to which the current sam/cloudformation stack will be deployed to. + :param capabilities: Corresponding IAM capabilities to be used during the stack deploy. + :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. + :return: + """ + + _parameters = parameter_overrides.copy() + for key, value in _parameters.items(): + if isinstance(value, dict): + _parameters[key] = value.get("Value", value) if not value.get("Hidden") else "*" * len(value.get("Value")) + + capabilities_string = json.dumps(capabilities) + + click.secho("\n\tDeploying with following values\n\t===============================", fg="yellow") + click.echo(f"\tStack name : {stack_name}") + click.echo(f"\tRegion : {region}") + click.echo(f"\tConfirm changeset : {confirm_changeset}") + click.echo(f"\tDeployment s3 bucket : {s3_bucket}") + click.echo(f"\tCapabilities : {capabilities_string}") + click.echo(f"\tParameter overrides : {_parameters}") + + click.secho("\nInitiating deployment\n=====================", fg="yellow") + + +def sanitize_parameter_overrides(parameter_overrides): + """ + Get sanitized parameter override values based on if the workflow went via a guided deploy to set the + parameter overrides for deployment. If a guided deploy was followed the parameter overrides consists + of additional information such as if a given parameter's value is hidden or not. + :param parameter_overrides: dictionary of parameter key values. + :return: + """ + return {key: value.get("Value") if isinstance(value, dict) else value for key, value in parameter_overrides.items()} diff --git a/samcli/commands/package/package_context.py b/samcli/commands/package/package_context.py index 7e985ab715..087ec7d162 100644 --- a/samcli/commands/package/package_context.py +++ b/samcli/commands/package/package_context.py @@ -15,18 +15,18 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -import os -import logging import json +import logging +import os import boto3 -from botocore.config import Config import click from samcli.commands.package.exceptions import PackageFailedError from samcli.lib.package.artifact_exporter import Template -from samcli.yamlhelper import yaml_dump from samcli.lib.package.s3_uploader import S3Uploader +from samcli.lib.utils.botoconfig import get_boto_config_with_user_agent +from samcli.yamlhelper import yaml_dump LOG = logging.getLogger(__name__) @@ -81,7 +81,10 @@ def run(self): session = boto3.Session(profile_name=self.profile if self.profile else None) s3_client = session.client( - "s3", config=Config(signature_version="s3v4", region_name=self.region if self.region else None) + "s3", + config=get_boto_config_with_user_agent( + signature_version="s3v4", region_name=self.region if self.region else None + ), ) self.s3_uploader = S3Uploader(s3_client, self.s3_bucket, self.s3_prefix, self.kms_key_id, self.force_upload) diff --git a/samcli/lib/utils/botoconfig.py b/samcli/lib/utils/botoconfig.py new file mode 100644 index 0000000000..7a7bd6d792 --- /dev/null +++ b/samcli/lib/utils/botoconfig.py @@ -0,0 +1,17 @@ +""" +Automatically add user agent string to boto configs. +""" +from botocore.config import Config + +from samcli import __version__ +from samcli.cli.global_config import GlobalConfig + + +def get_boto_config_with_user_agent(**kwargs): + gc = GlobalConfig() + return Config( + user_agent_extra=f"aws-sam-cli/{__version__}/{gc.installation_id}" + if gc.telemetry_enabled + else f"aws-sam-cli/{__version__}", + **kwargs, + ) diff --git a/tests/unit/commands/deploy/test_command.py b/tests/unit/commands/deploy/test_command.py index 4ea77dc5d5..a1fb581c8d 100644 --- a/tests/unit/commands/deploy/test_command.py +++ b/tests/unit/commands/deploy/test_command.py @@ -1,11 +1,21 @@ from unittest import TestCase -from unittest.mock import patch, Mock, ANY, MagicMock, call +from unittest.mock import ANY, MagicMock, Mock, call, patch from samcli.commands.deploy.command import do_cli +from samcli.commands.deploy.guided_config import GuidedConfig from tests.unit.cli.test_cli_config_file import MockContext -class TestDeployliCommand(TestCase): +def get_mock_sam_config(): + mock_sam_config = MagicMock() + mock_sam_config.exists = MagicMock(return_value=True) + return mock_sam_config + + +MOCK_SAM_CONFIG = get_mock_sam_config() + + +class TestDeployCliCommand(TestCase): def setUp(self): self.template_file = "input-template-file" @@ -28,6 +38,7 @@ def setUp(self): self.metadata = {} self.guided = False self.confirm_changeset = False + MOCK_SAM_CONFIG.reset_mock() @patch("samcli.commands.package.command.click") @patch("samcli.commands.package.package_context.PackageContext") @@ -86,16 +97,19 @@ def test_all_args(self, mock_deploy_context, mock_deploy_click, mock_package_con @patch("samcli.commands.package.package_context.PackageContext") @patch("samcli.commands.deploy.command.click") @patch("samcli.commands.deploy.deploy_context.DeployContext") - @patch("samcli.commands.deploy.command.save_config") - @patch("samcli.commands.deploy.command.manage_stack") - @patch("samcli.commands.deploy.command.get_template_parameters") - @patch("samcli.commands.deploy.command.get_config_ctx") + @patch("samcli.commands.deploy.command.print_deploy_args") + @patch("samcli.commands.deploy.guided_context.manage_stack") + @patch("samcli.commands.deploy.guided_context.get_template_parameters") + @patch.object(GuidedConfig, "get_config_ctx", MagicMock(return_value=(None, get_mock_sam_config()))) + @patch("samcli.commands.deploy.guided_context.prompt") + @patch("samcli.commands.deploy.guided_context.confirm") def test_all_args_guided( self, - mock_get_config_ctx, + mock_confirm, + mock_prompt, mock_get_template_parameters, mock_managed_stack, - mock_save_config, + mock_print_deploy_args, mock_deploy_context, mock_deploy_click, mock_package_context, @@ -103,93 +117,97 @@ def test_all_args_guided( ): context_mock = Mock() - mock_sam_config = MagicMock() - mock_sam_config.exists = MagicMock(return_value=True) - mock_get_config_ctx.return_value = (None, mock_sam_config) + mock_deploy_context.return_value.__enter__.return_value = context_mock + mock_confirm.side_effect = [True, False, True] + mock_prompt.side_effect = ["sam-app", "us-east-1", "guidedParameter", "secure", ("CAPABILITY_IAM",)] + mock_get_template_parameters.return_value = { "Myparameter": {"Type": "String"}, "MyNoEchoParameter": {"Type": "String", "NoEcho": True}, } - mock_deploy_context.return_value.__enter__.return_value = context_mock - mock_deploy_click.prompt = MagicMock( - side_effect=["sam-app", "us-east-1", "guidedParameter", "secure", ("CAPABILITY_IAM",)] - ) - mock_deploy_click.confirm = MagicMock(side_effect=[True, False, True]) mock_managed_stack.return_value = "managed-s3-bucket" - mock_save_config.return_value = True - - do_cli( - template_file=self.template_file, - stack_name=self.stack_name, - s3_bucket=None, - force_upload=self.force_upload, - s3_prefix=self.s3_prefix, - kms_key_id=self.kms_key_id, - parameter_overrides=self.parameter_overrides, - capabilities=self.capabilities, - no_execute_changeset=self.no_execute_changeset, - role_arn=self.role_arn, - notification_arns=self.notification_arns, - fail_on_empty_changeset=self.fail_on_empty_changset, - tags=self.tags, - region=self.region, - profile=self.profile, - use_json=self.use_json, - metadata=self.metadata, - guided=True, - confirm_changeset=True, - ) - - mock_deploy_context.assert_called_with( - template_file=ANY, - stack_name="sam-app", - s3_bucket="managed-s3-bucket", - force_upload=self.force_upload, - s3_prefix="sam-app", - kms_key_id=self.kms_key_id, - parameter_overrides={"Myparameter": "guidedParameter", "MyNoEchoParameter": "secure"}, - capabilities=self.capabilities, - no_execute_changeset=self.no_execute_changeset, - role_arn=self.role_arn, - notification_arns=self.notification_arns, - fail_on_empty_changeset=self.fail_on_empty_changset, - tags=self.tags, - region="us-east-1", - profile=self.profile, - confirm_changeset=True, - ) - context_mock.run.assert_called_with() - mock_save_config.assert_called_with( - "input-template-file", - capabilities=("CAPABILITY_IAM",), - confirm_changeset=True, - profile=self.profile, - region="us-east-1", - s3_bucket="managed-s3-bucket", - stack_name="sam-app", - s3_prefix="sam-app", - parameter_overrides={ - "Myparameter": {"Value": "guidedParameter", "Hidden": False}, - "MyNoEchoParameter": {"Value": "secure", "Hidden": True}, - }, - ) - mock_managed_stack.assert_called_with(profile=self.profile, region="us-east-1") - self.assertEqual(context_mock.run.call_count, 1) + with patch.object(GuidedConfig, "save_config", MagicMock(return_value=True)) as mock_save_config: + do_cli( + template_file=self.template_file, + stack_name=self.stack_name, + s3_bucket=None, + force_upload=self.force_upload, + s3_prefix=self.s3_prefix, + kms_key_id=self.kms_key_id, + parameter_overrides=self.parameter_overrides, + capabilities=self.capabilities, + no_execute_changeset=self.no_execute_changeset, + role_arn=self.role_arn, + notification_arns=self.notification_arns, + fail_on_empty_changeset=self.fail_on_empty_changset, + tags=self.tags, + region=self.region, + profile=self.profile, + use_json=self.use_json, + metadata=self.metadata, + guided=True, + confirm_changeset=True, + ) + + mock_deploy_context.assert_called_with( + template_file=ANY, + stack_name="sam-app", + s3_bucket="managed-s3-bucket", + force_upload=self.force_upload, + s3_prefix="sam-app", + kms_key_id=self.kms_key_id, + parameter_overrides={"Myparameter": "guidedParameter", "MyNoEchoParameter": "secure"}, + capabilities=self.capabilities, + no_execute_changeset=self.no_execute_changeset, + role_arn=self.role_arn, + notification_arns=self.notification_arns, + fail_on_empty_changeset=self.fail_on_empty_changset, + tags=self.tags, + region="us-east-1", + profile=self.profile, + confirm_changeset=True, + ) + + context_mock.run.assert_called_with() + mock_save_config.assert_called_with( + { + "Myparameter": {"Value": "guidedParameter", "Hidden": False}, + "MyNoEchoParameter": {"Value": "secure", "Hidden": True}, + }, + capabilities=("CAPABILITY_IAM",), + confirm_changeset=True, + profile=self.profile, + region="us-east-1", + s3_bucket="managed-s3-bucket", + stack_name="sam-app", + s3_prefix="sam-app", + ) + mock_managed_stack.assert_called_with(profile=self.profile, region="us-east-1") + self.assertEqual(context_mock.run.call_count, 1) @patch("samcli.commands.package.command.click") @patch("samcli.commands.package.package_context.PackageContext") @patch("samcli.commands.deploy.command.click") @patch("samcli.commands.deploy.deploy_context.DeployContext") - @patch("samcli.commands.deploy.command.manage_stack") - @patch("samcli.commands.deploy.command.get_template_parameters") - @patch("samcli.commands.deploy.command.get_config_ctx") + @patch("samcli.commands.deploy.command.print_deploy_args") + @patch("samcli.commands.deploy.guided_context.manage_stack") + @patch("samcli.commands.deploy.guided_context.get_template_parameters") + @patch.object( + GuidedConfig, + "get_config_ctx", + MagicMock(return_value=(MockContext(info_name="deploy", parent=None), MOCK_SAM_CONFIG)), + ) + @patch("samcli.commands.deploy.guided_context.prompt") + @patch("samcli.commands.deploy.guided_context.confirm") def test_all_args_guided_no_save_echo_param_to_config( self, - mock_get_config_ctx, + mock_confirm, + mock_prompt, mock_get_template_parameters, mock_managed_stack, + mock_print_deploy_args, mock_deploy_context, mock_deploy_click, mock_package_context, @@ -197,18 +215,21 @@ def test_all_args_guided_no_save_echo_param_to_config( ): context_mock = Mock() - mock_sam_config = MagicMock() - mock_sam_config.exists = MagicMock(return_value=True) - mock_get_config_ctx.return_value = (MockContext(info_name="deploy", parent=None), mock_sam_config) mock_get_template_parameters.return_value = { "Myparameter": {"Type": "String"}, + "MyParameterSpaces": {"Type": "String"}, "MyNoEchoParameter": {"Type": "String", "NoEcho": True}, } mock_deploy_context.return_value.__enter__.return_value = context_mock - mock_deploy_click.prompt = MagicMock( - side_effect=["sam-app", "us-east-1", "guidedParameter", "secure", ("CAPABILITY_IAM",)] - ) - mock_deploy_click.confirm = MagicMock(side_effect=[True, False, True]) + mock_prompt.side_effect = [ + "sam-app", + "us-east-1", + "guidedParameter", + "guided parameter with spaces", + "secure", + ("CAPABILITY_IAM",), + ] + mock_confirm.side_effect = [True, False, True] mock_managed_stack.return_value = "managed-s3-bucket" @@ -241,7 +262,11 @@ def test_all_args_guided_no_save_echo_param_to_config( force_upload=self.force_upload, s3_prefix="sam-app", kms_key_id=self.kms_key_id, - parameter_overrides={"Myparameter": "guidedParameter", "MyNoEchoParameter": "secure"}, + parameter_overrides={ + "Myparameter": "guidedParameter", + "MyParameterSpaces": "guided parameter with spaces", + "MyNoEchoParameter": "secure", + }, capabilities=self.capabilities, no_execute_changeset=self.no_execute_changeset, role_arn=self.role_arn, @@ -257,9 +282,9 @@ def test_all_args_guided_no_save_echo_param_to_config( 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, 7) + self.assertEqual(MOCK_SAM_CONFIG.put.call_count, 7) self.assertEqual( - mock_sam_config.put.call_args_list, + MOCK_SAM_CONFIG.put.call_args_list, [ call(["deploy"], "parameters", "stack_name", "sam-app"), call(["deploy"], "parameters", "s3_bucket", "managed-s3-bucket"), @@ -267,7 +292,12 @@ def test_all_args_guided_no_save_echo_param_to_config( call(["deploy"], "parameters", "region", "us-east-1"), call(["deploy"], "parameters", "confirm_changeset", True), call(["deploy"], "parameters", "capabilities", "CAPABILITY_IAM"), - call(["deploy"], "parameters", "parameter_overrides", "Myparameter=guidedParameter"), + call( + ["deploy"], + "parameters", + "parameter_overrides", + 'Myparameter="guidedParameter" MyParameterSpaces="guided parameter with spaces"', + ), ], ) @@ -275,16 +305,27 @@ def test_all_args_guided_no_save_echo_param_to_config( @patch("samcli.commands.package.package_context.PackageContext") @patch("samcli.commands.deploy.command.click") @patch("samcli.commands.deploy.deploy_context.DeployContext") - @patch("samcli.commands.deploy.command.manage_stack") - @patch("samcli.commands.deploy.command.get_template_parameters") - @patch("samcli.commands.deploy.command.SamConfig") - @patch("samcli.commands.deploy.command.get_cmd_names") + @patch("samcli.commands.deploy.command.print_deploy_args") + @patch("samcli.commands.deploy.guided_context.manage_stack") + @patch("samcli.commands.deploy.guided_context.get_template_parameters") + @patch.object( + GuidedConfig, + "get_config_ctx", + MagicMock(return_value=(MockContext(info_name="deploy", parent=None), MOCK_SAM_CONFIG)), + ) + @patch("samcli.commands.deploy.guided_context.prompt") + @patch("samcli.commands.deploy.guided_context.confirm") + @patch("samcli.commands.deploy.guided_config.SamConfig") + @patch("samcli.commands.deploy.guided_config.get_cmd_names") def test_all_args_guided_no_params_save_config( self, mock_get_cmd_names, mock_sam_config, + mock_confirm, + mock_prompt, mock_get_template_parameters, mock_managed_stack, + mock_print_deploy_args, mock_deploy_context, mock_deploy_click, mock_package_context, @@ -295,8 +336,8 @@ def test_all_args_guided_no_params_save_config( mock_get_template_parameters.return_value = {} mock_deploy_context.return_value.__enter__.return_value = context_mock - mock_deploy_click.prompt = MagicMock(side_effect=["sam-app", "us-east-1", ("CAPABILITY_IAM",)]) - mock_deploy_click.confirm = MagicMock(side_effect=[True, False, True]) + mock_prompt.side_effect = ["sam-app", "us-east-1", ("CAPABILITY_IAM",)] + mock_confirm.side_effect = [True, False, True] mock_get_cmd_names.return_value = ["deploy"] mock_managed_stack.return_value = "managed-s3-bucket" @@ -345,97 +386,37 @@ def test_all_args_guided_no_params_save_config( mock_managed_stack.assert_called_with(profile=self.profile, region="us-east-1") self.assertEqual(context_mock.run.call_count, 1) - @patch("samcli.commands.package.command.click") - @patch("samcli.commands.package.package_context.PackageContext") - @patch("samcli.commands.deploy.command.click") - @patch("samcli.commands.deploy.deploy_context.DeployContext") - @patch("samcli.commands.deploy.command.save_config") - @patch("samcli.commands.deploy.command.manage_stack") - @patch("samcli.commands.deploy.command.get_template_parameters") - @patch("samcli.commands.deploy.command.get_config_ctx") - def test_all_args_guided_no_params_no_save_config( - self, - mock_get_config_ctx, - mock_get_template_parameters, - mock_managed_stack, - mock_save_config, - mock_deploy_context, - mock_deploy_click, - mock_package_context, - mock_package_click, - ): - - context_mock = Mock() - mock_sam_config = MagicMock() - mock_sam_config.exists = MagicMock(return_value=True) - mock_get_config_ctx.return_value = (None, mock_sam_config) - mock_get_template_parameters.return_value = {} - mock_deploy_context.return_value.__enter__.return_value = context_mock - mock_deploy_click.prompt = MagicMock(side_effect=["sam-app", "us-east-1", ("CAPABILITY_IAM",)]) - mock_deploy_click.confirm = MagicMock(side_effect=[True, False, False]) - - mock_managed_stack.return_value = "managed-s3-bucket" - - do_cli( - template_file=self.template_file, - stack_name=self.stack_name, - s3_bucket=None, - force_upload=self.force_upload, - s3_prefix=self.s3_prefix, - kms_key_id=self.kms_key_id, - parameter_overrides=self.parameter_overrides, - capabilities=self.capabilities, - no_execute_changeset=self.no_execute_changeset, - role_arn=self.role_arn, - notification_arns=self.notification_arns, - fail_on_empty_changeset=self.fail_on_empty_changset, - tags=self.tags, - region=self.region, - profile=self.profile, - use_json=self.use_json, - metadata=self.metadata, - guided=True, - confirm_changeset=True, - ) - - mock_deploy_context.assert_called_with( - template_file=ANY, - stack_name="sam-app", - s3_bucket="managed-s3-bucket", - force_upload=self.force_upload, - s3_prefix="sam-app", - kms_key_id=self.kms_key_id, - parameter_overrides=self.parameter_overrides, - capabilities=self.capabilities, - no_execute_changeset=self.no_execute_changeset, - role_arn=self.role_arn, - notification_arns=self.notification_arns, - fail_on_empty_changeset=self.fail_on_empty_changset, - tags=self.tags, - region="us-east-1", - profile=self.profile, - confirm_changeset=True, + self.assertEqual(MOCK_SAM_CONFIG.put.call_count, 7) + self.assertEqual( + MOCK_SAM_CONFIG.put.call_args_list, + [ + call(["deploy"], "parameters", "stack_name", "sam-app"), + call(["deploy"], "parameters", "s3_bucket", "managed-s3-bucket"), + call(["deploy"], "parameters", "s3_prefix", "sam-app"), + call(["deploy"], "parameters", "region", "us-east-1"), + call(["deploy"], "parameters", "confirm_changeset", True), + call(["deploy"], "parameters", "capabilities", "CAPABILITY_IAM"), + call(["deploy"], "parameters", "parameter_overrides", 'a="b"'), + ], ) - context_mock.run.assert_called_with() - self.assertEqual(mock_save_config.call_count, 0) - mock_managed_stack.assert_called_with(profile=self.profile, region="us-east-1") - self.assertEqual(context_mock.run.call_count, 1) - @patch("samcli.commands.package.command.click") @patch("samcli.commands.package.package_context.PackageContext") @patch("samcli.commands.deploy.command.click") @patch("samcli.commands.deploy.deploy_context.DeployContext") - @patch("samcli.commands.deploy.command.save_config") - @patch("samcli.commands.deploy.command.manage_stack") - @patch("samcli.commands.deploy.command.get_template_parameters") - @patch("samcli.commands.deploy.command.get_config_ctx") + @patch("samcli.commands.deploy.command.print_deploy_args") + @patch("samcli.commands.deploy.guided_context.manage_stack") + @patch("samcli.commands.deploy.guided_context.get_template_parameters") + @patch.object(GuidedConfig, "get_config_ctx", MagicMock(return_value=(None, get_mock_sam_config()))) + @patch("samcli.commands.deploy.guided_context.prompt") + @patch("samcli.commands.deploy.guided_context.confirm") def test_all_args_guided_no_params_no_save_config( self, - mock_get_config_ctx, + mock_confirm, + mock_prompt, mock_get_template_parameters, mock_managed_stack, - mock_save_config, + mock_print_deploy_args, mock_deploy_context, mock_deploy_click, mock_package_context, @@ -443,58 +424,57 @@ def test_all_args_guided_no_params_no_save_config( ): context_mock = Mock() - mock_sam_config = MagicMock() - mock_sam_config.exists = MagicMock(return_value=True) - mock_get_config_ctx.return_value = (None, mock_sam_config) mock_get_template_parameters.return_value = {} mock_deploy_context.return_value.__enter__.return_value = context_mock - mock_deploy_click.prompt = MagicMock(side_effect=["sam-app", "us-east-1", ("CAPABILITY_IAM",)]) - mock_deploy_click.confirm = MagicMock(side_effect=[True, False, False]) + mock_prompt.side_effect = ["sam-app", "us-east-1", ("CAPABILITY_IAM",)] + mock_confirm.side_effect = [True, False, False] mock_managed_stack.return_value = "managed-s3-bucket" - do_cli( - template_file=self.template_file, - stack_name=self.stack_name, - s3_bucket=None, - force_upload=self.force_upload, - s3_prefix=self.s3_prefix, - kms_key_id=self.kms_key_id, - parameter_overrides=self.parameter_overrides, - capabilities=self.capabilities, - no_execute_changeset=self.no_execute_changeset, - role_arn=self.role_arn, - notification_arns=self.notification_arns, - fail_on_empty_changeset=self.fail_on_empty_changset, - tags=self.tags, - region=self.region, - profile=self.profile, - use_json=self.use_json, - metadata=self.metadata, - guided=True, - confirm_changeset=True, - ) - - mock_deploy_context.assert_called_with( - template_file=ANY, - stack_name="sam-app", - s3_bucket="managed-s3-bucket", - force_upload=self.force_upload, - s3_prefix="sam-app", - kms_key_id=self.kms_key_id, - parameter_overrides=self.parameter_overrides, - capabilities=self.capabilities, - no_execute_changeset=self.no_execute_changeset, - role_arn=self.role_arn, - notification_arns=self.notification_arns, - fail_on_empty_changeset=self.fail_on_empty_changset, - tags=self.tags, - region="us-east-1", - profile=self.profile, - confirm_changeset=True, - ) - - context_mock.run.assert_called_with() - self.assertEqual(mock_save_config.call_count, 0) - mock_managed_stack.assert_called_with(profile=self.profile, region="us-east-1") - self.assertEqual(context_mock.run.call_count, 1) + with patch.object(GuidedConfig, "save_config", MagicMock(return_value=False)) as mock_save_config: + + do_cli( + template_file=self.template_file, + stack_name=self.stack_name, + s3_bucket=None, + force_upload=self.force_upload, + s3_prefix=self.s3_prefix, + kms_key_id=self.kms_key_id, + parameter_overrides=self.parameter_overrides, + capabilities=self.capabilities, + no_execute_changeset=self.no_execute_changeset, + role_arn=self.role_arn, + notification_arns=self.notification_arns, + fail_on_empty_changeset=self.fail_on_empty_changset, + tags=self.tags, + region=self.region, + profile=self.profile, + use_json=self.use_json, + metadata=self.metadata, + guided=True, + confirm_changeset=True, + ) + + mock_deploy_context.assert_called_with( + template_file=ANY, + stack_name="sam-app", + s3_bucket="managed-s3-bucket", + force_upload=self.force_upload, + s3_prefix="sam-app", + kms_key_id=self.kms_key_id, + parameter_overrides=self.parameter_overrides, + capabilities=self.capabilities, + no_execute_changeset=self.no_execute_changeset, + role_arn=self.role_arn, + notification_arns=self.notification_arns, + fail_on_empty_changeset=self.fail_on_empty_changset, + tags=self.tags, + region="us-east-1", + profile=self.profile, + confirm_changeset=True, + ) + + context_mock.run.assert_called_with() + self.assertEqual(mock_save_config.call_count, 0) + mock_managed_stack.assert_called_with(profile=self.profile, region="us-east-1") + self.assertEqual(context_mock.run.call_count, 1) diff --git a/tests/unit/commands/samconfig/test_samconfig.py b/tests/unit/commands/samconfig/test_samconfig.py index e34f8231f2..1112d9b57b 100644 --- a/tests/unit/commands/samconfig/test_samconfig.py +++ b/tests/unit/commands/samconfig/test_samconfig.py @@ -400,6 +400,66 @@ def test_deploy(self, do_cli_mock): None, ) + @patch("samcli.commands.deploy.command.do_cli") + def test_deploy_different_parameter_override_format(self, do_cli_mock): + + config_values = { + "template_file": "mytemplate.yaml", + "stack_name": "mystack", + "s3_bucket": "mybucket", + "force_upload": True, + "s3_prefix": "myprefix", + "kms_key_id": "mykms", + "parameter_overrides": 'Key1=Value1 Key2="Multiple spaces in the value"', + "capabilities": "cap1 cap2", + "no_execute_changeset": True, + "role_arn": "arn", + "notification_arns": "notify1 notify2", + "fail_on_empty_changeset": True, + "use_json": True, + "tags": 'a=tag1 b="tag with spaces"', + "metadata": '{"m1": "value1", "m2": "value2"}', + "guided": True, + "confirm_changeset": True, + "region": "myregion", + } + + with samconfig_parameters(["deploy"], self.scratch_dir, **config_values) as config_path: + + from samcli.commands.deploy.command import cli + + LOG.debug(Path(config_path).read_text()) + runner = CliRunner() + result = runner.invoke(cli, []) + + LOG.info(result.output) + LOG.info(result.exception) + if result.exception: + LOG.exception("Command failed", exc_info=result.exc_info) + self.assertIsNone(result.exception) + + do_cli_mock.assert_called_with( + str(Path(os.getcwd(), "mytemplate.yaml")), + "mystack", + "mybucket", + True, + "myprefix", + "mykms", + {"Key1": "Value1", "Key2": "Multiple spaces in the value"}, + ["cap1", "cap2"], + True, + "arn", + ["notify1", "notify2"], + True, + True, + {"a": "tag1", "b": '"tag with spaces"'}, + {"m1": "value1", "m2": "value2"}, + True, + True, + "myregion", + None, + ) + @patch("samcli.commands.logs.command.do_cli") def test_logs(self, do_cli_mock): config_values = {