From a5e2d3452bf148c43d24f2bbc27826cb04846d92 Mon Sep 17 00:00:00 2001 From: Ahmed Elbayaa Date: Sun, 9 May 2021 17:56:58 -0700 Subject: [PATCH 1/4] Enable --app-template argument for Image package-type while generating a new SAM project using 'sam init' --- samcli/commands/init/__init__.py | 19 +- tests/unit/commands/init/test_cli.py | 248 +++++++++++++++++++++++++++ 2 files changed, 258 insertions(+), 9 deletions(-) diff --git a/samcli/commands/init/__init__.py b/samcli/commands/init/__init__.py index f53b77daa5b..969eae238f9 100644 --- a/samcli/commands/init/__init__.py +++ b/samcli/commands/init/__init__.py @@ -163,7 +163,7 @@ def wrapped(*args, **kwargs): default=None, help="Lambda Image of your app", cls=Mutex, - not_required=["location", "app_template", "runtime"], + not_required=["location", "runtime"], ) @click.option( "-d", @@ -182,7 +182,7 @@ def wrapped(*args, **kwargs): help="Identifier of the managed application template you want to use. " "If not sure, call 'sam init' without options for an interactive workflow.", cls=Mutex, - not_required=["location", "base_image"], + not_required=["location"], ) @click.option( "--no-input", @@ -277,13 +277,14 @@ def do_cli( if package_type == IMAGE and image_bool: base_image, runtime = _get_runtime_from_image(base_image) options = templates.init_options(package_type, runtime, base_image, dependency_manager) - if len(options) == 1: - app_template = options[0].get("appTemplate") - elif len(options) > 1: - raise LambdaImagesTemplateException( - "Multiple lambda image application templates found. " - "This should not be possible, please raise an issue." - ) + if not app_template: + if len(options) == 1: + app_template = options[0].get("appTemplate") + elif len(options) > 1: + raise LambdaImagesTemplateException( + "Multiple lambda image application templates found. " + "This should not be possible, please raise an issue." + ) if app_template and not location: location = templates.location_from_app_template( diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index a8ee9a82c15..8cb5383d08f 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -1101,3 +1101,251 @@ def test_init_cli_no_package_type(self, generate_project_patch, cd_mock): True, ANY, ) + + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_but_no_app_template_provided( + self, + init_options_from_manifest_mock, + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "python3.8-image/cookiecutter-aws-sam-hello-python-lambda-image", + "displayName": "Hello World Lambda Image Example", + "dependencyManager": "pip", + "appTemplate": "hello-world-lambda-image", + "packageType": "Image", + }, + { + "directory": "python3.8-image/cookiecutter-ml-apigw-pytorch", + "displayName": "PyTorch Machine Learning Inference API", + "dependencyManager": "pip", + "appTemplate": "ml-apigw-pytorch", + "packageType": "Image", + }, + ] + with self.assertRaises(UserException): + init_cli( + ctx=self.ctx, + no_interactive=self.no_interactive, + pt_explicit=self.pt_explicit, + package_type="Image", + base_image="amazon/python3.8-base", + dependency_manager="pip", + app_template=None, + name=self.name, + output_dir=self.output_dir, + location=None, + runtime=None, + no_input=self.no_input, + extra_context=self.extra_context, + ) + + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_and_provided_app_template_not_matching_any_managed_templates( + self, + init_options_from_manifest_mock, + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "python3.8-image/cookiecutter-aws-sam-hello-python-lambda-image", + "displayName": "Hello World Lambda Image Example", + "dependencyManager": "pip", + "appTemplate": "hello-world-lambda-image", + "packageType": "Image", + }, + { + "directory": "python3.8-image/cookiecutter-ml-apigw-pytorch", + "displayName": "PyTorch Machine Learning Inference API", + "dependencyManager": "pip", + "appTemplate": "ml-apigw-pytorch", + "packageType": "Image", + }, + ] + with self.assertRaises(UserException): + init_cli( + ctx=self.ctx, + no_interactive=self.no_interactive, + pt_explicit=self.pt_explicit, + package_type="Image", + base_image="amazon/python3.8-base", + dependency_manager="pip", + app_template="Not-ml-apigw-pytorch", # different value than appTemplates shown in the manifest above + name=self.name, + output_dir=self.output_dir, + location=None, + runtime=None, + no_input=self.no_input, + extra_context=self.extra_context, + ) + + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + @patch("samcli.commands.init.init_generator.generate_project") + def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_with_matching__app_template_provided( + self, + generate_project_patch, + init_options_from_manifest_mock, + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "python3.8-image/cookiecutter-aws-sam-hello-python-lambda-image", + "displayName": "Hello World Lambda Image Example", + "dependencyManager": "pip", + "appTemplate": "hello-world-lambda-image", + "packageType": "Image", + }, + { + "directory": "python3.8-image/cookiecutter-ml-apigw-pytorch", + "displayName": "PyTorch Machine Learning Inference API", + "dependencyManager": "pip", + "appTemplate": "ml-apigw-pytorch", + "packageType": "Image", + }, + ] + init_cli( + ctx=self.ctx, + no_interactive=True, + pt_explicit=True, + package_type="Image", + base_image="amazon/python3.8-base", + dependency_manager="pip", + app_template="ml-apigw-pytorch", # same value as one appTemplate in the manifest above + name=self.name, + output_dir=None, + location=None, + runtime=None, + no_input=None, + extra_context=None, + ) + generate_project_patch.assert_called_once_with( + "repository/python3.8-image/cookiecutter-ml-apigw-pytorch", # location + "Image", # package_type + "python3.8", # runtime + "pip", # dependency_manager + self.output_dir, + self.name, + True, # no_input + ANY, + ) + + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + @patch("samcli.commands.init.init_generator.generate_project") + def test_init_cli_image_pool_with_base_image_having_one_managed_template_does_not_need_app_template_argument( + self, + generate_project_patch, + init_options_from_manifest_mock, + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "python3.8-image/cookiecutter-ml-apigw-pytorch", + "displayName": "PyTorch Machine Learning Inference API", + "dependencyManager": "pip", + "appTemplate": "ml-apigw-pytorch", + "packageType": "Image", + }, + ] + init_cli( + ctx=self.ctx, + no_interactive=True, + pt_explicit=True, + package_type="Image", + base_image="amazon/python3.8-base", + dependency_manager="pip", + app_template=None, + name=self.name, + output_dir=None, + location=None, + runtime=None, + no_input=None, + extra_context=None, + ) + generate_project_patch.assert_called_once_with( + "repository/python3.8-image/cookiecutter-ml-apigw-pytorch", # location + "Image", # package_type + "python3.8", # runtime + "pip", # dependency_manager + self.output_dir, + self.name, + True, # no_input + ANY, + ) + + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + @patch("samcli.commands.init.init_generator.generate_project") + def test_init_cli_image_pool_with_base_image_having_one_managed_template_with_provided_app_template_matching_the_managed_template( + self, + generate_project_patch, + init_options_from_manifest_mock, + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "python3.8-image/cookiecutter-ml-apigw-pytorch", + "displayName": "PyTorch Machine Learning Inference API", + "dependencyManager": "pip", + "appTemplate": "ml-apigw-pytorch", + "packageType": "Image", + }, + ] + init_cli( + ctx=self.ctx, + no_interactive=True, + pt_explicit=True, + package_type="Image", + base_image="amazon/python3.8-base", + dependency_manager="pip", + app_template="ml-apigw-pytorch", # same value as appTemplate indicated in the manifest above + name=self.name, + output_dir=None, + location=None, + runtime=None, + no_input=None, + extra_context=None, + ) + generate_project_patch.assert_called_once_with( + "repository/python3.8-image/cookiecutter-ml-apigw-pytorch", # location + "Image", # package_type + "python3.8", # runtime + "pip", # dependency_manager + self.output_dir, + self.name, + True, # no_input + ANY, + ) + + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + @patch("samcli.commands.init.init_generator.generate_project") + def test_init_cli_image_pool_with_base_image_having_one_managed_template_with_provided_app_template_not_matching_the_managed_template( + self, + generate_project_patch, + init_options_from_manifest_mock, + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "python3.8-image/cookiecutter-ml-apigw-pytorch", + "displayName": "PyTorch Machine Learning Inference API", + "dependencyManager": "pip", + "appTemplate": "ml-apigw-pytorch", + "packageType": "Image", + }, + ] + with (self.assertRaises(UserException)): + init_cli( + ctx=self.ctx, + no_interactive=True, + pt_explicit=True, + package_type="Image", + base_image="amazon/python3.8-base", + dependency_manager="pip", + app_template="NOT-ml-apigw-pytorch", # different value than appTemplate shown in the manifest above + name=self.name, + output_dir=None, + location=None, + runtime=None, + no_input=None, + extra_context=None, + ) From 8139c05b9121a6d99a2727193580086f0c97d522 Mon Sep 17 00:00:00 2001 From: Ahmed Elbayaa Date: Sun, 9 May 2021 18:19:55 -0700 Subject: [PATCH 2/4] Fix the exception message --- samcli/commands/init/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samcli/commands/init/__init__.py b/samcli/commands/init/__init__.py index 969eae238f9..ef924325113 100644 --- a/samcli/commands/init/__init__.py +++ b/samcli/commands/init/__init__.py @@ -283,7 +283,7 @@ def do_cli( elif len(options) > 1: raise LambdaImagesTemplateException( "Multiple lambda image application templates found. " - "This should not be possible, please raise an issue." + "Please specify one using the --app-template parameter." ) if app_template and not location: From 230fdcd8987e95428076c2a428170c83a4d04206 Mon Sep 17 00:00:00 2001 From: Ahmed Elbayaa Date: Fri, 14 May 2021 11:21:21 -0700 Subject: [PATCH 3/4] normalize pathes in UT to pass on windows --- tests/unit/commands/init/test_cli.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index 8cb5383d08f..5d61386acce 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -1,3 +1,4 @@ +import os from pathlib import Path from unittest import TestCase from unittest.mock import patch, ANY @@ -9,7 +10,7 @@ from samcli.commands.exceptions import UserException from samcli.commands.init import cli as init_cmd from samcli.commands.init import do_cli as init_cli -from samcli.commands.init.init_templates import InitTemplates, APP_TEMPLATES_REPO_URL, APP_TEMPLATES_REPO_NAME +from samcli.commands.init.init_templates import InitTemplates, APP_TEMPLATES_REPO_URL from samcli.lib.init import GenerateProjectFailedError from samcli.lib.utils.git_repo import GitRepo from samcli.lib.utils.packagetype import IMAGE, ZIP @@ -1183,7 +1184,7 @@ def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_an @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") @patch("samcli.commands.init.init_generator.generate_project") - def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_with_matching__app_template_provided( + def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_with_matching_app_template_provided( self, generate_project_patch, init_options_from_manifest_mock, @@ -1220,7 +1221,7 @@ def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_wi extra_context=None, ) generate_project_patch.assert_called_once_with( - "repository/python3.8-image/cookiecutter-ml-apigw-pytorch", # location + os.path.normpath("repository/python3.8-image/cookiecutter-ml-apigw-pytorch"), # location "Image", # package_type "python3.8", # runtime "pip", # dependency_manager @@ -1263,7 +1264,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_does_no extra_context=None, ) generate_project_patch.assert_called_once_with( - "repository/python3.8-image/cookiecutter-ml-apigw-pytorch", # location + os.path.normpath("repository/python3.8-image/cookiecutter-ml-apigw-pytorch"), # location "Image", # package_type "python3.8", # runtime "pip", # dependency_manager @@ -1306,7 +1307,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_with_pr extra_context=None, ) generate_project_patch.assert_called_once_with( - "repository/python3.8-image/cookiecutter-ml-apigw-pytorch", # location + os.path.normpath("repository/python3.8-image/cookiecutter-ml-apigw-pytorch"), # location "Image", # package_type "python3.8", # runtime "pip", # dependency_manager From e03081bf698c79d7c382e697f4e386a3aa9783a0 Mon Sep 17 00:00:00 2001 From: Ahmed Elbayaa Date: Fri, 14 May 2021 14:46:40 -0700 Subject: [PATCH 4/4] normalize project-template local path --- samcli/commands/init/init_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samcli/commands/init/init_templates.py b/samcli/commands/init/init_templates.py index fee1a22ce67..7b85ed3d260 100644 --- a/samcli/commands/init/init_templates.py +++ b/samcli/commands/init/init_templates.py @@ -91,7 +91,7 @@ def location_from_app_template(self, package_type, runtime, base_image, dependen if template.get("init_location") is not None: return template["init_location"] if template.get("directory") is not None: - return os.path.join(self._git_repo.local_path, template["directory"]) + return os.path.normpath(os.path.join(self._git_repo.local_path, template["directory"])) raise InvalidInitTemplateError("Invalid template. This should not be possible, please raise an issue.") except StopIteration as ex: msg = "Can't find application template " + app_template + " - check valid values in interactive init."