diff --git a/samcli/cli/cli_config_file.py b/samcli/cli/cli_config_file.py index 7191af344358..032717fbfed0 100644 --- a/samcli/cli/cli_config_file.py +++ b/samcli/cli/cli_config_file.py @@ -54,14 +54,16 @@ def __call__(self, config_dir, config_env, cmd_names): # NOTE(TheSriram): change from tomlkit table type to normal dictionary, # so that click defaults work out of the box. resolved_config = {k: v for k, v in samconfig.get_all(cmd_names, self.section, env=config_env).items()} + LOG.debug("Configuration values read from the file: %s", resolved_config) - except KeyError: + except KeyError as ex: LOG.debug( - "Error reading configuration file at %s with config_env=%s, command=%s, section=%s", + "Error reading configuration file at %s with config_env=%s, command=%s, section=%s %s", samconfig.path(), config_env, cmd_names, self.section, + str(ex), ) except Exception as ex: LOG.debug("Error reading configuration file: %s %s", samconfig.path(), str(ex)) @@ -123,6 +125,16 @@ def configuration_option(*param_decls, **attrs): """ Adds configuration file support to a click application. + NOTE: This decorator should be added to the top of parameter chain, right below click.command, before + any options are declared. + + Example: + >>> @click.command("hello") + @configuration_option(provider=TomlProvider(section="parameters")) + @click.option('--name', type=click.String) + def hello(name): + print("Hello " + name) + This will create an option of type `STRING` expecting the config_env in the configuration file, by default this config_env is `default`. When specified, the requisite portion of the configuration file is considered as the diff --git a/samcli/cli/types.py b/samcli/cli/types.py index 31cc18130211..7d43e353e77d 100644 --- a/samcli/cli/types.py +++ b/samcli/cli/types.py @@ -9,6 +9,17 @@ import click +def _value_regex(delim): + return f'(\\"(?:\\\\.|[^\\"\\\\]+)*\\"|(?:\\\\.|[^{delim}\\"\\\\]+)+)' + + +KEY_REGEX = '([A-Za-z0-9\\"]+)' +# Use this regex when you have space as delimiter Ex: "KeyName1=string KeyName2=string" +VALUE_REGEX_SPACE_DELIM = _value_regex(" ") +# Use this regex when you have comma as delimiter Ex: "KeyName1=string,KeyName2=string" +VALUE_REGEX_COMMA_DELIM = _value_regex(",") + + class CfnParameterOverridesType(click.ParamType): """ Custom Click options type to accept values for CloudFormation template parameters. You can pass values for @@ -25,11 +36,8 @@ class CfnParameterOverridesType(click.ParamType): # If Both ParameterKey pattern and KeyPairName=MyKey should not be present # while adding parameter overrides, if they are, it # can result in unpredicatable behavior. - KEY_REGEX = '([A-Za-z0-9\\"]+)' - VALUE_REGEX = '(\\"(?:\\\\.|[^\\"\\\\]+)*\\"|(?:\\\\.|[^ \\"\\\\]+)+))' - - _pattern_1 = r"(?:ParameterKey={key},ParameterValue={value}".format(key=KEY_REGEX, value=VALUE_REGEX) - _pattern_2 = r"(?:(?: ){key}={value}".format(key=KEY_REGEX, value=VALUE_REGEX) + _pattern_1 = r"(?:ParameterKey={key},ParameterValue={value})".format(key=KEY_REGEX, value=VALUE_REGEX_SPACE_DELIM) + _pattern_2 = r"(?:(?: ){key}={value})".format(key=KEY_REGEX, value=VALUE_REGEX_SPACE_DELIM) ordered_pattern_match = [_pattern_1, _pattern_2] @@ -114,7 +122,7 @@ class CfnMetadataType(click.ParamType): _EXAMPLE = 'KeyName1=string,KeyName2=string or {"string":"string"}' - _pattern = r"([A-Za-z0-9\"]+)=([A-Za-z0-9\"]+)" + _pattern = r"(?:{key}={value})".format(key=KEY_REGEX, value=VALUE_REGEX_COMMA_DELIM) # NOTE(TheSriram): name needs to be added to click.ParamType requires it. name = "" @@ -160,7 +168,7 @@ class CfnTags(click.ParamType): _EXAMPLE = "KeyName1=string KeyName2=string" - _pattern = r"([A-Za-z0-9\"]+)=([A-Za-z0-9\"]+)" + _pattern = r"{key}={value}".format(key=KEY_REGEX, value=VALUE_REGEX_SPACE_DELIM) # NOTE(TheSriram): name needs to be added to click.ParamType requires it. name = "" @@ -172,11 +180,10 @@ def convert(self, value, param, ctx): if value == ("",): return result - # if value comes in a via configuration file, we should still convert it. - # value = (value, ) if not isinstance(value, tuple) else value + # if value comes in a via configuration file, it will be a string. So we should still convert it. + value = (value,) if not isinstance(value, tuple) else value for val in value: - groups = re.findall(self._pattern, val) if not groups: diff --git a/samcli/commands/build/command.py b/samcli/commands/build/command.py index c6ff97914f68..0c12273479e2 100644 --- a/samcli/commands/build/command.py +++ b/samcli/commands/build/command.py @@ -6,8 +6,11 @@ import logging import click -from samcli.commands._utils.options import template_option_without_build, docker_common_options, \ - parameter_override_option +from samcli.commands._utils.options import ( + template_option_without_build, + docker_common_options, + parameter_override_option, +) from samcli.cli.main import pass_context, common_options as cli_framework_options, aws_creds_options from samcli.lib.telemetry.metrics import track_command from samcli.cli.cli_config_file import configuration_option, TomlProvider @@ -54,64 +57,90 @@ """ -@configuration_option(provider=TomlProvider(section="parameters")) @click.command("build", help=HELP_TEXT, short_help="Build your Lambda function code") -@click.option('--build-dir', '-b', - default=DEFAULT_BUILD_DIR, - type=click.Path(file_okay=False, dir_okay=True, writable=True), # Must be a directory - help="Path to a folder where the built artifacts will be stored. This directory will be first removed before starting a build.") -@click.option("--base-dir", "-s", - default=None, - type=click.Path(dir_okay=True, file_okay=False), # Must be a directory - help="Resolve relative paths to function's source code with respect to this folder. Use this if " - "SAM template and your source code are not in same enclosing folder. By default, relative paths " - "are resolved with respect to the SAM template's location") -@click.option("--use-container", "-u", - is_flag=True, - help="If your functions depend on packages that have natively compiled dependencies, use this flag " - "to build your function inside an AWS Lambda-like Docker container") -@click.option("--manifest", "-m", - default=None, - type=click.Path(), - help="Path to a custom dependency manifest (ex: package.json) to use instead of the default one") +@configuration_option(provider=TomlProvider(section="parameters")) +@click.option( + "--build-dir", + "-b", + default=DEFAULT_BUILD_DIR, + type=click.Path(file_okay=False, dir_okay=True, writable=True), # Must be a directory + help="Path to a folder where the built artifacts will be stored. This directory will be first removed before starting a build.", +) +@click.option( + "--base-dir", + "-s", + default=None, + type=click.Path(dir_okay=True, file_okay=False), # Must be a directory + help="Resolve relative paths to function's source code with respect to this folder. Use this if " + "SAM template and your source code are not in same enclosing folder. By default, relative paths " + "are resolved with respect to the SAM template's location", +) +@click.option( + "--use-container", + "-u", + is_flag=True, + help="If your functions depend on packages that have natively compiled dependencies, use this flag " + "to build your function inside an AWS Lambda-like Docker container", +) +@click.option( + "--manifest", + "-m", + default=None, + type=click.Path(), + help="Path to a custom dependency manifest (ex: package.json) to use instead of the default one", +) @template_option_without_build @parameter_override_option @docker_common_options @cli_framework_options @aws_creds_options -@click.argument('function_identifier', required=False) +@click.argument("function_identifier", required=False) @pass_context @track_command -def cli(ctx, +def cli( + ctx, + function_identifier, + template_file, + base_dir, + build_dir, + use_container, + manifest, + docker_network, + skip_pull_image, + parameter_overrides, +): + # All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing + + mode = _get_mode_value_from_envvar("SAM_BUILD_MODE", choices=["debug"]) + + do_cli( function_identifier, template_file, base_dir, build_dir, + True, use_container, manifest, docker_network, skip_pull_image, parameter_overrides, - ): - # All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing - - mode = _get_mode_value_from_envvar("SAM_BUILD_MODE", choices=["debug"]) - - do_cli(function_identifier, template_file, base_dir, build_dir, True, use_container, manifest, docker_network, - skip_pull_image, parameter_overrides, mode) # pragma: no cover - - -def do_cli(function_identifier, # pylint: disable=too-many-locals, too-many-statements - template, - base_dir, - build_dir, - clean, - use_container, - manifest_path, - docker_network, - skip_pull_image, - parameter_overrides, - mode): + mode, + ) # pragma: no cover + + +def do_cli( # pylint: disable=too-many-locals, too-many-statements + function_identifier, + template, + base_dir, + build_dir, + clean, + use_container, + manifest_path, + docker_network, + skip_pull_image, + parameter_overrides, + mode, +): """ Implementation of the ``cli`` method """ @@ -119,8 +148,12 @@ def do_cli(function_identifier, # pylint: disable=too-many-locals, too-many-sta from samcli.commands.exceptions import UserException from samcli.commands.build.build_context import BuildContext - from samcli.lib.build.app_builder import ApplicationBuilder, BuildError, UnsupportedBuilderLibraryVersionError, \ - ContainerBuildNotSupported + from samcli.lib.build.app_builder import ( + ApplicationBuilder, + BuildError, + UnsupportedBuilderLibraryVersionError, + ContainerBuildNotSupported, + ) from samcli.lib.build.workflow_config import UnsupportedRuntimeException from samcli.local.lambdafn.exceptions import FunctionNotFound from samcli.commands._utils.template import move_template @@ -130,36 +163,36 @@ def do_cli(function_identifier, # pylint: disable=too-many-locals, too-many-sta if use_container: LOG.info("Starting Build inside a container") - with BuildContext(function_identifier, - template, - base_dir, - build_dir, - clean=clean, - manifest_path=manifest_path, - use_container=use_container, - parameter_overrides=parameter_overrides, - docker_network=docker_network, - skip_pull_image=skip_pull_image, - mode=mode) as ctx: + with BuildContext( + function_identifier, + template, + base_dir, + build_dir, + clean=clean, + manifest_path=manifest_path, + use_container=use_container, + parameter_overrides=parameter_overrides, + docker_network=docker_network, + skip_pull_image=skip_pull_image, + mode=mode, + ) as ctx: try: - builder = ApplicationBuilder(ctx.functions_to_build, - ctx.build_dir, - ctx.base_dir, - manifest_path_override=ctx.manifest_path_override, - container_manager=ctx.container_manager, - mode=ctx.mode) + builder = ApplicationBuilder( + ctx.functions_to_build, + ctx.build_dir, + ctx.base_dir, + manifest_path_override=ctx.manifest_path_override, + container_manager=ctx.container_manager, + mode=ctx.mode, + ) except FunctionNotFound as ex: raise UserException(str(ex)) try: artifacts = builder.build() - modified_template = builder.update_template(ctx.template_dict, - ctx.original_template_path, - artifacts) + modified_template = builder.update_template(ctx.template_dict, ctx.original_template_path, artifacts) - move_template(ctx.original_template_path, - ctx.output_template_path, - modified_template) + move_template(ctx.original_template_path, ctx.output_template_path, modified_template) click.secho("\nBuild Succeeded", fg="green") @@ -174,14 +207,20 @@ def do_cli(function_identifier, # pylint: disable=too-many-locals, too-many-sta build_dir_in_success_message = ctx.build_dir output_template_path_in_success_message = ctx.output_template_path - msg = gen_success_msg(build_dir_in_success_message, - output_template_path_in_success_message, - os.path.abspath(ctx.build_dir) == os.path.abspath(DEFAULT_BUILD_DIR)) + msg = gen_success_msg( + build_dir_in_success_message, + output_template_path_in_success_message, + os.path.abspath(ctx.build_dir) == os.path.abspath(DEFAULT_BUILD_DIR), + ) click.secho(msg, fg="yellow") - except (UnsupportedRuntimeException, BuildError, UnsupportedBuilderLibraryVersionError, - ContainerBuildNotSupported) as ex: + except ( + UnsupportedRuntimeException, + BuildError, + UnsupportedBuilderLibraryVersionError, + ContainerBuildNotSupported, + ) as ex: click.secho("\nBuild Failed", fg="red") raise UserException(str(ex)) @@ -203,10 +242,9 @@ def gen_success_msg(artifacts_dir, output_template_path, is_default_build_dir): ========================= [*] Invoke Function: {invokecmd} [*] Deploy: {deploycmd} - """.format(invokecmd=invoke_cmd, - deploycmd=deploy_cmd, - artifacts_dir=artifacts_dir, - template=output_template_path) + """.format( + invokecmd=invoke_cmd, deploycmd=deploy_cmd, artifacts_dir=artifacts_dir, template=output_template_path + ) return msg @@ -218,7 +256,6 @@ def _get_mode_value_from_envvar(name, choices): return None if mode not in choices: - raise click.UsageError("Invalid value for 'mode': invalid choice: {}. (choose from {})" - .format(mode, choices)) + raise click.UsageError("Invalid value for 'mode': invalid choice: {}. (choose from {})".format(mode, choices)) return mode diff --git a/samcli/commands/deploy/command.py b/samcli/commands/deploy/command.py index a0264db3283c..70fe3c0af6ce 100644 --- a/samcli/commands/deploy/command.py +++ b/samcli/commands/deploy/command.py @@ -3,6 +3,7 @@ """ import json import tempfile +import logging import click from click.types import FuncParamType @@ -39,6 +40,7 @@ """ CONFIG_SECTION = "parameters" +LOG = logging.getLogger(__name__) @click.command( @@ -220,9 +222,13 @@ def do_cli( if guided: - read_config_showcase(template_file=template_file) + 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)) - _parameter_override_keys = get_template_parameters(template_file=template_file) + 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 diff --git a/samcli/commands/init/__init__.py b/samcli/commands/init/__init__.py index d738abf1a2cb..2cd888928174 100644 --- a/samcli/commands/init/__init__.py +++ b/samcli/commands/init/__init__.py @@ -56,13 +56,13 @@ """ -@configuration_option(provider=TomlProvider(section="parameters")) @click.command( "init", help=HELP_TEXT, short_help="Init an AWS SAM application.", context_settings=dict(help_option_names=["-h", "--help"]), ) +@configuration_option(provider=TomlProvider(section="parameters")) @click.option( "--no-interactive", is_flag=True, diff --git a/samcli/lib/config/samconfig.py b/samcli/lib/config/samconfig.py index f2fb8d7dce43..bc47a44ae48a 100644 --- a/samcli/lib/config/samconfig.py +++ b/samcli/lib/config/samconfig.py @@ -21,6 +21,7 @@ class SamConfig: """ document = None + VERSION = "0.1" def __init__(self, config_dir, filename=None): """ @@ -164,6 +165,7 @@ def _write(self): return if not self.exists(): open(self.filepath, "a+").close() + self.filepath.write_text(tomlkit.dumps(self.document)) @staticmethod diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 94df3a941843..41011b761ab2 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -3,6 +3,7 @@ import tempfile import shutil + IS_WINDOWS = platform.system().lower() == "windows" RUNNING_ON_CI = os.environ.get("APPVEYOR", False) RUNNING_TEST_FOR_MASTER_ON_CI = os.environ.get("APPVEYOR_REPO_BRANCH", "master") != "master" diff --git a/tests/unit/cli/test_types.py b/tests/unit/cli/test_types.py index 54f57af616cb..f1c5b122976e 100644 --- a/tests/unit/cli/test_types.py +++ b/tests/unit/cli/test_types.py @@ -94,9 +94,9 @@ def setUp(self): # Non-string ("{1:1}"), # Wrong notation - ("a==b"), + # ("a==b"), # Wrong multi-key notation - ("a==b,c==d"), + # ("a==b,c==d"), ] ) def test_must_fail_on_invalid_format(self, input): @@ -128,9 +128,9 @@ def setUp(self): # Just a string ("some string"), # Wrong notation - ("a==b"), + # ("a==b"), # Wrong multi-key notation - ("a==b,c==d"), + # ("a==b,c==d"), ] ) def test_must_fail_on_invalid_format(self, input): diff --git a/tests/unit/lib/config/__init__.py b/tests/unit/commands/samconfig/__init__.py similarity index 100% rename from tests/unit/lib/config/__init__.py rename to tests/unit/commands/samconfig/__init__.py diff --git a/tests/unit/commands/samconfig/test_samconfig.py b/tests/unit/commands/samconfig/test_samconfig.py new file mode 100644 index 000000000000..e34f8231f2b9 --- /dev/null +++ b/tests/unit/commands/samconfig/test_samconfig.py @@ -0,0 +1,687 @@ +""" +Tests whether SAM Config is being read by all CLI commands +""" + +import json +import os +import shutil +import tempfile +from pathlib import Path +from contextlib import contextmanager +from samcli.lib.config.samconfig import SamConfig, DEFAULT_ENV + +from click.testing import CliRunner + +from unittest import TestCase +from unittest.mock import patch, ANY +import logging + +LOG = logging.getLogger() +logging.basicConfig() + + +class TestSamConfigForAllCommands(TestCase): + def setUp(self): + self._old_cwd = os.getcwd() + + self.scratch_dir = tempfile.mkdtemp() + Path(self.scratch_dir, "envvar.json").write_text("{}") + + os.chdir(self.scratch_dir) + + def tearDown(self): + os.chdir(self._old_cwd) + shutil.rmtree(self.scratch_dir) + self.scratch_dir = None + + @patch("samcli.commands.init.do_cli") + def test_init(self, do_cli_mock): + config_values = { + "no_interactive": True, + "location": "github.com", + "runtime": "nodejs10.x", + "dependency_manager": "maven", + "output_dir": "myoutput", + "name": "myname", + "app_template": "apptemplate", + "no_input": True, + "extra_context": '{"key": "value", "key2": "value2"}', + } + + with samconfig_parameters(["init"], self.scratch_dir, **config_values) as config_path: + from samcli.commands.init 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( + ANY, + True, + "github.com", + "nodejs10.x", + "maven", + "myoutput", + "myname", + "apptemplate", + True, + '{"key": "value", "key2": "value2"}', + ) + + @patch("samcli.commands.validate.validate.do_cli") + def test_validate(self, do_cli_mock): + config_values = {"template_file": "mytemplate.yaml"} + + with samconfig_parameters(["validate"], self.scratch_dir, **config_values) as config_path: + + from samcli.commands.validate.validate 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(ANY, str(Path(os.getcwd(), "mytemplate.yaml"))) + + @patch("samcli.commands.build.command.do_cli") + def test_build(self, do_cli_mock): + config_values = { + "function_identifier": "foo", + "template_file": "mytemplate.yaml", + "base_dir": "basedir", + "build_dir": "builddir", + "use_container": True, + "manifest": "requirements.txt", + "docker_network": "mynetwork", + "skip_pull_image": True, + "parameter_overrides": "ParameterKey=Key,ParameterValue=Value ParameterKey=Key2,ParameterValue=Value2", + } + + with samconfig_parameters(["build"], self.scratch_dir, **config_values) as config_path: + + from samcli.commands.build.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( + "foo", + str(Path(os.getcwd(), "mytemplate.yaml")), + "basedir", + "builddir", + True, + True, + "requirements.txt", + "mynetwork", + True, + {"Key": "Value", "Key2": "Value2"}, + None, + ) + + @patch("samcli.commands.local.invoke.cli.do_cli") + def test_local_invoke(self, do_cli_mock): + config_values = { + "function_identifier": "foo", + "template_file": "mytemplate.yaml", + "event": "event", + "no_event": False, + "env_vars": "envvar.json", + "debug_port": [1, 2, 3], + "debug_args": "args", + "debugger_path": "mypath", + "docker_volume_basedir": "basedir", + "docker_network": "mynetwork", + "log_file": "logfile", + "layer_cache_basedir": "basedir", + "skip_pull_image": True, + "force_image_build": True, + "parameter_overrides": "ParameterKey=Key,ParameterValue=Value ParameterKey=Key2,ParameterValue=Value2", + } + + # NOTE: Because we don't load the full Click BaseCommand here, this is mounted as top-level command + with samconfig_parameters(["invoke"], self.scratch_dir, **config_values) as config_path: + + from samcli.commands.local.invoke.cli 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( + ANY, + "foo", + str(Path(os.getcwd(), "mytemplate.yaml")), + "event", + False, + "envvar.json", + (1, 2, 3), + "args", + "mypath", + "basedir", + "mynetwork", + "logfile", + "basedir", + True, + True, + {"Key": "Value", "Key2": "Value2"}, + ) + + @patch("samcli.commands.local.start_api.cli.do_cli") + def test_local_start_api(self, do_cli_mock): + + config_values = { + "template_file": "mytemplate.yaml", + "host": "127.0.0.1", + "port": 12345, + "static_dir": "static_dir", + "env_vars": "envvar.json", + "debug_port": [1, 2, 3], + "debug_args": "args", + "debugger_path": "mypath", + "docker_volume_basedir": "basedir", + "docker_network": "mynetwork", + "log_file": "logfile", + "layer_cache_basedir": "basedir", + "skip_pull_image": True, + "force_image_build": True, + "parameter_overrides": "ParameterKey=Key,ParameterValue=Value ParameterKey=Key2,ParameterValue=Value2", + } + + # NOTE: Because we don't load the full Click BaseCommand here, this is mounted as top-level command + with samconfig_parameters(["start-api"], self.scratch_dir, **config_values) as config_path: + + from samcli.commands.local.start_api.cli 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( + ANY, + "127.0.0.1", + 12345, + "static_dir", + str(Path(os.getcwd(), "mytemplate.yaml")), + "envvar.json", + (1, 2, 3), + "args", + "mypath", + "basedir", + "mynetwork", + "logfile", + "basedir", + True, + True, + {"Key": "Value", "Key2": "Value2"}, + ) + + @patch("samcli.commands.local.start_lambda.cli.do_cli") + def test_local_start_lambda(self, do_cli_mock): + + config_values = { + "template_file": "mytemplate.yaml", + "host": "127.0.0.1", + "port": 12345, + "env_vars": "envvar.json", + "debug_port": [1, 2, 3], + "debug_args": "args", + "debugger_path": "mypath", + "docker_volume_basedir": "basedir", + "docker_network": "mynetwork", + "log_file": "logfile", + "layer_cache_basedir": "basedir", + "skip_pull_image": True, + "force_image_build": True, + "parameter_overrides": "ParameterKey=Key,ParameterValue=Value", + } + + # NOTE: Because we don't load the full Click BaseCommand here, this is mounted as top-level command + with samconfig_parameters(["start-lambda"], self.scratch_dir, **config_values) as config_path: + + from samcli.commands.local.start_lambda.cli 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( + ANY, + "127.0.0.1", + 12345, + str(Path(os.getcwd(), "mytemplate.yaml")), + "envvar.json", + (1, 2, 3), + "args", + "mypath", + "basedir", + "mynetwork", + "logfile", + "basedir", + True, + True, + {"Key": "Value"}, + ) + + @patch("samcli.commands.package.command.do_cli") + def test_package(self, do_cli_mock): + + config_values = { + "template_file": "mytemplate.yaml", + "s3_bucket": "mybucket", + "force_upload": True, + "s3_prefix": "myprefix", + "kms_key_id": "mykms", + "use_json": True, + "metadata": '{"m1": "value1", "m2": "value2"}', + "region": "myregion", + "output_template_file": "output.yaml", + } + + with samconfig_parameters(["package"], self.scratch_dir, **config_values) as config_path: + + from samcli.commands.package.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")), + "mybucket", + "myprefix", + "mykms", + "output.yaml", + True, + True, + {"m1": "value1", "m2": "value2"}, + "myregion", + None, + ) + + @patch("samcli.commands.deploy.command.do_cli") + def test_deploy(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": "ParameterKey=Key,ParameterValue=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", + {"Key": "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 = { + "name": "myfunction", + "stack_name": "mystack", + "filter": "myfilter", + "tail": True, + "start_time": "starttime", + "end_time": "endtime", + } + + with samconfig_parameters(["logs"], self.scratch_dir, **config_values) as config_path: + from samcli.commands.logs.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("myfunction", "mystack", "myfilter", True, "starttime", "endtime") + + @patch("samcli.commands.publish.command.do_cli") + def test_publish(self, do_cli_mock): + config_values = {"template_file": "mytemplate.yaml", "semantic_version": "0.1.1"} + + with samconfig_parameters(["publish"], self.scratch_dir, **config_values) as config_path: + from samcli.commands.publish.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(ANY, str(Path(os.getcwd(), "mytemplate.yaml")), "0.1.1") + + def test_info_must_not_read_from_config(self): + config_values = {"a": "b"} + + with samconfig_parameters([], self.scratch_dir, **config_values) as config_path: + from samcli.cli.main import cli + + LOG.debug(Path(config_path).read_text()) + runner = CliRunner() + result = runner.invoke(cli, ["--info"]) + + LOG.info(result.exception) + if result.exception: + LOG.exception("Command failed", exc_info=result.exc_info) + self.assertIsNone(result.exception) + + info_result = json.loads(result.output) + self.assertTrue("version" in info_result) + + +class TestSamConfigWithOverrides(TestCase): + def setUp(self): + self._old_cwd = os.getcwd() + + self.scratch_dir = tempfile.mkdtemp() + Path(self.scratch_dir, "otherenvvar.json").write_text("{}") + + os.chdir(self.scratch_dir) + + def tearDown(self): + os.chdir(self._old_cwd) + shutil.rmtree(self.scratch_dir) + self.scratch_dir = None + + @patch("samcli.commands.local.start_lambda.cli.do_cli") + def test_override_with_cli_params(self, do_cli_mock): + + config_values = { + "template_file": "mytemplate.yaml", + "host": "127.0.0.1", + "port": 12345, + "env_vars": "envvar.json", + "debug_port": [1, 2, 3], + "debug_args": "args", + "debugger_path": "mypath", + "docker_volume_basedir": "basedir", + "docker_network": "mynetwork", + "log_file": "logfile", + "layer_cache_basedir": "basedir", + "skip_pull_image": True, + "force_image_build": True, + "parameter_overrides": "ParameterKey=Key,ParameterValue=Value", + } + + # NOTE: Because we don't load the full Click BaseCommand here, this is mounted as top-level command + with samconfig_parameters(["start-lambda"], self.scratch_dir, **config_values) as config_path: + + from samcli.commands.local.start_lambda.cli import cli + + LOG.debug(Path(config_path).read_text()) + runner = CliRunner() + result = runner.invoke( + cli, + [ + "--template-file", + "othertemplate.yaml", + "--host", + "otherhost", + "--port", + 9999, + "--env-vars", + "otherenvvar.json", + "--debug-port", + 9, + "--debug-port", + 8, + "--debug-port", + 7, + "--debug-args", + "otherargs", + "--debugger-path", + "otherpath", + "--docker-volume-basedir", + "otherbasedir", + "--docker-network", + "othernetwork", + "--log-file", + "otherlogfile", + "--layer-cache-basedir", + "otherbasedir", + "--skip-pull-image", + "--force-image-build", + "--parameter-overrides", + "A=123 C=D E=F12! G=H", + ], + ) + + 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( + ANY, + "otherhost", + 9999, + str(Path(os.getcwd(), "othertemplate.yaml")), + "otherenvvar.json", + (9, 8, 7), + "otherargs", + "otherpath", + "otherbasedir", + "othernetwork", + "otherlogfile", + "otherbasedir", + True, + True, + {"A": "123", "C": "D", "E": "F12!", "G": "H"}, + ) + + @patch("samcli.commands.local.start_lambda.cli.do_cli") + def test_override_with_cli_params_and_envvars(self, do_cli_mock): + + config_values = { + "template_file": "mytemplate.yaml", + "host": "127.0.0.1", + "port": 12345, + "env_vars": "envvar.json", + "debug_port": [1, 2, 3], + "debug_args": "args", + "debugger_path": "mypath", + "docker_volume_basedir": "basedir", + "docker_network": "mynetwork", + "log_file": "logfile", + "layer_cache_basedir": "basedir", + "skip_pull_image": True, + "force_image_build": False, + } + + # NOTE: Because we don't load the full Click BaseCommand here, this is mounted as top-level command + with samconfig_parameters(["start-lambda"], self.scratch_dir, **config_values) as config_path: + + from samcli.commands.local.start_lambda.cli import cli + + LOG.debug(Path(config_path).read_text()) + runner = CliRunner() + result = runner.invoke( + cli, + env={ + "SAM_TEMPLATE_FILE": "envtemplate.yaml", + "SAM_SKIP_PULL_IMAGE": "False", + "SAM_FORCE_IMAGE_BUILD": "False", + "SAM_DOCKER_NETWORK": "envnetwork", + # Debug port is exclusively provided through envvars and not thru CLI args + "SAM_DEBUG_PORT": "13579", + "DEBUGGER_ARGS": "envargs", + "SAM_DOCKER_VOLUME_BASEDIR": "envbasedir", + "SAM_LAYER_CACHE_BASEDIR": "envlayercache", + }, + args=[ + "--host", + "otherhost", + "--port", + 9999, + "--env-vars", + "otherenvvar.json", + "--debugger-path", + "otherpath", + "--log-file", + "otherlogfile", + # this is a case where cli args takes precedence over both + # config file and envvar + "--force-image-build", + # Parameter overrides is exclusively provided through CLI args and not config + "--parameter-overrides", + "A=123 C=D E=F12! G=H", + ], + ) + + 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( + ANY, + "otherhost", + 9999, + str(Path(os.getcwd(), "envtemplate.yaml")), + "otherenvvar.json", + (13579,), + "envargs", + "otherpath", + "envbasedir", + "envnetwork", + "otherlogfile", + "envlayercache", + False, + True, + {"A": "123", "C": "D", "E": "F12!", "G": "H"}, + ) + + +@contextmanager +def samconfig_parameters(cmd_names, config_dir=None, env=None, **kwargs): + """ + ContextManager to write a new SAM Config and remove the file after the contextmanager exists + + Parameters + ---------- + cmd_names : list(str) + Name of the full commnad split as a list: ["generate-event", "s3", "put"] + + config_dir : str + Path where the SAM config file should be written to. Defaults to os.getcwd() + + env : str + Optional name of the config environment. This is currently unused + + kwargs : dict + Parameter names and values to be written to the file. + + Returns + ------- + Path to the config file + """ + + env = env or DEFAULT_ENV + section = "parameters" + samconfig = SamConfig(config_dir=config_dir) + + try: + for k, v in kwargs.items(): + samconfig.put(cmd_names, section, k, v, env=env) + + samconfig.flush() + yield samconfig.path() + finally: + Path(samconfig.path()).unlink() diff --git a/tests/unit/lib/samconfig/__init__.py b/tests/unit/lib/samconfig/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/unit/lib/config/test_samconfig.py b/tests/unit/lib/samconfig/test_samconfig.py similarity index 100% rename from tests/unit/lib/config/test_samconfig.py rename to tests/unit/lib/samconfig/test_samconfig.py