diff --git a/samcli/commands/init/__init__.py b/samcli/commands/init/__init__.py index db92f30765..23c4537fb9 100644 --- a/samcli/commands/init/__init__.py +++ b/samcli/commands/init/__init__.py @@ -3,9 +3,12 @@ Init command to scaffold a project app from a template """ import logging +import json +from json import JSONDecodeError import click +from samcli.commands.exceptions import UserException from samcli.cli.main import pass_context, common_options, global_cfg from samcli.local.common.runtime_template import RUNTIMES, SUPPORTED_DEP_MANAGERS from samcli.lib.telemetry.metrics import track_command @@ -86,12 +89,32 @@ default=False, help="Disable Cookiecutter prompting and accept default values defined template config", ) +@click.option( + "--extra_context", + default=None, + help="Override any custom parameters in the template's cookiecutter.json configuration e.g. " + "" + '{"customParam1": "customValue1", "customParam2":"customValue2"}' + """ """, + required=False, +) @common_options @pass_context @track_command -def cli(ctx, no_interactive, location, runtime, dependency_manager, output_dir, name, app_template, no_input): +def cli( + ctx, no_interactive, location, runtime, dependency_manager, output_dir, name, app_template, no_input, extra_context +): do_cli( - ctx, no_interactive, location, runtime, dependency_manager, output_dir, name, app_template, no_input + ctx, + no_interactive, + location, + runtime, + dependency_manager, + output_dir, + name, + app_template, + no_input, + extra_context, ) # pragma: no cover @@ -106,9 +129,9 @@ def do_cli( name, app_template, no_input, + extra_context, auto_clone=True, ): - from samcli.commands.exceptions import UserException from samcli.commands.init.init_generator import do_generate from samcli.commands.init.init_templates import InitTemplates from samcli.commands.init.interactive_init_flow import do_interactive @@ -126,12 +149,15 @@ def do_cli( # check for required parameters if location or (name and runtime and dependency_manager and app_template): # need to turn app_template into a location before we generate - extra_context = None if app_template: templates = InitTemplates(no_interactive, auto_clone) location = templates.location_from_app_template(runtime, dependency_manager, app_template) no_input = True - extra_context = {"project_name": name, "runtime": runtime} + default_context = {"project_name": name, "runtime": runtime} + if extra_context is None: + extra_context = default_context + else: + extra_context = _merge_extra_context(default_context, extra_context) if not output_dir: output_dir = "." do_generate(location, runtime, dependency_manager, output_dir, name, no_input, extra_context) @@ -149,3 +175,13 @@ def do_cli( else: # proceed to interactive state machine, which will call do_generate do_interactive(location, runtime, dependency_manager, output_dir, name, app_template, no_input) + + +def _merge_extra_context(default_context, extra_context): + try: + extra_context_dict = json.loads(extra_context) + except JSONDecodeError: + raise UserException( + "Parse error reading the --extra-content parameter. The value of this parameter must be valid JSON." + ) + return {**extra_context_dict, **default_context} diff --git a/tests/integration/init/test_init_command.py b/tests/integration/init/test_init_command.py index 97e50a240b..9ba5418bc1 100644 --- a/tests/integration/init/test_init_command.py +++ b/tests/integration/init/test_init_command.py @@ -101,6 +101,32 @@ def test_init_command_java_gradle(self): self.assertEqual(return_code, 0) self.assertTrue(os.path.isdir(temp + "/sam-app-gradle")) + def test_init_command_with_extra_context_parameter(self): + with tempfile.TemporaryDirectory() as temp: + process = Popen( + [ + TestBasicInitCommand._get_command(), + "init", + "--runtime", + "java8", + "--dependency-manager", + "maven", + "--app-template", + "hello-world", + "--name", + "sam-app-maven", + "--no-interactive", + "--extra_context", + '{"schema_name": "codedeploy", "schema_type": "aws"}', + "-o", + temp, + ] + ) + return_code = process.wait() + + self.assertEqual(return_code, 0) + self.assertTrue(os.path.isdir(temp + "/sam-app-maven")) + @staticmethod def _get_command(): command = "sam" diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index 0e9ebc364e..72125793d5 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -1,7 +1,6 @@ from unittest import TestCase from unittest.mock import patch, ANY -import click from click.testing import CliRunner from samcli.commands.init import cli as init_cmd @@ -21,7 +20,8 @@ def setUp(self): self.name = "testing project" self.app_template = "hello-world" self.no_input = False - self.extra_context = {"project_name": "testing project", "runtime": "python3.6"} + self.extra_context = '{"project_name": "testing project", "runtime": "python3.6"}' + self.extra_context_as_json = {"project_name": "testing project", "runtime": "python3.6"} @patch("samcli.commands.init.init_templates.InitTemplates._shared_dir_check") @patch("samcli.commands.init.init_generator.generate_project") @@ -38,6 +38,7 @@ def test_init_cli(self, generate_project_patch, sd_mock): name=self.name, app_template=self.app_template, no_input=self.no_input, + extra_context=None, auto_clone=False, ) @@ -50,7 +51,7 @@ def test_init_cli(self, generate_project_patch, sd_mock): self.output_dir, self.name, True, - self.extra_context, + self.extra_context_as_json, ) @patch("samcli.commands.init.init_templates.InitTemplates._shared_dir_check") @@ -68,6 +69,7 @@ def test_init_fails_invalid_template(self, sd_mock): name=self.name, app_template="wrong-and-bad", no_input=self.no_input, + extra_context=None, auto_clone=False, ) @@ -86,6 +88,7 @@ def test_init_fails_invalid_dep_mgr(self, sd_mock): name=self.name, app_template=self.app_template, no_input=self.no_input, + extra_context=None, auto_clone=False, ) @@ -224,6 +227,7 @@ def test_init_cli_missing_params_fails(self): name=None, app_template=None, no_input=True, + extra_context=None, auto_clone=False, ) @@ -241,6 +245,7 @@ def test_init_cli_mutually_exclusive_params_fails(self): name=self.name, app_template="fails-anyways", no_input=self.no_input, + extra_context=None, auto_clone=False, ) @@ -266,9 +271,110 @@ def test_init_cli_generate_project_fails(self, generate_project_patch, sd_mock): name=self.name, app_template=None, no_input=self.no_input, + extra_context=None, auto_clone=False, ) generate_project_patch.assert_called_with( self.location, self.runtime, self.dependency_manager, self.output_dir, self.name, self.no_input ) + + @patch("samcli.commands.init.init_generator.generate_project") + def test_init_cli_with_extra_context_parameter_not_passed(self, generate_project_patch): + # GIVEN generate_project successfully created a project + # WHEN a project name has been passed + init_cli( + ctx=self.ctx, + no_interactive=self.no_interactive, + location=self.location, + runtime=self.runtime, + dependency_manager=self.dependency_manager, + output_dir=self.output_dir, + name=self.name, + app_template=self.app_template, + no_input=self.no_input, + extra_context=None, + auto_clone=False, + ) + + # THEN we should receive no errors + generate_project_patch.assert_called_once_with( + ANY, self.runtime, self.dependency_manager, ".", self.name, True, self.extra_context_as_json + ) + + @patch("samcli.commands.init.init_generator.generate_project") + def test_init_cli_with_extra_context_parameter_passed(self, generate_project_patch): + # GIVEN generate_project successfully created a project + # WHEN a project name has been passed + init_cli( + ctx=self.ctx, + no_interactive=self.no_interactive, + location=self.location, + runtime=self.runtime, + dependency_manager=self.dependency_manager, + output_dir=self.output_dir, + name=self.name, + app_template=self.app_template, + no_input=self.no_input, + extra_context='{"schema_name":"events", "schema_type":"aws"}', + auto_clone=False, + ) + + # THEN we should receive no errors + generate_project_patch.assert_called_once_with( + ANY, + self.runtime, + self.dependency_manager, + ".", + self.name, + True, + {"project_name": "testing project", "runtime": "python3.6", "schema_name": "events", "schema_type": "aws"}, + ) + + @patch("samcli.commands.init.init_generator.generate_project") + def test_init_cli_with_extra_context_not_overriding_default_parameter(self, generate_project_patch): + # GIVEN generate_project successfully created a project + # WHEN a project name has been passed + init_cli( + ctx=self.ctx, + no_interactive=self.no_interactive, + location=self.location, + runtime=self.runtime, + dependency_manager=self.dependency_manager, + output_dir=self.output_dir, + name=self.name, + app_template=self.app_template, + no_input=self.no_input, + extra_context='{"project_name": "my_project", "runtime": "java8", "schema_name":"events", "schema_type": "aws"}', + auto_clone=False, + ) + + # THEN we should receive no errors + generate_project_patch.assert_called_once_with( + ANY, + self.runtime, + self.dependency_manager, + ".", + self.name, + True, + {"project_name": "testing project", "runtime": "python3.6", "schema_name": "events", "schema_type": "aws"}, + ) + + @patch("samcli.commands.init.init_generator.generate_project") + def test_init_cli_with_extra_context_input_as_wrong_json_raises_exception(self, generate_project_patch): + # GIVEN generate_project successfully created a project + # WHEN a project name has been passed + with self.assertRaises(UserException): + init_cli( + ctx=self.ctx, + no_interactive=self.no_interactive, + location=self.location, + runtime=self.runtime, + dependency_manager=self.dependency_manager, + output_dir=self.output_dir, + name=self.name, + app_template=self.app_template, + no_input=self.no_input, + extra_context='{"project_name", "my_project", "runtime": "java8", "schema_name":"events", "schema_type": "aws"}', + auto_clone=False, + )