From 61eba16673181a5132c713e0e4da261a97ea2d8e Mon Sep 17 00:00:00 2001 From: Jonathan Ifegunni Date: Tue, 26 Oct 2021 12:07:09 -0500 Subject: [PATCH 01/10] SAM INIT Update --- samcli/commands/exceptions.py | 5 + samcli/commands/init/__init__.py | 8 +- samcli/commands/init/init_templates.py | 153 +-- samcli/commands/init/interactive_init_flow.py | 384 +++++-- samcli/local/common/runtime_template.py | 69 +- .../init/schemas/schemas_test_data_setup.py | 15 +- .../schemas/test_init_with_schemas_command.py | 120 +-- tests/unit/commands/init/test_cli.py | 961 ++++++++++++++++-- tests/unit/commands/init/test_manifest.json | 42 + tests/unit/commands/init/test_templates.py | 32 +- 10 files changed, 1390 insertions(+), 399 deletions(-) create mode 100644 tests/unit/commands/init/test_manifest.json diff --git a/samcli/commands/exceptions.py b/samcli/commands/exceptions.py index d27094352d..b8deca5f52 100644 --- a/samcli/commands/exceptions.py +++ b/samcli/commands/exceptions.py @@ -80,6 +80,11 @@ class AppPipelineTemplateMetadataException(UserException): """ +class InvalidInitOptionException(UserException): + """ + Exception class when user provides wrong options + """ + class InvalidImageException(UserException): """ Value provided to --build-image or --invoke-image is invalid URI diff --git a/samcli/commands/init/__init__.py b/samcli/commands/init/__init__.py index cd8aaba830..70eb175c13 100644 --- a/samcli/commands/init/__init__.py +++ b/samcli/commands/init/__init__.py @@ -13,7 +13,7 @@ from samcli.lib.utils.version_checker import check_newer_version from samcli.local.common.runtime_template import RUNTIMES, SUPPORTED_DEP_MANAGERS, LAMBDA_IMAGES_RUNTIMES from samcli.lib.telemetry.metric import track_command -from samcli.commands.init.interactive_init_flow import _get_runtime_from_image, get_architectures +from samcli.commands.init.interactive_init_flow import _get_runtime_from_image, get_architectures, get_sorted_runtimes from samcli.commands.local.cli_common.click_mutex import Mutex from samcli.lib.utils.packagetype import IMAGE, ZIP from samcli.lib.utils.architecture import X86_64, ARM64 @@ -152,7 +152,7 @@ def wrapped(*args, **kwargs): @click.option( "-r", "--runtime", - type=click.Choice(RUNTIMES), + type=click.Choice(get_sorted_runtimes(RUNTIMES)), help="Lambda Runtime of your app", cls=Mutex, not_required=["location", "base_image"], @@ -286,9 +286,9 @@ def do_cli( image_bool = name and pt_explicit and base_image if location or zip_bool or image_bool: # need to turn app_template into a location before we generate - templates = InitTemplates(no_interactive) + templates = InitTemplates() if package_type == IMAGE and image_bool: - base_image, runtime = _get_runtime_from_image(base_image) + runtime = _get_runtime_from_image(base_image) options = templates.init_options(package_type, runtime, base_image, dependency_manager) if not app_template: if len(options) == 1: diff --git a/samcli/commands/init/init_templates.py b/samcli/commands/init/init_templates.py index a498e88c82..805b8ee228 100644 --- a/samcli/commands/init/init_templates.py +++ b/samcli/commands/init/init_templates.py @@ -1,7 +1,7 @@ """ Manages the set of application templates. """ - +import re import itertools import json import logging @@ -9,9 +9,7 @@ from pathlib import Path from typing import Dict -import click -from samcli.cli.global_config import GlobalConfig - +from samcli.cli.main import global_cfg from samcli.commands.exceptions import UserException, AppTemplateUpdateException from samcli.lib.utils.git_repo import GitRepo, CloneRepoException, CloneRepoUnstableStateException from samcli.lib.utils.packagetype import IMAGE @@ -27,62 +25,9 @@ class InvalidInitTemplateError(UserException): class InitTemplates: - def __init__(self, no_interactive=False): - self._no_interactive = no_interactive + def __init__(self): self._git_repo: GitRepo = GitRepo(url=APP_TEMPLATES_REPO_URL) - - def prompt_for_location(self, package_type, runtime, base_image, dependency_manager): - """ - Prompt for template location based on other information provided in previous steps. - - Parameters - ---------- - package_type : str - the package type, 'Zip' or 'Image', see samcli/lib/utils/packagetype.py - runtime : str - the runtime string - base_image : str - the base image string - dependency_manager : str - the dependency manager string - - Returns - ------- - location : str - The location of the template - app_template : str - The name of the template - """ - options = self.init_options(package_type, runtime, base_image, dependency_manager) - - if len(options) == 1: - template_md = options[0] - else: - choices = list(map(str, range(1, len(options) + 1))) - choice_num = 1 - click.echo("\nAWS quick start application templates:") - for o in options: - if o.get("displayName") is not None: - msg = "\t" + str(choice_num) + " - " + o.get("displayName") - click.echo(msg) - else: - msg = ( - "\t" - + str(choice_num) - + " - Default Template for runtime " - + runtime - + " with dependency manager " - + dependency_manager - ) - click.echo(msg) - choice_num = choice_num + 1 - choice = click.prompt("Template selection", type=click.Choice(choices), show_choices=False) - template_md = options[int(choice) - 1] # zero index - if template_md.get("init_location") is not None: - return (template_md["init_location"], template_md["appTemplate"]) - if template_md.get("directory") is not None: - return os.path.join(self._git_repo.local_path, template_md["directory"]), template_md["appTemplate"] - raise InvalidInitTemplateError("Invalid template. This should not be possible, please raise an issue.") + self.manifest_file_name = "manifest.json" def location_from_app_template(self, package_type, runtime, base_image, dependency_manager, app_template): options = self.init_options(package_type, runtime, base_image, dependency_manager) @@ -104,6 +49,12 @@ def _check_app_template(entry: Dict, app_template: str) -> bool: return bool(entry["appTemplate"] == app_template) def init_options(self, package_type, runtime, base_image, dependency_manager): + self.clone_templates_repo() + if self._git_repo.local_path is None: + return self._init_options_from_bundle(package_type, runtime, dependency_manager) + return self._init_options_from_manifest(package_type, runtime, base_image, dependency_manager) + + def clone_templates_repo(self): if not self._git_repo.clone_attempted: shared_dir: Path = GlobalConfig().config_dir try: @@ -111,16 +62,13 @@ def init_options(self, package_type, runtime, base_image, dependency_manager): except CloneRepoUnstableStateException as ex: raise AppTemplateUpdateException(str(ex)) from ex except (OSError, CloneRepoException): - # If can't clone, try using an old clone from a previous run if already exist + LOG.debug("Clone error, attempting to use an old clone from a previous run") expected_previous_clone_local_path: Path = shared_dir.joinpath(APP_TEMPLATES_REPO_NAME) if expected_previous_clone_local_path.exists(): self._git_repo.local_path = expected_previous_clone_local_path - if self._git_repo.local_path is None: - return self._init_options_from_bundle(package_type, runtime, dependency_manager) - return self._init_options_from_manifest(package_type, runtime, base_image, dependency_manager) def _init_options_from_manifest(self, package_type, runtime, base_image, dependency_manager): - manifest_path = os.path.join(self._git_repo.local_path, "manifest.json") + manifest_path = self.get_manifest_path() with open(str(manifest_path)) as fp: body = fp.read() manifest_body = json.loads(body) @@ -171,3 +119,80 @@ def is_dynamic_schemas_template(self, package_type, app_template, runtime, base_ if option.get("appTemplate") == app_template: return option.get("isDynamicTemplate", False) return False + + def get_app_template_location(self, template_directory): + return os.path.normpath(os.path.join(self._git_repo.local_path, template_directory)) + + def get_manifest_path(self): + return Path(self._git_repo.local_path, self.manifest_file_name) + + def get_preprocessed_manifest(self, filter_value=None): + """ + This method get the manifest cloned from the git repo and preprocessed it. + Below is the link to manifest: + https://github.com/aws/aws-sam-cli-app-templates/blob/master/manifest.json + The structure of the manifest is shown below: + { + "dotnetcore2.1": [ + { + "directory": "dotnetcore2.1/cookiecutter-aws-sam-hello-dotnet", + "displayName": "Hello World Example", + "dependencyManager": "cli-package", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API" + }, + ] + } + Parameters + ---------- + filter_value : string, optional + This could be a runtime or a base-image, by default None + Returns + ------- + [dict] + This is preprocessed manifest with the use_case as key + """ + self.clone_templates_repo() + manifest_path = self.get_manifest_path() + with open(str(manifest_path)) as fp: + body = fp.read() + manifest_body = json.loads(body) + + # This would ensure the Use-Case Hello World Example appears + # at the top of list template example displayed to the Customer. + preprocessed_manifest = {"Hello World Example": {}} + for template_runtime in manifest_body: + if filter_value and filter_value != template_runtime: + continue + + template_list = manifest_body[template_runtime] + for template in template_list: + package_type = get_template_value("packageType", template) + use_case_name = get_template_value("useCaseName", template) + runtime = get_runtime(package_type, template_runtime) + + use_case = preprocessed_manifest.get(use_case_name, {}) + use_case[runtime] = use_case.get(runtime, {}) + use_case[runtime][package_type] = use_case[runtime].get(package_type, []) + use_case[runtime][package_type].append(template) + + preprocessed_manifest[use_case_name] = use_case + return preprocessed_manifest + + def get_bundle_option(self, package_type, runtime, dependency_manager): + return self._init_options_from_bundle(package_type, runtime, dependency_manager) + + +def get_template_value(value, template): + if value not in template: + raise InvalidInitTemplateError( + f"Template is missing the value for {value} in manifest file. Please raise a a github issue." + ) + return template.get(value) + + +def get_runtime(package_type, template_runtime): + if package_type == IMAGE: + template_runtime = re.split("/|-", template_runtime)[1] + return template_runtime diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index 2d0c85cd55..2152c56464 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -12,11 +12,11 @@ get_schemas_api_caller, get_schemas_template_parameter, ) -from samcli.commands.exceptions import SchemasApiException +from samcli.commands.exceptions import SchemasApiException, InvalidInitOptionException from samcli.lib.schemas.schemas_code_manager import do_download_source_code_binding, do_extract_and_merge_schemas_code -from samcli.local.common.runtime_template import INIT_RUNTIMES, RUNTIME_TO_DEPENDENCY_MANAGERS, LAMBDA_IMAGES_RUNTIMES +from samcli.local.common.runtime_template import LAMBDA_IMAGES_RUNTIMES_MAP, INIT_RUNTIMES from samcli.commands.init.init_generator import do_generate -from samcli.commands.init.init_templates import InitTemplates +from samcli.commands.init.init_templates import InitTemplates, InvalidInitTemplateError from samcli.lib.utils.osutils import remove from samcli.lib.utils.packagetype import IMAGE, ZIP from samcli.lib.utils.architecture import X86_64 @@ -24,6 +24,7 @@ LOG = logging.getLogger(__name__) +# pylint: disable=too-many-arguments def do_interactive( location, pt_explicit, @@ -47,21 +48,41 @@ def do_interactive( click.echo("Which template source would you like to use?") click.echo("\t1 - AWS Quick Start Templates\n\t2 - Custom Template Location") location_opt_choice = click.prompt("Choice", type=click.Choice(["1", "2"]), show_choices=False) - if location_opt_choice == "2": - _generate_from_location(location, package_type, runtime, dependency_manager, output_dir, name, no_input) - else: - if not pt_explicit: - click.echo("What package type would you like to use?") - click.echo("\t1 - Zip (artifact is a zip uploaded to S3)\t") - click.echo("\t2 - Image (artifact is an image uploaded to an ECR image repository)") - package_opt_choice = click.prompt("Package type", type=click.Choice(["1", "2"]), show_choices=False) - if package_opt_choice == "1": - package_type = ZIP - else: - package_type = IMAGE - - _generate_from_app_template( + + generate_application( + location, + pt_explicit, + package_type, + runtime, + architecture, + base_image, + dependency_manager, + output_dir, + name, + app_template, + no_input, + location_opt_choice, + ) + + +def generate_application( + location, + pt_explicit, + package_type, + runtime, + architecture, + base_image, + dependency_manager, + output_dir, + name, + app_template, + no_input, + location_opt_choice, +): # pylint: disable=too-many-arguments + if location_opt_choice == "1": + _generate_from_use_case( location, + pt_explicit, package_type, runtime, base_image, @@ -72,7 +93,11 @@ def do_interactive( architecture, ) + else: + _generate_from_location(location, package_type, runtime, dependency_manager, output_dir, name, no_input) + +# pylint: disable=too-many-statements def _generate_from_location(location, package_type, runtime, dependency_manager, output_dir, name, no_input): location = click.prompt("\nTemplate location (git, mercurial, http(s), zip, path)", type=str) summary_msg = """ @@ -89,35 +114,51 @@ def _generate_from_location(location, package_type, runtime, dependency_manager, # pylint: disable=too-many-statements -def _generate_from_app_template( - location, package_type, runtime, base_image, dependency_manager, output_dir, name, app_template, architecture +def _generate_from_use_case( + location, + pt_explicit, + package_type, + runtime, + base_image, + dependency_manager, + output_dir, + name, + app_template, + architecture, ): - extra_context = None - if package_type == IMAGE: - base_image, runtime = _get_runtime_from_image(base_image) - else: - runtime = _get_runtime(runtime) - dependency_manager = _get_dependency_manager(dependency_manager, runtime) + templates = InitTemplates() + filter_value = runtime if runtime else base_image + preprocessed_options = templates.get_preprocessed_manifest(filter_value) + + question = "Choose an AWS Quick Start application template" + use_case = _get_choice_from_options( + None, + preprocessed_options, + question, + "Template", + ) + + default_app_template_properties = _generate_default_hello_world_application( + use_case, package_type, runtime, dependency_manager, pt_explicit + ) + + chosen_app_template_properties = _get_app_template_properties( + preprocessed_options, use_case, base_image, default_app_template_properties + ) + runtime, base_image, package_type, dependency_manager, template_chosen = chosen_app_template_properties + + app_template = template_chosen["appTemplate"] + location = templates.location_from_app_template(package_type, runtime, base_image, dependency_manager, app_template) + if not name: name = click.prompt("\nProject name", type=str, default="sam-app") - templates = InitTemplates() + final_architecture = get_architectures(architecture) - if app_template is not None: - location = templates.location_from_app_template( - package_type, runtime, base_image, dependency_manager, app_template - ) - extra_context = { - "project_name": name, - "runtime": runtime, - "architectures": {"value": final_architecture}, - } - else: - location, app_template = templates.prompt_for_location(package_type, runtime, base_image, dependency_manager) - extra_context = { - "project_name": name, - "runtime": runtime, - "architectures": {"value": final_architecture}, - } + extra_context = { + "project_name": name, + "runtime": runtime, + "architectures": {"value": final_architecture}, + } # executing event_bridge logic if call is for Schema dynamic template is_dynamic_schemas_template = templates.is_dynamic_schemas_template( @@ -130,34 +171,9 @@ def _generate_from_app_template( extra_context = {**schemas_template_parameter, **extra_context} no_input = True - summary_msg = "" - if package_type == ZIP: - summary_msg = f""" - ----------------------- - Generating application: - ----------------------- - Name: {name} - Runtime: {runtime} - Architectures: {final_architecture[0]} - Dependency Manager: {dependency_manager} - Application Template: {app_template} - Output Directory: {output_dir} - - Next application steps can be found in the README file at {output_dir}/{name}/README.md - """ - elif package_type == IMAGE: - summary_msg = f""" - ----------------------- - Generating application: - ----------------------- - Name: {name} - Base Image: {base_image} - Architectures: {final_architecture[0]} - Dependency Manager: {dependency_manager} - Output Directory: {output_dir} - - Next application steps can be found in the README file at {output_dir}/{name}/README.md - """ + summary_msg = generate_summary_message( + package_type, runtime, base_image, dependency_manager, output_dir, name, app_template, final_architecture + ) click.echo(summary_msg) next_commands_msg = f""" @@ -173,56 +189,148 @@ def _generate_from_app_template( _package_schemas_code(runtime, schemas_api_caller, schema_template_details, output_dir, name, location) -def _get_runtime(runtime): - if not runtime: - choices = list(map(str, range(1, len(INIT_RUNTIMES) + 1))) - choice_num = 1 - click.echo("\nWhich runtime would you like to use?") - for r in INIT_RUNTIMES: - msg = "\t" + str(choice_num) + " - " + r - click.echo(msg) - choice_num = choice_num + 1 - choice = click.prompt("Runtime", type=click.Choice(choices), show_choices=False) - runtime = INIT_RUNTIMES[int(choice) - 1] # zero index - return runtime +def _generate_default_hello_world_application(use_case, package_type, runtime, dependency_manager, pt_explicit): + if use_case == "Hello World Example": + question = "Use the most popular runtime and package type? (Nodejs and zip)" + if click.confirm(f"\n{question}"): + runtime, package_type, dependency_manager, pt_explicit = "nodejs14.x", ZIP, "npm", True + return (runtime, package_type, dependency_manager, pt_explicit) + + +def _get_app_template_properties(preprocessed_options, use_case, base_image, template_properties): + runtime, package_type, dependency_manager, pt_explicit = template_properties + runtime_options = preprocessed_options[use_case] + if not runtime and not base_image: + question = "Which runtime would you like to use?" + runtime = _get_choice_from_options(runtime, runtime_options, question, "Runtime") + + if base_image: + runtime = _get_runtime_from_image(base_image) + + try: + package_types_options = runtime_options[runtime] + if not pt_explicit: + message = "What package type would you like to use?" + package_type = _get_choice_from_options(None, package_types_options, message, "Package type") + if package_type == IMAGE: + base_image = _get_image_from_runtime(runtime) + except KeyError as ex: + raise InvalidInitOptionException(f"Lambda Runtime {runtime} is not supported for {use_case} examples.") from ex + + try: + dependency_manager_options = package_types_options[package_type] + except KeyError as ex: + raise InvalidInitOptionException( + f"{package_type} package type is not supported for {use_case} examples and runtime {runtime} selected." + ) from ex + + dependency_manager = _get_dependency_manager(dependency_manager_options, dependency_manager, runtime) + template_chosen = _get_app_template_choice(dependency_manager_options, dependency_manager) + return (runtime, base_image, package_type, dependency_manager, template_chosen) + + +def _get_choice_from_options(chosen, options, question, msg): + + if chosen: + return chosen + + click_choices = [] + + options_list = options if isinstance(options, list) else list(options.keys()) + + if len(options_list) == 1: + click.echo( + f"\nBased on your selections, the only {msg} available is {options_list[0]}." + + f"\nWe will proceed to selecting the {msg} as {options_list[0]}." + ) + return options_list[0] + + click.echo(f"\n{question}") + options_list = ( + get_sorted_runtimes(options_list) if msg == "Runtime" and not isinstance(options, list) else options_list + ) + for index, option in enumerate(options_list): + click.echo(f"\t{index+1} - {option}") + click_choices.append(str(index + 1)) + choice = click.prompt(msg, type=click.Choice(click_choices), show_choices=False) + return options_list[int(choice) - 1] + + +def get_sorted_runtimes(options_list): + """ + sort lst of runtime name in an ascending order and version in a descending order + Parameters + ---------- + options_list : [list] + list of runtimes + Returns + ------- + [list] + list of sorted runtimes + """ + runtimes = [] + for runtime in options_list: + position = INIT_RUNTIMES.index(runtime) + runtimes.append(position) + sorted_runtimes = sorted(runtimes) + for index, position in enumerate(sorted_runtimes): + sorted_runtimes[index] = INIT_RUNTIMES[position] + return sorted_runtimes + + +def _get_app_template_choice(templates_options, dependency_manager): + templates = _get_templates_with_dependency_manager(templates_options, dependency_manager) + chosen_template = templates[0] + if len(templates) > 1: + click.echo("\nSelect your starter template") + click_template_choices = [] + for index, template in enumerate(templates): + click.echo(f"\t{index+1} - {template['displayName']}") + click_template_choices.append(str(index + 1)) + template_choice = click.prompt("Template", type=click.Choice(click_template_choices), show_choices=False) + chosen_template = templates[int(template_choice) - 1] + return chosen_template + + +def _get_templates_with_dependency_manager(templates_options, dependency_manager): + return [t for t in templates_options if t.get("dependencyManager") == dependency_manager] def _get_runtime_from_image(image): """ Get corresponding runtime from the base-image parameter """ - if not image: - choices = list(map(str, range(1, len(LAMBDA_IMAGES_RUNTIMES) + 1))) - choice_num = 1 - click.echo("\nWhich base image would you like to use?") - for r in LAMBDA_IMAGES_RUNTIMES: - msg = "\t" + str(choice_num) + " - " + r - click.echo(msg) - choice_num = choice_num + 1 - choice = click.prompt("Base image", type=click.Choice(choices), show_choices=False) - image = LAMBDA_IMAGES_RUNTIMES[int(choice) - 1] # zero index - runtime = image[image.find("/") + 1 : image.find("-")] - return image, runtime + return runtime -def _get_dependency_manager(dependency_manager, runtime): +def _get_image_from_runtime(runtime): + """ + Get corresponding base-image from the runtime parameter + """ + return LAMBDA_IMAGES_RUNTIMES_MAP[runtime] + + +def _get_dependency_manager(options, dependency_manager, runtime): + valid_dep_managers = sorted(list(set(template["dependencyManager"] for template in options))) if not dependency_manager: - valid_dep_managers = RUNTIME_TO_DEPENDENCY_MANAGERS.get(runtime) - if valid_dep_managers is None: - dependency_manager = None - elif len(valid_dep_managers) == 1: + if len(valid_dep_managers) == 1: dependency_manager = valid_dep_managers[0] + click.echo( + f"\nBased on your selections, the only dependency manager available is {dependency_manager}." + + f"\nWe will proceed copying the template using {dependency_manager}." + ) else: - choices = list(map(str, range(1, len(valid_dep_managers) + 1))) - choice_num = 1 - click.echo("\nWhich dependency manager would you like to use?") - for dm in valid_dep_managers: - msg = "\t" + str(choice_num) + " - " + dm - click.echo(msg) - choice_num = choice_num + 1 - choice = click.prompt("Dependency manager", type=click.Choice(choices), show_choices=False) - dependency_manager = valid_dep_managers[int(choice) - 1] # zero index + question = "Which dependency manager would you like to use?" + dependency_manager = _get_choice_from_options( + dependency_manager, valid_dep_managers, question, "Dependency manager" + ) + elif dependency_manager and dependency_manager not in valid_dep_managers: + msg = ( + f"Lambda Runtime {runtime} and dependency manager {dependency_manager} " + + "do not have an available initialization template." + ) + raise InvalidInitTemplateError(msg) return dependency_manager @@ -255,3 +363,63 @@ def get_architectures(architecture): Returns list of architecture value based on the init input value """ return [X86_64] if architecture is None else [architecture] + + +def generate_summary_message( + package_type, runtime, base_image, dependency_manager, output_dir, name, app_template, architecture +): + """ + Parameters + ---------- + package_type : str + The package type, 'Zip' or 'Image', see samcli/lib/utils/packagetype.py + runtime : str + AWS Lambda function runtime + base_image : str + base image + dependency_manager : str + dependency manager + output_dir : str + the directory where project will be generated in + name : str + Project Name + app_template : str + application template generated + architecture : list + Architecture type either x86_64 or arm64 on AWS lambda + + Returns + ------- + str + Summary Message of the application template generated + """ + + summary_msg = "" + if package_type == ZIP: + summary_msg = f""" + ----------------------- + Generating application: + ----------------------- + Name: {name} + Runtime: {runtime} + Architectures: {architecture[0]} + Dependency Manager: {dependency_manager} + Application Template: {app_template} + Output Directory: {output_dir} + + Next steps can be found in the README file at {output_dir}/{name}/README.md + """ + elif package_type == IMAGE: + summary_msg = f""" + ----------------------- + Generating application: + ----------------------- + Name: {name} + Base Image: {base_image} + Architectures: {architecture[0]} + Dependency Manager: {dependency_manager} + Output Directory: {output_dir} + Next steps can be found in the README file at {output_dir}/{name}/README.md + """ + + return summary_msg diff --git a/samcli/local/common/runtime_template.py b/samcli/local/common/runtime_template.py index 6d3759ec4f..99acede90e 100644 --- a/samcli/local/common/runtime_template.py +++ b/samcli/local/common/runtime_template.py @@ -110,51 +110,54 @@ def get_local_lambda_images_location(mapping, runtime): ) # When adding new Lambda runtimes, please update SAM_RUNTIME_TO_SCHEMAS_CODE_LANG_MAPPING -# Order here should be a the group of the latest versions of runtimes followed by runtime groups +# Runtimes are ordered in alphabetical fashion with reverse version order (latest versions first) INIT_RUNTIMES = [ - # latest of each runtime version - "nodejs14.x", - "python3.9", - "ruby2.7", + # dotnetcore runtimes in descending order + "dotnet5.0", + "dotnetcore3.1", + "dotnetcore2.1", "go1.x", + # java runtimes in descending order "java11", - "dotnetcore3.1", - # older nodejs runtimes + "java8.al2", + "java8", + # nodejs runtimes in descending order + "nodejs14.x", "nodejs12.x", "nodejs10.x", - # older python runtimes + # python runtimes in descending order + "python3.9", "python3.8", "python3.7", "python3.6", "python2.7", - # older ruby runtimes + # ruby runtimes in descending order + "ruby2.7", "ruby2.5", - # older java runtimes - "java8.al2", - "java8", - # older dotnetcore runtimes - "dotnetcore2.1", ] -LAMBDA_IMAGES_RUNTIMES = [ - "amazon/nodejs14.x-base", - "amazon/nodejs12.x-base", - "amazon/nodejs10.x-base", - "amazon/python3.9-base", - "amazon/python3.8-base", - "amazon/python3.7-base", - "amazon/python3.6-base", - "amazon/python2.7-base", - "amazon/ruby2.7-base", - "amazon/ruby2.5-base", - "amazon/go1.x-base", - "amazon/java11-base", - "amazon/java8.al2-base", - "amazon/java8-base", - "amazon/dotnet5.0-base", - "amazon/dotnetcore3.1-base", - "amazon/dotnetcore2.1-base", -] + +LAMBDA_IMAGES_RUNTIMES_MAP = { + "dotnet5.0": "amazon/dotnet5.0-base", + "dotnetcore3.1": "amazon/dotnetcore3.1-base", + "dotnetcore2.1": "amazon/dotnetcore2.1-base", + "go1.x": "amazon/go1.x-base", + "java11": "amazon/java11-base", + "java8.al2": "amazon/java8.al2-base", + "java8": "amazon/java8-base", + "nodejs14.x": "amazon/nodejs14.x-base", + "nodejs12.x": "amazon/nodejs12.x-base", + "nodejs10.x": "amazon/nodejs10.x-base", + "python3.9": "amazon/python3.9-base", + "python3.8": "amazon/python3.8-base", + "python3.7": "amazon/python3.7-base", + "python3.6": "amazon/python3.6-base", + "python2.7": "amazon/python2.7-base", + "ruby2.7": "amazon/ruby2.7-base", + "ruby2.5": "amazon/ruby2.5-base", +} + +LAMBDA_IMAGES_RUNTIMES = LAMBDA_IMAGES_RUNTIMES_MAP.values() # Schemas Code lang is a MINIMUM supported version # - this is why later Lambda runtimes can be mapped to earlier Schemas Code Languages diff --git a/tests/integration/init/schemas/schemas_test_data_setup.py b/tests/integration/init/schemas/schemas_test_data_setup.py index cb0c353649..3a57119d6e 100644 --- a/tests/integration/init/schemas/schemas_test_data_setup.py +++ b/tests/integration/init/schemas/schemas_test_data_setup.py @@ -34,9 +34,10 @@ def setUpClass(cls): setup_non_partner_schema_data("other-schema", schemas_client) # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 1: Zip Packagetype - # 13: Java runtime - # 1: dependency manager maven + # 1: Hello World Example + # N: do not use DEFAULT hello world template + # 11: Java runtime + # 2: dependency manager maven # eb-app-maven: response to name # Y: clone/update the source repo # 1: hello world @@ -44,12 +45,12 @@ def setUpClass(cls): user_input = """ 1 1 -13 +N +5 1 +2 eb-app-maven -1 -1 - """ + """ with tempfile.TemporaryDirectory() as temp: runner = CliRunner() runner.invoke(init_cmd, ["--output-dir", temp], input=user_input) diff --git a/tests/integration/init/schemas/test_init_with_schemas_command.py b/tests/integration/init/schemas/test_init_with_schemas_command.py index 205c3e473f..c877fa4cfe 100644 --- a/tests/integration/init/schemas/test_init_with_schemas_command.py +++ b/tests/integration/init/schemas/test_init_with_schemas_command.py @@ -19,25 +19,27 @@ class TestBasicInitWithEventBridgeCommand(SchemaTestDataSetup): def test_init_interactive_with_event_bridge_app_aws_registry(self): # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 1: Zip Packagetype - # 15: Java runtime - # 1: dependency manager maven - # eb-app-maven: response to name - # 3: select event-bridge app from scratch - # Y: Use default profile - # 1: select aws.events as registries - # 1: select aws schema + # 7: Infrastructure event management - Use case + # 1: Java Runtime + # 2: Maven + # 2: select event-bridge app from scratch + # test-project: response to name + # Y: Use default aws configuration + # 1: select schema from cli_paginator + # 4: select aws.events as registries + # 9: select schema AWSAPICallViaCloudTrail user_input = """ 1 +7 1 -15 -1 +2 +2 eb-app-maven -3 Y 1 -1 +4 +9 """ with tempfile.TemporaryDirectory() as temp: runner = CliRunner() @@ -55,22 +57,22 @@ def test_init_interactive_with_event_bridge_app_partner_registry(self): # setup schema data # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 1: Zip Packagetype - # 15: Java runtime - # 1: dependency manager maven - # eb-app-maven: response to name - # 3: select event-bridge app from scratch - # Y: Use default profile + # 7: Infrastructure event management - Use case + # 1: Java Runtime + # 2: Maven + # 2: select event-bridge app from scratch + # test-project: response to name + # Y: Use default aws configuration # 3: partner registry # 1: select aws schema user_input = """ 1 +7 1 -15 -1 +2 +2 eb-app-maven -3 Y 3 1 @@ -102,12 +104,12 @@ def test_init_interactive_with_event_bridge_app_partner_registry(self): def test_init_interactive_with_event_bridge_app_pagination(self): # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 1: Zip Packagetype - # 15: Java Runtime - # 1: dependency manager maven + # 7: Infrastructure event management - Use case + # 1: Java Runtime + # 2: Maven + # 2: select event-bridge app from scratch # eb-app-maven: response to name - # 3: select event-bridge app from scratch - # Y: Use default profile + # Y: Use default aws configuration # 4: select pagination-registry as registries # N: Go to next page # P Go to previous page @@ -115,11 +117,11 @@ def test_init_interactive_with_event_bridge_app_pagination(self): user_input = """ 1 +7 1 -14 -1 +2 +2 eb-app-maven -3 Y 4 N @@ -142,22 +144,22 @@ def test_init_interactive_with_event_bridge_app_pagination(self): def test_init_interactive_with_event_bridge_app_customer_registry(self): # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 1: Zip Packagetype - # 15: Java Runtime - # 1: dependency manager maven + # 7: Infrastructure event management - Use case + # 1: Java Runtime + # 2: Maven + # 2: select event-bridge app from scratch # eb-app-maven: response to name - # 3: select event-bridge app from scratch - # Y: Use default profile + # Y: Use default aws configuration # 2: select 2p-schema other-schema # 1: select 1 schema user_input = """ 1 +7 1 -15 -1 +2 +2 eb-app-maven -3 Y 2 1 @@ -189,22 +191,23 @@ def test_init_interactive_with_event_bridge_app_customer_registry(self): def test_init_interactive_with_event_bridge_app_aws_schemas_python(self): # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 1: Zip Packagetype - # 10: Python 3.7 + # 7: Infrastructure event management - Use case + # 6: Python 3.7 + # 2: select event-bridge app from scratch # eb-app-python37: response to name - # 3: select event-bridge app from scratch - # Y: Use default profile - # 1: select aws.events as registries + # Y: Use default aws configuration + # 4: select aws.events as registries # 1: select aws schema user_input = """ 1 -1 -10 +7 +6 +2 eb-app-python37 -3 Y 1 +4 1 """ with tempfile.TemporaryDirectory() as temp: @@ -221,10 +224,10 @@ def test_init_interactive_with_event_bridge_app_non_default_profile_selection(se self._init_custom_config("mynewprofile", "us-west-2") # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 1: Zip Packagetype - # 10: Python 3.7 + # 3: Infrastructure event management - Use case + # 6: Python 3.7 + # 2: select event-bridge app from scratch # eb-app-python37: response to name - # 3: select event-bridge app from scratch # N: Use default profile # 2: uses second profile from displayed one (myprofile) # schemas aws region us-east-1 @@ -232,9 +235,10 @@ def test_init_interactive_with_event_bridge_app_non_default_profile_selection(se # 1: select aws schema user_input = """ -1 -1 -10 +2 +3 +6 +2 eb-app-python37 3 N @@ -259,20 +263,20 @@ def test_init_interactive_with_event_bridge_app_non_supported_schemas_region(sel self._init_custom_config("default", "cn-north-1") # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 1: Zip Pacakgetype - # 10: Python 3.7 + # 7: Infrastructure event management - Use case + # 6: Python 3.7 + # 2: select event-bridge app from scratch # eb-app-python37: response to name - # 3: select event-bridge app from scratch # Y: Use default profile # 1: select aws.events as registries # 1: select aws schema user_input = """ -1 -1 -10 -eb-app-python37 +2 3 +6 +2 +eb-app-python37 Y 1 1 diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index 146d663b95..b3ee39db83 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -15,12 +15,19 @@ from samcli.commands.init import cli as init_cmd from samcli.commands.init import do_cli as init_cli from samcli.commands.init import PackageType -from samcli.commands.init.init_templates import InitTemplates, APP_TEMPLATES_REPO_URL +from samcli.commands.init.init_templates import ( + InitTemplates, + APP_TEMPLATES_REPO_URL, + get_runtime, + InvalidInitTemplateError, + get_template_value, +) from samcli.lib.init import GenerateProjectFailedError from samcli.lib.utils import osutils from samcli.lib.utils.git_repo import GitRepo from samcli.lib.utils.packagetype import IMAGE, ZIP from samcli.lib.utils.architecture import X86_64, ARM64 +from samcli.cli.main import global_cfg class MockInitTemplates: @@ -30,7 +37,8 @@ def __init__(self, no_interactive=False): url=APP_TEMPLATES_REPO_URL, ) self._git_repo.clone_attempted = True - self._git_repo.local_path = Path("repository") + self._git_repo.local_path = Path("tests/unit/commands/init") + self.manifest_file_name = "test_manifest.json" class TestCli(TestCase): @@ -296,8 +304,9 @@ def test_init_cli_generate_project_image_fails(self, generate_project_patch, git self.location, self.runtime, self.dependency_manager, self.output_dir, self.name, self.no_input ) + @patch("samcli.lib.utils.git_repo.GitRepo.clone") @patch("samcli.commands.init.init_generator.generate_project") - def test_init_cli_with_extra_context_parameter_not_passed(self, generate_project_patch): + def test_init_cli_with_extra_context_parameter_not_passed(self, generate_project_patch, git_repo_clone_mock): # GIVEN no extra_context parameter passed # WHEN sam init init_cli( @@ -323,8 +332,9 @@ def test_init_cli_with_extra_context_parameter_not_passed(self, generate_project ANY, ZIP, self.runtime, self.dependency_manager, ".", self.name, True, self.extra_context_as_json ) + @patch("samcli.lib.utils.git_repo.GitRepo.clone") @patch("samcli.commands.init.init_generator.generate_project") - def test_init_cli_with_extra_context_parameter_passed(self, generate_project_patch): + def test_init_cli_with_extra_context_parameter_passed(self, generate_project_patch, git_repo_clone_mock): # GIVEN extra_context and default_parameter(name, runtime) # WHEN sam init init_cli( @@ -362,8 +372,11 @@ def test_init_cli_with_extra_context_parameter_passed(self, generate_project_pat }, ) + @patch("samcli.lib.utils.git_repo.GitRepo.clone") @patch("samcli.commands.init.init_generator.generate_project") - def test_init_cli_with_extra_context_not_overriding_default_parameter(self, generate_project_patch): + def test_init_cli_with_extra_context_not_overriding_default_parameter( + self, generate_project_patch, git_repo_clone_mock + ): # GIVEN default_parameters(name, runtime) and extra_context trying to override default parameter # WHEN sam init init_cli( @@ -401,7 +414,8 @@ def test_init_cli_with_extra_context_not_overriding_default_parameter(self, gene }, ) - def test_init_cli_with_extra_context_input_as_wrong_json_raises_exception(self): + @patch("samcli.lib.utils.git_repo.GitRepo.clone") + def test_init_cli_with_extra_context_input_as_wrong_json_raises_exception(self, git_repo_clone_mock): # GIVEN extra_context as wrong json # WHEN a sam init is called with self.assertRaises(click.UsageError): @@ -537,8 +551,9 @@ def test_init_cli_must_only_set_passed_runtime_when_location_is_provided(self, g }, ) + @patch("samcli.lib.utils.git_repo.GitRepo.clone") @patch("samcli.commands.init.init_generator.generate_project") - def test_init_cli_with_extra_context_parameter_passed_as_escaped(self, generate_project_patch): + def test_init_cli_with_extra_context_parameter_passed_as_escaped(self, generate_project_patch, git_repo_clone_mock): # GIVEN extra_context and default_parameter(name, runtime) # WHEN sam init init_cli( @@ -579,6 +594,7 @@ def test_init_cli_with_extra_context_parameter_passed_as_escaped(self, generate_ ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") @patch("samcli.lib.schemas.schemas_aws_config.Session") @patch("samcli.commands.init.interactive_init_flow.do_extract_and_merge_schemas_code") @@ -593,22 +609,59 @@ def test_init_cli_int_with_event_bridge_app_template( do_extract_and_merge_schemas_code_mock, session_mock, init_options_from_manifest_mock, + get_preprocessed_manifest_mock, ): init_options_from_manifest_mock.return_value = [ { - "directory": "java8/cookiecutter-aws-sam-hello-java-maven", + "directory": "java11/cookiecutter-aws-sam-hello-java-maven", "displayName": "Hello World Example: Maven", "dependencyManager": "maven", "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", }, { - "directory": "java8/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", - "displayName": "Hello World Schema example Example: Maven", + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", "dependencyManager": "maven", "appTemplate": "eventBridge-schema-app", "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Infrastructure event management", }, ] + + get_preprocessed_manifest_mock.return_value = { + "Serverless API": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-hello-java-maven", + "displayName": "Hello World Example: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ] + } + }, + "Infrastructure event management": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", + "dependencyManager": "maven", + "appTemplate": "eventBridge-schema-app", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Infrastructure event management", + }, + ] + } + }, + } session_mock.return_value.profile_name = "test" session_mock.return_value.region_name = "ap-northeast-1" schemas_api_caller_mock.return_value.list_registries.return_value = { @@ -637,25 +690,23 @@ def test_init_cli_int_with_event_bridge_app_template( # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 5: Java Runtime - # 1: dependency manager maven + # 4: Infrastructure event management - Use case + # Java Runtime + # Zip + # select event-bridge app from scratch # test-project: response to name - # Y: Don't clone/update the source repo - # 2: select event-bridge app from scratch # Y: Use default aws configuration - # 1: select aws.events as registries - # 1: select schema AWSAPICallViaCloudTrail + # 1: select schema from cli_paginator + # 4: select aws.events as registries + # 9: select schema AWSAPICallViaCloudTrail user_input = """ 1 -1 -5 -1 -test-project -Y 2 +test-project Y 1 -1 +4 +9 . """ runner = CliRunner() @@ -686,35 +737,51 @@ def test_init_cli_int_with_event_bridge_app_template( ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") @patch("samcli.commands.init.init_generator.generate_project") def test_init_cli_int_with_image_app_template( - self, - generate_project_patch, - init_options_from_manifest_mock, + self, generate_project_patch, init_options_from_manifest_mock, get_preprocessed_manifest_mock ): init_options_from_manifest_mock.return_value = [ { - "directory": "java8-base/cookiecutter-aws-sam-hello-java-maven-lambda-image", + "directory": "java8-image/cookiecutter-aws-sam-hello-java-maven-lambda-image", "displayName": "Hello World Lambda Image Example: Maven", "dependencyManager": "maven", "appTemplate": "hello-world-lambda-image", + "packageType": "Image", + "useCaseName": "Serverless API", } ] + get_preprocessed_manifest_mock.return_value = { + "Serverless API": { + "java8": { + "Image": [ + { + "directory": "java8-image/cookiecutter-aws-sam-hello-java-maven-lambda-image", + "displayName": "Hello World Lambda Image Example: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world-lambda-image", + "packageType": "Image", + "useCaseName": "Serverless API", + }, + ] + } + }, + } + # WHEN the user follows interactive init prompts - # 1: AWS Quick Start Templates - # 2: Package type - Image - # 14: Java8 base image - # 1: dependency manager maven + # 2: AWS Quick Start Templates + # 1: Serverless API - Use case + # Java8 + # Package type - Image + # Hello World Lambda Image Example: Maven # test-project: response to name user_input = """ 1 -2 -14 -1 test-project """ runner = CliRunner() @@ -732,6 +799,7 @@ def test_init_cli_int_with_image_app_template( ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") @patch("samcli.lib.schemas.schemas_aws_config.Session") @patch("samcli.commands.init.interactive_init_flow.do_extract_and_merge_schemas_code") @@ -746,22 +814,59 @@ def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration( do_extract_and_merge_schemas_code_mock, session_mock, init_options_from_manifest_mock, + get_preprocessed_manifest_mock, ): init_options_from_manifest_mock.return_value = [ { - "directory": "java8/cookiecutter-aws-sam-hello-java-maven", + "directory": "java11/cookiecutter-aws-sam-hello-java-maven", "displayName": "Hello World Example: Maven", "dependencyManager": "maven", "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", }, { - "directory": "java8/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", - "displayName": "Hello World Schema example Example: Maven", + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", "dependencyManager": "maven", "appTemplate": "eventBridge-schema-app", "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Infrastructure event management", }, ] + + get_preprocessed_manifest_mock.return_value = { + "Serverless API": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-hello-java-maven", + "displayName": "Hello World Example: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ] + } + }, + "Infrastructure event management": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", + "dependencyManager": "maven", + "appTemplate": "eventBridge-schema-app", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Infrastructure event management", + }, + ] + } + }, + } session_mock.return_value.profile_name = "default" session_mock.return_value.region_name = "ap-south-1" session_mock.return_value.available_profiles = ["default", "test-profile"] @@ -792,29 +897,25 @@ def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration( # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 5: Java Runtime - # 1: dependency manager maven + # 2: Infrastructure event management - Use case + # Java Runtime + # Zip + # select event-bridge app from scratch # test-project: response to name - # Y: Don't clone/update the source repo - # 2: select event-bridge app from scratch # N: Use default AWS profile # 1: Select profile # us-east-1: Select region - # 1: select aws.events as registries - # 1: select schema AWSAPICallViaCloudTrail + # 4: select aws.events as registries + # 9: select schema AWSAPICallViaCloudTrail user_input = """ 1 -1 -5 -1 -test-project -Y 2 +test-project N 1 us-east-1 -1 -1 +4 +9 . """ runner = CliRunner() @@ -844,28 +945,71 @@ def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration( do_extract_and_merge_schemas_code_mock.do_extract_and_merge_schemas_code("result.zip", ".", "test-project", ANY) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") @patch("samcli.lib.schemas.schemas_aws_config.Session") @patch("samcli.commands.init.interactive_event_bridge_flow.SchemasApiCaller") @patch("samcli.commands.init.interactive_event_bridge_flow.get_schemas_client") def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration_with_wrong_region_name( - self, get_schemas_client_mock, schemas_api_caller_mock, session_mock, init_options_from_manifest_mock + self, + get_schemas_client_mock, + schemas_api_caller_mock, + session_mock, + init_options_from_manifest_mock, + get_preprocessed_manifest_mock, ): init_options_from_manifest_mock.return_value = [ { - "directory": "java8/cookiecutter-aws-sam-hello-java-maven", + "directory": "java11/cookiecutter-aws-sam-hello-java-maven", "displayName": "Hello World Example: Maven", "dependencyManager": "maven", "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", }, { - "directory": "java8/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", - "displayName": "Hello World Schema example Example: Maven", + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", "dependencyManager": "maven", "appTemplate": "eventBridge-schema-app", "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Infrastructure event management", }, ] + + get_preprocessed_manifest_mock.return_value = { + "Serverless API": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-hello-java-maven", + "displayName": "Hello World Example: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ] + } + }, + "Infrastructure event management": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", + "dependencyManager": "maven", + "appTemplate": "eventBridge-schema-app", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Infrastructure event management", + }, + ] + } + }, + } + session_mock.return_value.profile_name = "default" session_mock.return_value.region_name = "ap-south-1" session_mock.return_value.available_profiles = ["default", "test-profile"] @@ -876,29 +1020,27 @@ def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration_with_ # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 5: Java Runtime - # 1: dependency manager maven + # 2: Infrastructure event management - Use case + # Java Runtime + # Zip + # select event-bridge app from scratch # test-project: response to name - # Y: Don't clone/update the source repo - # 2: select event-bridge app from scratch # N: Use default AWS profile # 1: Select profile # invalid-region: Select region - # 1: select aws.events as registries - # 1: select schema AWSAPICallViaCloudTrail + # 4: select aws.events as registries + # 9: select schema AWSAPICallViaCloudTrail user_input = """ 1 +2 1 -5 1 test-project -Y -2 N 1 invalid-region -1 -1 +4 +9 . """ runner = CliRunner() @@ -907,6 +1049,7 @@ def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration_with_ self.assertTrue(result.exception) get_schemas_client_mock.assert_called_once_with("default", "invalid-region") + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") @patch("samcli.lib.schemas.schemas_aws_config.Session") @patch("samcli.commands.init.interactive_init_flow.do_extract_and_merge_schemas_code") @@ -922,22 +1065,60 @@ def test_init_cli_int_with_download_manager_raises_exception( do_extract_and_merge_schemas_code_mock, session_mock, init_options_from_manifest_mock, + get_preprocessed_manifest_mock, ): init_options_from_manifest_mock.return_value = [ { - "directory": "java8/cookiecutter-aws-sam-hello-java-maven", + "directory": "java11/cookiecutter-aws-sam-hello-java-maven", "displayName": "Hello World Example: Maven", "dependencyManager": "maven", "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", }, { - "directory": "java8/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", - "displayName": "Hello World Schema example Example: Maven", + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", "dependencyManager": "maven", "appTemplate": "eventBridge-schema-app", "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Infrastructure event management", }, ] + + get_preprocessed_manifest_mock.return_value = { + "Serverless API": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-hello-java-maven", + "displayName": "Hello World Example: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ] + } + }, + "Infrastructure event management": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", + "dependencyManager": "maven", + "appTemplate": "eventBridge-schema-app", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Infrastructure event management", + }, + ] + } + }, + } + session_mock.return_value.profile_name = "test" session_mock.return_value.region_name = "ap-northeast-1" schemas_api_caller_mock.return_value.list_registries.return_value = { @@ -978,15 +1159,12 @@ def test_init_cli_int_with_download_manager_raises_exception( # 1: select schema AWSAPICallViaCloudTrail user_input = """ 1 -1 -5 -1 -test-project -Y 2 +test-project Y 1 -1 +4 +9 . """ runner = CliRunner() @@ -1017,6 +1195,7 @@ def test_init_cli_int_with_download_manager_raises_exception( ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") @patch("samcli.lib.schemas.schemas_aws_config.Session") @patch("samcli.commands.init.interactive_init_flow.do_extract_and_merge_schemas_code") @@ -1031,22 +1210,59 @@ def test_init_cli_int_with_schemas_details_raises_exception( do_extract_and_merge_schemas_code_mock, session_mock, init_options_from_manifest_mock, + get_preprocessed_manifest_mock, ): init_options_from_manifest_mock.return_value = [ { - "directory": "java8/cookiecutter-aws-sam-hello-java-maven", + "directory": "java11/cookiecutter-aws-sam-hello-java-maven", "displayName": "Hello World Example: Maven", "dependencyManager": "maven", "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", }, { - "directory": "java8/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", - "displayName": "Hello World Schema example Example: Maven", + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", "dependencyManager": "maven", "appTemplate": "eventBridge-schema-app", "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Infrastructure event management", }, ] + + get_preprocessed_manifest_mock.return_value = { + "Serverless API": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-hello-java-maven", + "displayName": "Hello World Example: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ] + } + }, + "Infrastructure event management": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", + "dependencyManager": "maven", + "appTemplate": "eventBridge-schema-app", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Infrastructure event management", + }, + ] + } + }, + } session_mock.return_value.profile_name = "test" session_mock.return_value.region_name = "ap-northeast-1" schemas_api_caller_mock.return_value.list_registries.return_value = { @@ -1070,25 +1286,25 @@ def test_init_cli_int_with_schemas_details_raises_exception( ) # WHEN the user follows interactive init prompts # 1: AWS Quick Start Templates - # 5: Java Runtime - # 1: dependency manager maven + # 2: Infrastructure event management - Use case + # 1: Java Runtime + # 1: Zip + # select event-bridge app from scratch # test-project: response to name - # Y: Don't clone/update the source repo - # 2: select event-bridge app from scratch - # Y: Used default aws configuration - # 1: select aws.events as registries - # 1: select schema AWSAPICallViaCloudTrail + # Y: Use default aws configuration + # 1: select schema from cli_paginator + # 4: select aws.events as registries + # 9: select schema AWSAPICallViaCloudTrail user_input = """ 1 +2 1 -5 1 test-project Y -2 -Y -1 1 +4 +9 """ runner = CliRunner() result = runner.invoke(init_cmd, input=user_input) @@ -1132,9 +1348,9 @@ def test_init_passes_dynamic_event_bridge_template(self, generate_project_patch, self.extra_context_as_json, ) - @patch("samcli.lib.utils.git_repo.GitRepo._ensure_clone_directory_exists") + @patch("samcli.lib.utils.git_repo.GitRepo.clone") @patch("samcli.commands.init.init_generator.generate_project") - def test_init_cli_int_from_location(self, generate_project_patch, cd_mock): + def test_init_cli_int_from_location(self, generate_project_patch, git_repo_clone_mock): # WHEN the user follows interactive init prompts # 2: selecting custom location @@ -1161,16 +1377,17 @@ def test_init_cli_int_from_location(self, generate_project_patch, cd_mock): None, ) - @patch("samcli.lib.utils.git_repo.GitRepo._ensure_clone_directory_exists") + @patch("samcli.lib.utils.git_repo.GitRepo.clone") @patch("samcli.commands.init.init_generator.generate_project") - def test_init_cli_no_package_type(self, generate_project_patch, cd_mock): + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_init_cli_no_package_type(self, generate_project_patch, git_repo_clone_mock): # WHEN the user follows interactive init prompts # 1: selecting template source # 2s: selecting package type user_input = """ 1 -2 +n 1 """ args = [ @@ -1319,7 +1536,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( - os.path.normpath("repository/python3.8-image/cookiecutter-ml-apigw-pytorch"), # location + ANY, # location "Image", # package_type "python3.8", # runtime "pip", # dependency_manager @@ -1363,7 +1580,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_does_no architecture=None, ) generate_project_patch.assert_called_once_with( - os.path.normpath("repository/python3.8-image/cookiecutter-ml-apigw-pytorch"), # location + ANY, # location "Image", # package_type "python3.8", # runtime "pip", # dependency_manager @@ -1407,7 +1624,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_with_pr architecture=None, ) generate_project_patch.assert_called_once_with( - os.path.normpath("repository/python3.8-image/cookiecutter-ml-apigw-pytorch"), # location + ANY, # location "Image", # package_type "python3.8", # runtime "pip", # dependency_manager @@ -1491,3 +1708,557 @@ def test_init_cli_must_pass_with_architecture_and_base_image(self, generate_proj PackageType.explicit = ( False # Other tests fail after we pass --packge-type in this test, so let's reset this variable ) + + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + @patch("samcli.commands.init.init_generator.generate_project") + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_init_cli_generate_default_hello_world_app( + self, generate_project_patch, init_options_from_manifest_mock, get_preprocessed_manifest_mock + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "nodejs14.x/cookiecutter-aws-sam-hello-nodejs", + "displayName": "Hello World Example", + "dependencyManager": "npm", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", + "dependencyManager": "maven", + "appTemplate": "eventBridge-schema-app", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + + get_preprocessed_manifest_mock.return_value = { + "Hello World Example": { + "nodejs14.x": { + "Zip": [ + { + "directory": "nodejs14.x/cookiecutter-aws-sam-hello-nodejs", + "displayName": "Hello World Example", + "dependencyManager": "npm", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + }, + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "Hello World Example: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + }, + }, + } + + # WHEN the user follows interactive init prompts + # 1: AWS Quick Start Templates + # 1: Hello World Template + # y: use default + # test-project: response to name + user_input = """ +1 +1 +y +test-project + """ + + runner = CliRunner() + result = runner.invoke(init_cmd, input=user_input) + self.assertFalse(result.exception) + generate_project_patch.assert_called_once_with( + ANY, + ZIP, + "nodejs14.x", + "npm", + ".", + "test-project", + True, + {"project_name": "test-project", "runtime": "nodejs14.x", "architectures": {"value": ["x86_64"]}}, + ) + + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + @patch("samcli.commands.init.init_generator.generate_project") + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_init_cli_must_not_generate_default_hello_world_app( + self, generate_project_patch, init_options_from_manifest_mock, get_preprocessed_manifest_mock + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "nodejs14.x/cookiecutter-aws-sam-hello-nodejs", + "displayName": "Hello World Example", + "dependencyManager": "npm", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", + "dependencyManager": "maven", + "appTemplate": "eventBridge-schema-app", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + + get_preprocessed_manifest_mock.return_value = { + "Hello World Example": { + "nodejs14.x": { + "Zip": [ + { + "directory": "nodejs14.x/cookiecutter-aws-sam-hello-nodejs", + "displayName": "Hello World Example", + "dependencyManager": "npm", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + }, + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "Hello World Example: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + }, + }, + } + + # WHEN the user follows interactive init prompts + # 1: AWS Quick Start Templates + # 1: Hello World Template + # n: do not use default + # 1: Java runtime + # test-project: response to name + user_input = """ +1 +1 +n +1 +test-project + """ + + runner = CliRunner() + result = runner.invoke(init_cmd, input=user_input) + self.assertFalse(result.exception) + generate_project_patch.assert_called_once_with( + ANY, + ZIP, + "java11", + "maven", + ".", + "test-project", + True, + {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, + ) + + def test_must_return_runtime_from_base_image_name(self): + base_images = [ + "amazon/dotnet5.0-base", + "amazon/dotnetcore3.1-base", + "amazon/go1.x-base", + "amazon/java11-base", + "amazon/nodejs14.x-base", + "amazon/python3.8-base", + "amazon/ruby2.7-base", + ] + + expected_runtime = [ + "dotnet5.0", + "dotnetcore3.1", + "go1.x", + "java11", + "nodejs14.x", + "python3.8", + "ruby2.7", + ] + + for index, base_image in enumerate(base_images): + runtime = get_runtime(IMAGE, base_image) + self.assertEqual(runtime, expected_runtime[index]) + + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_must_process_manifest(self): + template = InitTemplates() + preprocess_manifest = template.get_preprocessed_manifest() + expected_result = { + "Hello World Example": { + "dotnetcore2.1": { + "Zip": [ + { + "directory": "dotnetcore2.1/cookiecutter-aws-sam-hello-dotnet", + "displayName": "Hello World Example", + "dependencyManager": "cli-package", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + } + ] + }, + "go1.x": { + "Zip": [ + { + "directory": "go1.x/cookiecutter-aws-sam-hello-golang", + "displayName": "Hello World Example", + "dependencyManager": "mod", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + } + ] + }, + "nodejs14.x": { + "Image": [ + { + "directory": "nodejs14.x-image/cookiecutter-aws-sam-hello-nodejs-lambda-image", + "displayName": "Hello World Image Example", + "dependencyManager": "npm", + "appTemplate": "hello-world-lambda-image", + "packageType": "Image", + "useCaseName": "Hello World Example", + } + ] + }, + "python3.8": { + "Image": [ + { + "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", + "useCaseName": "Hello World Example", + } + ] + }, + } + } + self.assertEqual(preprocess_manifest, expected_result) + + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_must_process_manifest_with_runtime_as_filter_value(self): + template = InitTemplates() + filter_value = "go1.x" + preprocess_manifest = template.get_preprocessed_manifest(filter_value) + expected_result = { + "Hello World Example": { + "go1.x": { + "Zip": [ + { + "directory": "go1.x/cookiecutter-aws-sam-hello-golang", + "displayName": "Hello World Example", + "dependencyManager": "mod", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + } + ] + }, + } + } + self.assertEqual(preprocess_manifest, expected_result) + + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_must_process_manifest_with_image_as_filter_value(self): + template = InitTemplates() + filter_value = "amazon/nodejs14.x-base" + preprocess_manifest = template.get_preprocessed_manifest(filter_value) + expected_result = { + "Hello World Example": { + "nodejs14.x": { + "Image": [ + { + "directory": "nodejs14.x-image/cookiecutter-aws-sam-hello-nodejs-lambda-image", + "displayName": "Hello World Image Example", + "dependencyManager": "npm", + "appTemplate": "hello-world-lambda-image", + "packageType": "Image", + "useCaseName": "Hello World Example", + } + ] + } + } + } + self.assertEqual(preprocess_manifest, expected_result) + + @patch("samcli.lib.utils.git_repo.GitRepo.clone") + def test_init_fails_unsupported_dep_mgr_for_runtime(self, git_repo_clone_mock): + # WHEN the wrong dependency_manager is passed for a runtime + # THEN an exception should be raised + with self.assertRaises(InvalidInitTemplateError) as ex: + init_cli( + ctx=self.ctx, + no_interactive=self.no_interactive, + location=self.location, + pt_explicit=self.pt_explicit, + package_type=self.package_type, + runtime="java8", + base_image=self.base_image, + dependency_manager="pip", + output_dir=None, + name=self.name, + app_template=self.app_template, + no_input=self.no_input, + extra_context=None, + architecture=X86_64, + ) + expected_error_message = ( + "Lambda Runtime java8 and dependency manager pip does not have an available initialization template." + ) + self.assertEqual(str(ex.exception), expected_error_message) + + @patch("samcli.lib.utils.git_repo.GitRepo.clone") + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_init_cli_with_mismatch_dep_runtime(self, git_repo_clone_mock): + # WHEN the user follows interactive init prompts + + # 1: selecting template source + # 1: selecting package type + user_input = """ +1 +n + + """ + args = [ + "--name", + "untitled6", + "--runtime", + "go1.x", + "--dependency-manager", + "pip", + ] + runner = CliRunner() + result = runner.invoke(init_cmd, args=args, input=user_input) + + self.assertTrue(result.exception) + expected_error_message = ( + "Lambda Runtime go1.x and dependency manager pip do not have an available initialization template." + ) + self.assertIn(expected_error_message, result.output) + + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + @patch("samcli.commands.init.init_generator.generate_project") + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_init_cli_int_with_multiple_app_templates( + self, generate_project_patch, init_options_from_manifest_mock, get_preprocessed_manifest_mock + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "java11/cookiecutter-aws-sam-hello1-java-maven", + "displayName": "Hello World Example 1: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + { + "directory": "java11/cookiecutter-aws-sam-hello2-java-maven", + "displayName": "Hello World Example 2: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world_x", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ] + + get_preprocessed_manifest_mock.return_value = { + "Serverless API": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-hello1-java-maven", + "displayName": "Hello World Example 1: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + { + "directory": "java11/cookiecutter-aws-sam-hello2-java-maven", + "displayName": "Hello World Example 1: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ] + } + }, + } + + # WHEN the user follows interactive init prompts + + # 1: AWS Quick Start Templates + # 1: Serverless API - Use case + # Java11 + # Package type - Image + # Hello World Lambda Image Example: Maven + # 1: Hello-world template + # test-project: response to name + + user_input = """ +1 +1 +test-project + """ + runner = CliRunner() + result = runner.invoke(init_cmd, input=user_input) + self.assertFalse(result.exception) + generate_project_patch.assert_called_once_with( + ANY, + ZIP, + "java11", + "maven", + ".", + "test-project", + True, + {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, + ) + + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_init_cli_init_must_raise_for_unknown_property(self): + template = ( + { + "directory": "java11/cookiecutter-aws-sam-hello1-java-maven", + "displayName": "Hello World Example 1: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ) + + with self.assertRaises(InvalidInitTemplateError): + get_template_value("unknown_parameter", template) + + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_init_cli_int_must_raise_for_unsupported_runtime( + self, init_options_from_manifest_mock, get_preprocessed_manifest_mock + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "java11/cookiecutter-aws-sam-hello1-java-maven", + "displayName": "Hello World Example 1: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ] + + get_preprocessed_manifest_mock.return_value = { + "Serverless API": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-hello1-java-maven", + "displayName": "Hello World Example 1: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ] + } + }, + } + + # WHEN the user follows interactive init prompts + + # 2: AWS Quick Start Templates + # 1: Serverless API - Use case + # Java11 + # Package type - Image + # Hello World Lambda Image Example: Maven + # 1: Hello-world template + # test-project: response to name + + user_input = """ +2 +1 +test-project + """ + runner = CliRunner() + result = runner.invoke(init_cmd, ["--runtime", "python3.7"], input=user_input) + self.assertTrue(result.exception) + + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_init_cli_int_must_raise_for_unsupported_dependency( + self, init_options_from_manifest_mock, get_preprocessed_manifest_mock + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "java11/cookiecutter-aws-sam-hello1-java-maven", + "displayName": "Hello World Example 1: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ] + + get_preprocessed_manifest_mock.return_value = { + "Serverless API": { + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-hello1-java-maven", + "displayName": "Hello World Example 1: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + }, + ] + } + }, + } + + # WHEN the user follows interactive init prompts + + # 2: AWS Quick Start Templates + # 1: Serverless API - Use case + # Java11 + # Package type - Image + # Hello World Lambda Image Example: Maven + # 1: Hello-world template + # test-project: response to name + + user_input = """ +2 +1 +test-project + """ + runner = CliRunner() + result = runner.invoke(init_cmd, ["--dependency-manager", "pip"], input=user_input) + self.assertTrue(result.exception) diff --git a/tests/unit/commands/init/test_manifest.json b/tests/unit/commands/init/test_manifest.json new file mode 100644 index 0000000000..7aed62eee2 --- /dev/null +++ b/tests/unit/commands/init/test_manifest.json @@ -0,0 +1,42 @@ +{ + "dotnetcore2.1": [ + { + "directory": "dotnetcore2.1/cookiecutter-aws-sam-hello-dotnet", + "displayName": "Hello World Example", + "dependencyManager": "cli-package", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example" + } + ], + "go1.x": [ + { + "directory": "go1.x/cookiecutter-aws-sam-hello-golang", + "displayName": "Hello World Example", + "dependencyManager": "mod", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example" + } + ], + "amazon/nodejs14.x-base": [ + { + "directory": "nodejs14.x-image/cookiecutter-aws-sam-hello-nodejs-lambda-image", + "displayName": "Hello World Image Example", + "dependencyManager": "npm", + "appTemplate": "hello-world-lambda-image", + "packageType": "Image", + "useCaseName": "Hello World Example" + } + ], + "amazon/python3.8-base": [ + { + "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", + "useCaseName": "Hello World Example" + } + ] +} \ No newline at end of file diff --git a/tests/unit/commands/init/test_templates.py b/tests/unit/commands/init/test_templates.py index 0492905a6a..b23424c305 100644 --- a/tests/unit/commands/init/test_templates.py +++ b/tests/unit/commands/init/test_templates.py @@ -15,7 +15,7 @@ class TestTemplates(TestCase): @patch("samcli.lib.utils.git_repo.GitRepo._ensure_clone_directory_exists") @patch("shutil.copytree") def test_location_from_app_template_zip(self, subprocess_mock, git_exec_mock, cd_mock, copy_mock): - it = InitTemplates(True) + it = InitTemplates() manifest = { "ruby2.5": [ @@ -42,7 +42,7 @@ def test_location_from_app_template_zip(self, subprocess_mock, git_exec_mock, cd @patch("samcli.lib.utils.git_repo.GitRepo._ensure_clone_directory_exists") @patch("shutil.copytree") def test_location_from_app_template_image(self, subprocess_mock, git_exec_mock, cd_mock, copy_mock): - it = InitTemplates(True) + it = InitTemplates() manifest = { "ruby2.5-image": [ @@ -65,31 +65,3 @@ def test_location_from_app_template_image(self, subprocess_mock, git_exec_mock, IMAGE, None, "ruby2.5-image", "bundler", "hello-world-lambda-image" ) self.assertTrue(search("mock-ruby-image-template", location)) - - @patch("samcli.lib.utils.git_repo.GitRepo._git_executable") - @patch("click.prompt") - @patch("samcli.lib.utils.git_repo.GitRepo._ensure_clone_directory_exists") - def test_fallback_options(self, git_exec_mock, prompt_mock, cd_mock): - prompt_mock.return_value = "1" - with patch("samcli.lib.utils.git_repo.check_output", new_callable=MagicMock) as mock_sub: - with patch("samcli.cli.global_config.GlobalConfig.config_dir", new_callable=PropertyMock) as mock_cfg: - mock_sub.side_effect = OSError("Fail") - mock_cfg.return_value = Path("/tmp/test-sam") - it = InitTemplates(True) - location, app_template = it.prompt_for_location(ZIP, "ruby2.5", None, "bundler") - self.assertTrue(search("cookiecutter-aws-sam-hello-ruby", location)) - self.assertEqual("hello-world", app_template) - - @patch("samcli.lib.utils.git_repo.GitRepo._git_executable") - @patch("click.prompt") - @patch("samcli.lib.utils.git_repo.GitRepo._ensure_clone_directory_exists") - def test_fallback_process_error(self, git_exec_mock, prompt_mock, cd_mock): - prompt_mock.return_value = "1" - with patch("samcli.lib.utils.git_repo.check_output", new_callable=MagicMock) as mock_sub: - with patch("samcli.cli.global_config.GlobalConfig.config_dir", new_callable=PropertyMock) as mock_cfg: - mock_sub.side_effect = subprocess.CalledProcessError("fail", "fail", "not found".encode("utf-8")) - mock_cfg.return_value = Path("/tmp/test-sam") - it = InitTemplates(True) - location, app_template = it.prompt_for_location(ZIP, "ruby2.5", None, "bundler") - self.assertTrue(search("cookiecutter-aws-sam-hello-ruby", location)) - self.assertEqual("hello-world", app_template) From 4388d4786cd0e8c1e7ff0f85c8a7e83481bae445 Mon Sep 17 00:00:00 2001 From: Jonathan Ifegunni Date: Tue, 26 Oct 2021 12:54:37 -0500 Subject: [PATCH 02/10] resolved conflict --- samcli/commands/exceptions.py | 1 + samcli/commands/init/init_templates.py | 11 ++- samcli/commands/init/interactive_init_flow.py | 76 +++++++++++-------- tests/unit/commands/init/test_cli.py | 34 +++++---- 4 files changed, 73 insertions(+), 49 deletions(-) diff --git a/samcli/commands/exceptions.py b/samcli/commands/exceptions.py index b8deca5f52..10a8dbf337 100644 --- a/samcli/commands/exceptions.py +++ b/samcli/commands/exceptions.py @@ -85,6 +85,7 @@ class InvalidInitOptionException(UserException): Exception class when user provides wrong options """ + class InvalidImageException(UserException): """ Value provided to --build-image or --invoke-image is invalid URI diff --git a/samcli/commands/init/init_templates.py b/samcli/commands/init/init_templates.py index 805b8ee228..ad6e483f96 100644 --- a/samcli/commands/init/init_templates.py +++ b/samcli/commands/init/init_templates.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import Dict -from samcli.cli.main import global_cfg +from samcli.cli.global_config import GlobalConfig from samcli.commands.exceptions import UserException, AppTemplateUpdateException from samcli.lib.utils.git_repo import GitRepo, CloneRepoException, CloneRepoUnstableStateException from samcli.lib.utils.packagetype import IMAGE @@ -170,14 +170,16 @@ def get_preprocessed_manifest(self, filter_value=None): for template in template_list: package_type = get_template_value("packageType", template) use_case_name = get_template_value("useCaseName", template) + if not package_type or not use_case_name: + continue runtime = get_runtime(package_type, template_runtime) - use_case = preprocessed_manifest.get(use_case_name, {}) use_case[runtime] = use_case.get(runtime, {}) use_case[runtime][package_type] = use_case[runtime].get(package_type, []) use_case[runtime][package_type].append(template) preprocessed_manifest[use_case_name] = use_case + return preprocessed_manifest def get_bundle_option(self, package_type, runtime, dependency_manager): @@ -186,8 +188,9 @@ def get_bundle_option(self, package_type, runtime, dependency_manager): def get_template_value(value, template): if value not in template: - raise InvalidInitTemplateError( - f"Template is missing the value for {value} in manifest file. Please raise a a github issue." + LOG.debug( + f"Template is missing the value for {value} in manifest file. Please raise a github issue." + + f" Template details: {template}" ) return template.get(value) diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index 2152c56464..b40a2e85b5 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -127,8 +127,8 @@ def _generate_from_use_case( architecture, ): templates = InitTemplates() - filter_value = runtime if runtime else base_image - preprocessed_options = templates.get_preprocessed_manifest(filter_value) + runtime_or_base_image = runtime if runtime else base_image + preprocessed_options = templates.get_preprocessed_manifest(runtime_or_base_image) question = "Choose an AWS Quick Start application template" use_case = _get_choice_from_options( @@ -139,7 +139,7 @@ def _generate_from_use_case( ) default_app_template_properties = _generate_default_hello_world_application( - use_case, package_type, runtime, dependency_manager, pt_explicit + use_case, package_type, runtime, base_image, dependency_manager, pt_explicit ) chosen_app_template_properties = _get_app_template_properties( @@ -189,10 +189,32 @@ def _generate_from_use_case( _package_schemas_code(runtime, schemas_api_caller, schema_template_details, output_dir, name, location) -def _generate_default_hello_world_application(use_case, package_type, runtime, dependency_manager, pt_explicit): - if use_case == "Hello World Example": - question = "Use the most popular runtime and package type? (Nodejs and zip)" - if click.confirm(f"\n{question}"): +def _generate_default_hello_world_application( + use_case, package_type, runtime, base_image, dependency_manager, pt_explicit +): + """ + Generate the default Hello World template if Hello World Example is selected + Parameters + ---------- + use_case : string + Type of template example selected + package_type : str + The package type, 'Zip' or 'Image', see samcli/lib/utils/packagetype.py + runtime : str + AWS Lambda function runtime + dependency_manager : str + dependency manager + pt_explicit : bool + True --package-type was passed or Vice versa + + Returns + ------- + tuple + configuration for a default Hello World Example + """ + + if use_case == "Hello World Example" and not (runtime or base_image): + if click.confirm("\n Use the most popular runtime and package type? (Nodejs and zip)"): runtime, package_type, dependency_manager, pt_explicit = "nodejs14.x", ZIP, "npm", True return (runtime, package_type, dependency_manager, pt_explicit) @@ -207,22 +229,20 @@ def _get_app_template_properties(preprocessed_options, use_case, base_image, tem if base_image: runtime = _get_runtime_from_image(base_image) - try: - package_types_options = runtime_options[runtime] - if not pt_explicit: - message = "What package type would you like to use?" - package_type = _get_choice_from_options(None, package_types_options, message, "Package type") - if package_type == IMAGE: - base_image = _get_image_from_runtime(runtime) - except KeyError as ex: - raise InvalidInitOptionException(f"Lambda Runtime {runtime} is not supported for {use_case} examples.") from ex - - try: - dependency_manager_options = package_types_options[package_type] - except KeyError as ex: + package_types_options = runtime_options.get(runtime) + if not package_types_options: + raise InvalidInitOptionException(f"Lambda Runtime {runtime} is not supported for {use_case} examples.") + if not pt_explicit: + message = "What package type would you like to use?" + package_type = _get_choice_from_options(None, package_types_options, message, "Package type") + if package_type == IMAGE: + base_image = _get_image_from_runtime(runtime) + + dependency_manager_options = package_types_options.get(package_type) + if not dependency_manager_options: raise InvalidInitOptionException( f"{package_type} package type is not supported for {use_case} examples and runtime {runtime} selected." - ) from ex + ) dependency_manager = _get_dependency_manager(dependency_manager_options, dependency_manager, runtime) template_chosen = _get_app_template_choice(dependency_manager_options, dependency_manager) @@ -238,6 +258,9 @@ def _get_choice_from_options(chosen, options, question, msg): options_list = options if isinstance(options, list) else list(options.keys()) + if not options_list: + raise InvalidInitOptionException(f"There are no {msg} options available to be selected.") + if len(options_list) == 1: click.echo( f"\nBased on your selections, the only {msg} available is {options_list[0]}." @@ -257,17 +280,6 @@ def _get_choice_from_options(chosen, options, question, msg): def get_sorted_runtimes(options_list): - """ - sort lst of runtime name in an ascending order and version in a descending order - Parameters - ---------- - options_list : [list] - list of runtimes - Returns - ------- - [list] - list of sorted runtimes - """ runtimes = [] for runtime in options_list: position = INIT_RUNTIMES.index(runtime) diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index b3ee39db83..0f1ad7cb0e 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -2,6 +2,9 @@ import shutil import subprocess import tempfile +import logging +from unittest.case import expectedFailure +import pytest from pathlib import Path from typing import Dict, Any from unittest import TestCase @@ -27,7 +30,6 @@ from samcli.lib.utils.git_repo import GitRepo from samcli.lib.utils.packagetype import IMAGE, ZIP from samcli.lib.utils.architecture import X86_64, ARM64 -from samcli.cli.main import global_cfg class MockInitTemplates: @@ -2141,21 +2143,27 @@ def test_init_cli_int_with_multiple_app_templates( {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, ) + @pytest.fixture(autouse=True) + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def get_caplog(self, caplog): + self._caplog = caplog + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) def test_init_cli_init_must_raise_for_unknown_property(self): - template = ( - { - "directory": "java11/cookiecutter-aws-sam-hello1-java-maven", - "displayName": "Hello World Example 1: Maven", - "dependencyManager": "maven", - "appTemplate": "hello-world", - "packageType": "Zip", - "useCaseName": "Serverless API", - }, - ) + template = { + "directory": "java11/cookiecutter-aws-sam-hello1-java-maven", + "displayName": "Hello World Example 1: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Serverless API", + } - with self.assertRaises(InvalidInitTemplateError): - get_template_value("unknown_parameter", template) + expected_msg = f"Template is missing the value for unknown_parameter in manifest file. Please raise a github issue. Template details: {template}" + self._caplog.set_level(logging.DEBUG) + result = get_template_value("unknown_parameter", template) + self.assertEqual(result, None) + self.assertIn(expected_msg, self._caplog.text) @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") From 9a8058fdcae53a96e481c3191f0bc5b137228734 Mon Sep 17 00:00:00 2001 From: Jonathan Ifegunni Date: Tue, 30 Nov 2021 11:18:25 -0600 Subject: [PATCH 03/10] test added and updated --- samcli/commands/init/interactive_init_flow.py | 3 + tests/unit/commands/init/test_cli.py | 101 ++++++++++++++++-- 2 files changed, 95 insertions(+), 9 deletions(-) diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index b40a2e85b5..1a1fe158d1 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -194,6 +194,7 @@ def _generate_default_hello_world_application( ): """ Generate the default Hello World template if Hello World Example is selected + Parameters ---------- use_case : string @@ -202,6 +203,8 @@ def _generate_default_hello_world_application( The package type, 'Zip' or 'Image', see samcli/lib/utils/packagetype.py runtime : str AWS Lambda function runtime + base_image : str + AWS Lambda function base-image dependency_manager : str dependency manager pt_explicit : bool diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index 0f1ad7cb0e..7f85ad8775 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -2143,13 +2143,9 @@ def test_init_cli_int_with_multiple_app_templates( {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, ) - @pytest.fixture(autouse=True) + @patch("samcli.commands.init.init_templates.LOG") @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) - def get_caplog(self, caplog): - self._caplog = caplog - - @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) - def test_init_cli_init_must_raise_for_unknown_property(self): + def test_init_cli_init_must_raise_for_unknown_property(self, log_mock): template = { "directory": "java11/cookiecutter-aws-sam-hello1-java-maven", "displayName": "Hello World Example 1: Maven", @@ -2159,11 +2155,10 @@ def test_init_cli_init_must_raise_for_unknown_property(self): "useCaseName": "Serverless API", } - expected_msg = f"Template is missing the value for unknown_parameter in manifest file. Please raise a github issue. Template details: {template}" - self._caplog.set_level(logging.DEBUG) + debug_msg = f"Template is missing the value for unknown_parameter in manifest file. Please raise a github issue. Template details: {template}" result = get_template_value("unknown_parameter", template) + log_mock.debug.assert_called_once_with(debug_msg) self.assertEqual(result, None) - self.assertIn(expected_msg, self._caplog.text) @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") @@ -2270,3 +2265,91 @@ def test_init_cli_int_must_raise_for_unsupported_dependency( runner = CliRunner() result = runner.invoke(init_cmd, ["--dependency-manager", "pip"], input=user_input) self.assertTrue(result.exception) + + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + @patch("samcli.commands.init.init_generator.generate_project") + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_init_cli_generate_hello_world_app_without_default_prompt( + self, generate_project_patch, init_options_from_manifest_mock, get_preprocessed_manifest_mock + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "nodejs14.x/cookiecutter-aws-sam-hello-nodejs", + "displayName": "Hello World Example", + "dependencyManager": "npm", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", + "dependencyManager": "maven", + "appTemplate": "eventBridge-schema-app", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + + get_preprocessed_manifest_mock.return_value = { + "Hello World Example": { + "nodejs14.x": { + "Zip": [ + { + "directory": "nodejs14.x/cookiecutter-aws-sam-hello-nodejs", + "displayName": "Hello World Example", + "dependencyManager": "npm", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + }, + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "Hello World Example: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + }, + }, + } + + # WHEN the user follows interactive init prompts + # 1: AWS Quick Start Templates + # 1: Hello World Template + # y: use default + # test-project: response to name + user_input = """ +1 +test-project + """ + + runner = CliRunner() + result = runner.invoke(init_cmd, ["--runtime", "java11"], input=user_input) + self.assertFalse(result.exception) + generate_project_patch.assert_called_once_with( + ANY, + ZIP, + "java11", + "maven", + ".", + "test-project", + True, + {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, + ) + + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_must_get_manifest_path(self): + template = InitTemplates() + manifest_path = template.get_manifest_path() + expected_path = Path("tests/unit/commands/init/test_manifest.json") + self.assertEqual(expected_path, manifest_path) From c0927f7a94313986839f18c58109e3612b7afa8c Mon Sep 17 00:00:00 2001 From: Jonathan Ifegunni Date: Thu, 9 Dec 2021 16:07:35 -0600 Subject: [PATCH 04/10] block default prompt when Image packageType is provide and skip use case prompt when --app-template is provided --- samcli/commands/init/init_templates.py | 6 +- samcli/commands/init/interactive_init_flow.py | 6 +- .../schemas/test_init_with_schemas_command.py | 8 +- tests/unit/commands/init/test_cli.py | 80 +++++++++++++++++++ 4 files changed, 91 insertions(+), 9 deletions(-) diff --git a/samcli/commands/init/init_templates.py b/samcli/commands/init/init_templates.py index ad6e483f96..05fc918fcf 100644 --- a/samcli/commands/init/init_templates.py +++ b/samcli/commands/init/init_templates.py @@ -126,7 +126,7 @@ def get_app_template_location(self, template_directory): def get_manifest_path(self): return Path(self._git_repo.local_path, self.manifest_file_name) - def get_preprocessed_manifest(self, filter_value=None): + def get_preprocessed_manifest(self, filter_value=None, app_template=None): """ This method get the manifest cloned from the git repo and preprocessed it. Below is the link to manifest: @@ -148,6 +148,8 @@ def get_preprocessed_manifest(self, filter_value=None): ---------- filter_value : string, optional This could be a runtime or a base-image, by default None + app_template : string, optional + Application template generated Returns ------- [dict] @@ -170,7 +172,7 @@ def get_preprocessed_manifest(self, filter_value=None): for template in template_list: package_type = get_template_value("packageType", template) use_case_name = get_template_value("useCaseName", template) - if not package_type or not use_case_name: + if not (package_type or use_case_name) or (app_template and app_template != template["appTemplate"]): continue runtime = get_runtime(package_type, template_runtime) use_case = preprocessed_manifest.get(use_case_name, {}) diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index 1a1fe158d1..1082b5eabf 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -126,10 +126,10 @@ def _generate_from_use_case( app_template, architecture, ): + # breakpoint() templates = InitTemplates() runtime_or_base_image = runtime if runtime else base_image - preprocessed_options = templates.get_preprocessed_manifest(runtime_or_base_image) - + preprocessed_options = templates.get_preprocessed_manifest(runtime_or_base_image, app_template) question = "Choose an AWS Quick Start application template" use_case = _get_choice_from_options( None, @@ -148,6 +148,7 @@ def _generate_from_use_case( runtime, base_image, package_type, dependency_manager, template_chosen = chosen_app_template_properties app_template = template_chosen["appTemplate"] + base_image = LAMBDA_IMAGES_RUNTIMES_MAP.get(runtime) if not base_image and package_type == IMAGE else base_image location = templates.location_from_app_template(package_type, runtime, base_image, dependency_manager, app_template) if not name: @@ -215,7 +216,6 @@ def _generate_default_hello_world_application( tuple configuration for a default Hello World Example """ - if use_case == "Hello World Example" and not (runtime or base_image): if click.confirm("\n Use the most popular runtime and package type? (Nodejs and zip)"): runtime, package_type, dependency_manager, pt_explicit = "nodejs14.x", ZIP, "npm", True diff --git a/tests/integration/init/schemas/test_init_with_schemas_command.py b/tests/integration/init/schemas/test_init_with_schemas_command.py index c877fa4cfe..5e5492d44a 100644 --- a/tests/integration/init/schemas/test_init_with_schemas_command.py +++ b/tests/integration/init/schemas/test_init_with_schemas_command.py @@ -235,8 +235,8 @@ def test_init_interactive_with_event_bridge_app_non_default_profile_selection(se # 1: select aws schema user_input = """ -2 -3 +1 +7 6 2 eb-app-python37 @@ -272,8 +272,8 @@ def test_init_interactive_with_event_bridge_app_non_supported_schemas_region(sel # 1: select aws schema user_input = """ -2 -3 +1 +7 6 2 eb-app-python37 diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index 7f85ad8775..e55484575e 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -2353,3 +2353,83 @@ def test_must_get_manifest_path(self): manifest_path = template.get_manifest_path() expected_path = Path("tests/unit/commands/init/test_manifest.json") self.assertEqual(expected_path, manifest_path) + + @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") + @patch("samcli.commands.init.init_templates.InitTemplates._init_options_from_manifest") + @patch("samcli.commands.init.init_generator.generate_project") + @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) + def test_init_cli_generate_app_template_provide_via_options( + self, generate_project_patch, init_options_from_manifest_mock, get_preprocessed_manifest_mock + ): + init_options_from_manifest_mock.return_value = [ + { + "directory": "nodejs14.x/cookiecutter-aws-sam-hello-nodejs", + "displayName": "Hello World Example", + "dependencyManager": "npm", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "EventBridge App from scratch (100+ Event Schemas): Maven", + "dependencyManager": "maven", + "appTemplate": "eventBridge-schema-app", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + + get_preprocessed_manifest_mock.return_value = { + "Hello World Example": { + "nodejs14.x": { + "Zip": [ + { + "directory": "nodejs14.x/cookiecutter-aws-sam-hello-nodejs", + "displayName": "Hello World Example", + "dependencyManager": "npm", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + }, + "java11": { + "Zip": [ + { + "directory": "java11/cookiecutter-aws-sam-eventbridge-schema-app-java-maven", + "displayName": "Hello World Example: Maven", + "dependencyManager": "maven", + "appTemplate": "hello-world", + "isDynamicTemplate": "True", + "packageType": "Zip", + "useCaseName": "Hello World Example", + }, + ] + }, + }, + } + + # WHEN the user follows interactive init prompts + # 1: AWS Quick Start Templates + # 2: Java 11 + # test-project: response to name + user_input = """ +1 +test-project + """ + + runner = CliRunner() + result = runner.invoke(init_cmd, ["--app-template", "hello-world"], input=user_input) + self.assertFalse(result.exception) + generate_project_patch.assert_called_once_with( + ANY, + ZIP, + "java11", + "maven", + ".", + "test-project", + True, + {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, + ) From 5a0492aae110fb395f2d8add3e17482b24ea02bc Mon Sep 17 00:00:00 2001 From: Jonathan Ifegunni Date: Tue, 14 Dec 2021 12:23:24 -0600 Subject: [PATCH 05/10] update default hello world prompt --- samcli/commands/init/interactive_init_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index 1082b5eabf..edad002b37 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -126,7 +126,6 @@ def _generate_from_use_case( app_template, architecture, ): - # breakpoint() templates = InitTemplates() runtime_or_base_image = runtime if runtime else base_image preprocessed_options = templates.get_preprocessed_manifest(runtime_or_base_image, app_template) @@ -216,7 +215,8 @@ def _generate_default_hello_world_application( tuple configuration for a default Hello World Example """ - if use_case == "Hello World Example" and not (runtime or base_image): + is_package_type_image = bool(package_type == IMAGE) + if use_case == "Hello World Example" and not (runtime or base_image or is_package_type_image): if click.confirm("\n Use the most popular runtime and package type? (Nodejs and zip)"): runtime, package_type, dependency_manager, pt_explicit = "nodejs14.x", ZIP, "npm", True return (runtime, package_type, dependency_manager, pt_explicit) From b56e08aa8d95d5b2e8a7ac22756adb494d47d54e Mon Sep 17 00:00:00 2001 From: Jonathan Ifegunni Date: Tue, 14 Dec 2021 20:20:04 -0600 Subject: [PATCH 06/10] filtering updates --- samcli/commands/init/init_templates.py | 25 ++++++++++++++----- samcli/commands/init/interactive_init_flow.py | 6 +++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/samcli/commands/init/init_templates.py b/samcli/commands/init/init_templates.py index 05fc918fcf..710ab80911 100644 --- a/samcli/commands/init/init_templates.py +++ b/samcli/commands/init/init_templates.py @@ -126,7 +126,9 @@ def get_app_template_location(self, template_directory): def get_manifest_path(self): return Path(self._git_repo.local_path, self.manifest_file_name) - def get_preprocessed_manifest(self, filter_value=None, app_template=None): + def get_preprocessed_manifest( + self, filter_value=None, app_template=None, package_type=None, dependency_manager=None + ): """ This method get the manifest cloned from the git repo and preprocessed it. Below is the link to manifest: @@ -170,20 +172,31 @@ def get_preprocessed_manifest(self, filter_value=None, app_template=None): template_list = manifest_body[template_runtime] for template in template_list: - package_type = get_template_value("packageType", template) + template_package_type = get_template_value("packageType", template) use_case_name = get_template_value("useCaseName", template) - if not (package_type or use_case_name) or (app_template and app_template != template["appTemplate"]): + if ( + not (template_package_type or use_case_name) + or (app_template and app_template != template["appTemplate"]) + or self.does_template_meet_filtering_criterial(package_type, dependency_manager, template) + ): continue - runtime = get_runtime(package_type, template_runtime) + runtime = get_runtime(template_package_type, template_runtime) use_case = preprocessed_manifest.get(use_case_name, {}) use_case[runtime] = use_case.get(runtime, {}) - use_case[runtime][package_type] = use_case[runtime].get(package_type, []) - use_case[runtime][package_type].append(template) + use_case[runtime][template_package_type] = use_case[runtime].get(template_package_type, []) + use_case[runtime][template_package_type].append(template) preprocessed_manifest[use_case_name] = use_case return preprocessed_manifest + def does_template_meet_filtering_criterial(self, package_type, dependency_manager, template): + pt_filter_criteria_status = bool(package_type and package_type == template["packageType"]) + dp_filter_criteria_status = bool(dependency_manager and dependency_manager == template["dependencyManager"]) + if pt_filter_criteria_status == True and dp_filter_criteria_status == True: + return False + return True + def get_bundle_option(self, package_type, runtime, dependency_manager): return self._init_options_from_bundle(package_type, runtime, dependency_manager) diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index edad002b37..4fe101d65e 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -128,7 +128,9 @@ def _generate_from_use_case( ): templates = InitTemplates() runtime_or_base_image = runtime if runtime else base_image - preprocessed_options = templates.get_preprocessed_manifest(runtime_or_base_image, app_template) + preprocessed_options = templates.get_preprocessed_manifest( + runtime_or_base_image, app_template, package_type, dependency_manager + ) question = "Choose an AWS Quick Start application template" use_case = _get_choice_from_options( None, @@ -216,7 +218,7 @@ def _generate_default_hello_world_application( configuration for a default Hello World Example """ is_package_type_image = bool(package_type == IMAGE) - if use_case == "Hello World Example" and not (runtime or base_image or is_package_type_image): + if use_case == "Hello World Example" and not (runtime or base_image or is_package_type_image or dependency_manager): if click.confirm("\n Use the most popular runtime and package type? (Nodejs and zip)"): runtime, package_type, dependency_manager, pt_explicit = "nodejs14.x", ZIP, "npm", True return (runtime, package_type, dependency_manager, pt_explicit) From fff8584c9c52595a103f18f5a78023ea49e15fff Mon Sep 17 00:00:00 2001 From: Jonathan Ifegunni Date: Thu, 16 Dec 2021 09:41:22 -0600 Subject: [PATCH 07/10] added typing --- samcli/commands/init/init_templates.py | 64 ++++++++++----- samcli/commands/init/interactive_init_flow.py | 79 +++++++++++++------ tests/unit/commands/init/test_cli.py | 40 +++++++++- 3 files changed, 140 insertions(+), 43 deletions(-) diff --git a/samcli/commands/init/init_templates.py b/samcli/commands/init/init_templates.py index 710ab80911..ad2164b233 100644 --- a/samcli/commands/init/init_templates.py +++ b/samcli/commands/init/init_templates.py @@ -7,7 +7,7 @@ import logging import os from pathlib import Path -from typing import Dict +from typing import Dict, Optional from samcli.cli.global_config import GlobalConfig from samcli.commands.exceptions import UserException, AppTemplateUpdateException @@ -127,8 +127,12 @@ def get_manifest_path(self): return Path(self._git_repo.local_path, self.manifest_file_name) def get_preprocessed_manifest( - self, filter_value=None, app_template=None, package_type=None, dependency_manager=None - ): + self, + filter_value: Optional[str] = None, + app_template: Optional[str] = None, + package_type: Optional[str] = None, + dependency_manager: Optional[str] = None, + ) -> dict: """ This method get the manifest cloned from the git repo and preprocessed it. Below is the link to manifest: @@ -152,6 +156,10 @@ def get_preprocessed_manifest( This could be a runtime or a base-image, by default None app_template : string, optional Application template generated + package_type : string, optional + The package type, 'Zip' or 'Image', see samcli/lib/utils/packagetype.py + dependency_manager : string, optional + dependency manager Returns ------- [dict] @@ -165,19 +173,16 @@ def get_preprocessed_manifest( # This would ensure the Use-Case Hello World Example appears # at the top of list template example displayed to the Customer. - preprocessed_manifest = {"Hello World Example": {}} + preprocessed_manifest = {"Hello World Example": {}} # type: dict for template_runtime in manifest_body: if filter_value and filter_value != template_runtime: continue - template_list = manifest_body[template_runtime] for template in template_list: template_package_type = get_template_value("packageType", template) use_case_name = get_template_value("useCaseName", template) - if ( - not (template_package_type or use_case_name) - or (app_template and app_template != template["appTemplate"]) - or self.does_template_meet_filtering_criterial(package_type, dependency_manager, template) + if not (template_package_type or use_case_name) or template_does_not_meet_filter_criteria( + app_template, package_type, dependency_manager, template ): continue runtime = get_runtime(template_package_type, template_runtime) @@ -188,20 +193,16 @@ def get_preprocessed_manifest( preprocessed_manifest[use_case_name] = use_case - return preprocessed_manifest + if not bool(preprocessed_manifest["Hello World Example"]): + del preprocessed_manifest["Hello World Example"] - def does_template_meet_filtering_criterial(self, package_type, dependency_manager, template): - pt_filter_criteria_status = bool(package_type and package_type == template["packageType"]) - dp_filter_criteria_status = bool(dependency_manager and dependency_manager == template["dependencyManager"]) - if pt_filter_criteria_status == True and dp_filter_criteria_status == True: - return False - return True + return preprocessed_manifest def get_bundle_option(self, package_type, runtime, dependency_manager): return self._init_options_from_bundle(package_type, runtime, dependency_manager) -def get_template_value(value, template): +def get_template_value(value: str, template: dict) -> Optional[str]: if value not in template: LOG.debug( f"Template is missing the value for {value} in manifest file. Please raise a github issue." @@ -210,7 +211,34 @@ def get_template_value(value, template): return template.get(value) -def get_runtime(package_type, template_runtime): +def get_runtime(package_type: Optional[str], template_runtime: str) -> str: if package_type == IMAGE: template_runtime = re.split("/|-", template_runtime)[1] return template_runtime + + +def template_does_not_meet_filter_criteria( + app_template: Optional[str], package_type: Optional[str], dependency_manager: Optional[str], template: dict +) -> bool: + """ + Parameters + ---------- + app_template : Optional[str] + Application template generated + package_type : Optional[str] + The package type, 'Zip' or 'Image', see samcli/lib/utils/packagetype.py + dependency_manager : Optional[str] + Dependency manager + template : dict + key-value pair app template configuration + + Returns + ------- + bool + True if template does not meet filter criteria else False + """ + return bool( + (app_template and app_template != template["appTemplate"]) + or (package_type and package_type != template["packageType"]) + or (dependency_manager and dependency_manager != template["dependencyManager"]) + ) diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index 4fe101d65e..78e8a23a89 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -3,6 +3,7 @@ """ import tempfile import logging +from typing import Optional, Tuple import click from botocore.exceptions import ClientError, WaiterError @@ -115,21 +116,22 @@ def _generate_from_location(location, package_type, runtime, dependency_manager, # pylint: disable=too-many-statements def _generate_from_use_case( - location, - pt_explicit, - package_type, - runtime, - base_image, - dependency_manager, - output_dir, - name, - app_template, - architecture, -): + location: Optional[str], + pt_explicit: bool, + package_type: Optional[str], + runtime: Optional[str], + base_image: Optional[str], + dependency_manager: Optional[str], + output_dir: Optional[str], + name: Optional[str], + app_template: Optional[str], + architecture: Optional[str], +) -> None: templates = InitTemplates() runtime_or_base_image = runtime if runtime else base_image + package_type_filter_value = package_type if pt_explicit else None preprocessed_options = templates.get_preprocessed_manifest( - runtime_or_base_image, app_template, package_type, dependency_manager + runtime_or_base_image, app_template, package_type_filter_value, dependency_manager ) question = "Choose an AWS Quick Start application template" use_case = _get_choice_from_options( @@ -149,7 +151,9 @@ def _generate_from_use_case( runtime, base_image, package_type, dependency_manager, template_chosen = chosen_app_template_properties app_template = template_chosen["appTemplate"] - base_image = LAMBDA_IMAGES_RUNTIMES_MAP.get(runtime) if not base_image and package_type == IMAGE else base_image + base_image = ( + LAMBDA_IMAGES_RUNTIMES_MAP.get(str(runtime)) if not base_image and package_type == IMAGE else base_image + ) location = templates.location_from_app_template(package_type, runtime, base_image, dependency_manager, app_template) if not name: @@ -192,29 +196,34 @@ def _generate_from_use_case( def _generate_default_hello_world_application( - use_case, package_type, runtime, base_image, dependency_manager, pt_explicit -): + use_case: str, + package_type: Optional[str], + runtime: Optional[str], + base_image: Optional[str], + dependency_manager: Optional[str], + pt_explicit: bool, +) -> Tuple: """ Generate the default Hello World template if Hello World Example is selected Parameters ---------- - use_case : string + use_case : str Type of template example selected - package_type : str + package_type : Optional[str] The package type, 'Zip' or 'Image', see samcli/lib/utils/packagetype.py - runtime : str + runtime : Optional[str] AWS Lambda function runtime - base_image : str + base_image : Optional[str] AWS Lambda function base-image - dependency_manager : str + dependency_manager : Optional[str] dependency manager pt_explicit : bool True --package-type was passed or Vice versa Returns ------- - tuple + Tuple configuration for a default Hello World Example """ is_package_type_image = bool(package_type == IMAGE) @@ -224,7 +233,33 @@ def _generate_default_hello_world_application( return (runtime, package_type, dependency_manager, pt_explicit) -def _get_app_template_properties(preprocessed_options, use_case, base_image, template_properties): +def _get_app_template_properties( + preprocessed_options: dict, use_case: str, base_image: Optional[str], template_properties: Tuple +) -> Tuple: + """ + This is the heart of the interactive flow, this method fetchs the templates options needed to generate a template + + Parameters + ---------- + preprocessed_options : dict + Preprocessed manifest from https://github.com/aws/aws-sam-cli-app-templates + use_case : Optional[str] + Type of template example selected + base_image : str + AWS Lambda function base-image + template_properties : Tuple + Tuple of template properties like runtime, packages type and dependency manager + + Returns + ------- + Tuple + Tuple of template configuration and the chosen template + + Raises + ------ + InvalidInitOptionException + exception raised when invalid option is provided + """ runtime, package_type, dependency_manager, pt_explicit = template_properties runtime_options = preprocessed_options[use_case] if not runtime and not base_image: diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index e55484575e..d2f66d261b 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -24,9 +24,11 @@ get_runtime, InvalidInitTemplateError, get_template_value, + template_does_not_meet_filter_criteria, ) from samcli.lib.init import GenerateProjectFailedError from samcli.lib.utils import osutils +from samcli.lib.utils import packagetype from samcli.lib.utils.git_repo import GitRepo from samcli.lib.utils.packagetype import IMAGE, ZIP from samcli.lib.utils.architecture import X86_64, ARM64 @@ -2058,9 +2060,7 @@ def test_init_cli_with_mismatch_dep_runtime(self, git_repo_clone_mock): result = runner.invoke(init_cmd, args=args, input=user_input) self.assertTrue(result.exception) - expected_error_message = ( - "Lambda Runtime go1.x and dependency manager pip do not have an available initialization template." - ) + expected_error_message = "There are no Template options available to be selected." self.assertIn(expected_error_message, result.output) @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @@ -2433,3 +2433,37 @@ def test_init_cli_generate_app_template_provide_via_options( True, {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, ) + + def does_template_meet_filter_criteria(self): + template1 = { + "directory": "nodejs14.x/cookiecutter-aws-sam-hello-nodejs", + "displayName": "Hello World Example", + "dependencyManager": "npm", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + } + app_template = "hello-world" + self.assertFalse(template_does_not_meet_filter_criteria(app_template, None, None, template1)) + + template2 = { + "directory": "java8/cookiecutter-aws-sam-hello-nodejs", + "displayName": "Hello World Example", + "dependencyManager": "Gradle", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + } + package_type = "Image" + self.assertTrue(template_does_not_meet_filter_criteria(app_template, package_type, None, template2)) + + template3 = { + "directory": "java8/cookiecutter-aws-sam-hello-nodejs", + "displayName": "Hello World Example", + "dependencyManager": "Gradle", + "appTemplate": "hello-world", + "packageType": "Zip", + "useCaseName": "Hello World Example", + } + dependency_manager = "Gradle" + self.assertTrue(template_does_not_meet_filter_criteria(app_template, None, dependency_manager, template3)) From 0f65d730208a23447ff7c6948d5b9ddd107e7d4e Mon Sep 17 00:00:00 2001 From: Qingchuan Ma <69653965+qingchm@users.noreply.github.com> Date: Thu, 16 Dec 2021 17:48:05 -0500 Subject: [PATCH 08/10] Fixing failed canaries (#3532) --- .../integration/deploy/test_deploy_command.py | 26 ------------------- .../sync/infra/after/Ruby/function/Gemfile | 2 +- .../sync/infra/after/Ruby/layer/Gemfile | 2 +- .../sync/infra/before/Ruby/function/Gemfile | 2 +- .../sync/infra/before/Ruby/layer/Gemfile | 2 +- .../sync/infra/template-ruby-after.yaml | 6 ++--- .../sync/infra/template-ruby-before.yaml | 6 ++--- 7 files changed, 10 insertions(+), 36 deletions(-) diff --git a/tests/integration/deploy/test_deploy_command.py b/tests/integration/deploy/test_deploy_command.py index 370bef1fac..9f0231127b 100644 --- a/tests/integration/deploy/test_deploy_command.py +++ b/tests/integration/deploy/test_deploy_command.py @@ -1197,29 +1197,3 @@ def test_deploy_logs_warning_with_cdk_project(self): deploy_process_execute = run_command(deploy_command_list) self.assertIn(warning_message, deploy_process_execute.stdout) self.assertEqual(deploy_process_execute.process.returncode, 0) - - def _method_to_stack_name(self, method_name): - """Method expects method name which can be a full path. Eg: test.integration.test_deploy_command.method_name""" - method_name = method_name.split(".")[-1] - return f"{method_name.replace('_', '-')}-{CFN_PYTHON_VERSION_SUFFIX}" - - def _stack_name_to_companion_stack(self, stack_name): - return CompanionStack(stack_name).stack_name - - def _delete_companion_stack(self, cfn_client, ecr_client, companion_stack_name): - repos = list() - try: - cfn_client.describe_stacks(StackName=companion_stack_name) - except ClientError: - return - stack = boto3.resource("cloudformation").Stack(companion_stack_name) - resources = stack.resource_summaries.all() - for resource in resources: - if resource.resource_type == "AWS::ECR::Repository": - repos.append(resource.physical_resource_id) - for repo in repos: - try: - ecr_client.delete_repository(repositoryName=repo, force=True) - except ecr_client.exceptions.RepositoryNotFoundException: - pass - cfn_client.delete_stack(StackName=companion_stack_name) diff --git a/tests/integration/testdata/sync/infra/after/Ruby/function/Gemfile b/tests/integration/testdata/sync/infra/after/Ruby/function/Gemfile index 69fc281fa7..2000ad3a89 100644 --- a/tests/integration/testdata/sync/infra/after/Ruby/function/Gemfile +++ b/tests/integration/testdata/sync/infra/after/Ruby/function/Gemfile @@ -2,4 +2,4 @@ source "https://rubygems.org" gem "ruby-statistics" -ruby '~> 2.7.0' +ruby '~> 2.5.0' diff --git a/tests/integration/testdata/sync/infra/after/Ruby/layer/Gemfile b/tests/integration/testdata/sync/infra/after/Ruby/layer/Gemfile index 620aaa62f8..a76cdc43e5 100644 --- a/tests/integration/testdata/sync/infra/after/Ruby/layer/Gemfile +++ b/tests/integration/testdata/sync/infra/after/Ruby/layer/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" -ruby '~> 2.7.0' +ruby '~> 2.5.0' diff --git a/tests/integration/testdata/sync/infra/before/Ruby/function/Gemfile b/tests/integration/testdata/sync/infra/before/Ruby/function/Gemfile index 69fc281fa7..2000ad3a89 100644 --- a/tests/integration/testdata/sync/infra/before/Ruby/function/Gemfile +++ b/tests/integration/testdata/sync/infra/before/Ruby/function/Gemfile @@ -2,4 +2,4 @@ source "https://rubygems.org" gem "ruby-statistics" -ruby '~> 2.7.0' +ruby '~> 2.5.0' diff --git a/tests/integration/testdata/sync/infra/before/Ruby/layer/Gemfile b/tests/integration/testdata/sync/infra/before/Ruby/layer/Gemfile index 620aaa62f8..a76cdc43e5 100644 --- a/tests/integration/testdata/sync/infra/before/Ruby/layer/Gemfile +++ b/tests/integration/testdata/sync/infra/before/Ruby/layer/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" -ruby '~> 2.7.0' +ruby '~> 2.5.0' diff --git a/tests/integration/testdata/sync/infra/template-ruby-after.yaml b/tests/integration/testdata/sync/infra/template-ruby-after.yaml index dc869b1691..7a50622960 100644 --- a/tests/integration/testdata/sync/infra/template-ruby-after.yaml +++ b/tests/integration/testdata/sync/infra/template-ruby-after.yaml @@ -12,7 +12,7 @@ Resources: AutoPublishAlias: Hello1Alias CodeUri: after/Ruby/function/ Handler: app.lambda_handler - Runtime: ruby2.7 + Runtime: ruby2.5 Architectures: - x86_64 Layers: @@ -25,6 +25,6 @@ Resources: Description: Hello World Ruby Layer ContentUri: after/Ruby/layer/ CompatibleRuntimes: - - ruby2.7 + - ruby2.5 Metadata: - BuildMethod: ruby2.7 \ No newline at end of file + BuildMethod: ruby2.5 \ No newline at end of file diff --git a/tests/integration/testdata/sync/infra/template-ruby-before.yaml b/tests/integration/testdata/sync/infra/template-ruby-before.yaml index f5c312b2cb..5dfc0a72c5 100644 --- a/tests/integration/testdata/sync/infra/template-ruby-before.yaml +++ b/tests/integration/testdata/sync/infra/template-ruby-before.yaml @@ -12,7 +12,7 @@ Resources: AutoPublishAlias: Hello1Alias CodeUri: before/Ruby/function/ Handler: app.lambda_handler - Runtime: ruby2.7 + Runtime: ruby2.5 Architectures: - x86_64 Layers: @@ -25,6 +25,6 @@ Resources: Description: Hello World Ruby Layer ContentUri: before/Ruby/layer/ CompatibleRuntimes: - - ruby2.7 + - ruby2.5 Metadata: - BuildMethod: ruby2.7 \ No newline at end of file + BuildMethod: ruby2.5 \ No newline at end of file From d6a9e701e7385fa3332d991cf849295fa238c38f Mon Sep 17 00:00:00 2001 From: Mohamed Elasmar <71043312+moelasmar@users.noreply.github.com> Date: Thu, 16 Dec 2021 21:47:36 -0800 Subject: [PATCH 09/10] (fix) stop resolving relative paths for Function image URI (#3531) * (fix) stop resolve relative paths for function imageUri * fix unit testing --- samcli/commands/_utils/template.py | 8 +- .../integration/buildcmd/build_integ_base.py | 10 +++ tests/integration/buildcmd/test_build_cmd.py | 19 +++-- tests/unit/commands/_utils/test_template.py | 80 ++++++++++++++++++- 4 files changed, 107 insertions(+), 10 deletions(-) diff --git a/samcli/commands/_utils/template.py b/samcli/commands/_utils/template.py index 61b1c497e9..234e13f0bf 100644 --- a/samcli/commands/_utils/template.py +++ b/samcli/commands/_utils/template.py @@ -10,7 +10,7 @@ from botocore.utils import set_value_from_jmespath from samcli.commands.exceptions import UserException -from samcli.lib.utils.packagetype import ZIP +from samcli.lib.utils.packagetype import ZIP, IMAGE from samcli.yamlhelper import yaml_parse, yaml_dump from samcli.lib.utils.resources import ( METADATA_WITH_LOCAL_PATHS, @@ -155,6 +155,12 @@ def _update_relative_paths(template_dict, original_root, new_root): for path_prop_name in RESOURCES_WITH_LOCAL_PATHS[resource_type]: properties = resource.get("Properties", {}) + if ( + resource_type in [AWS_SERVERLESS_FUNCTION, AWS_LAMBDA_FUNCTION] + and properties.get("PackageType", ZIP) == IMAGE + ): + continue + path = jmespath.search(path_prop_name, properties) updated_path = _resolve_relative_to(path, original_root, new_root) diff --git a/tests/integration/buildcmd/build_integ_base.py b/tests/integration/buildcmd/build_integ_base.py index d89c23b29f..f0ab85714f 100644 --- a/tests/integration/buildcmd/build_integ_base.py +++ b/tests/integration/buildcmd/build_integ_base.py @@ -161,6 +161,16 @@ def verify_pulled_image(self, runtime, architecture=X86_64): def _make_parameter_override_arg(self, overrides): return " ".join(["ParameterKey={},ParameterValue={}".format(key, value) for key, value in overrides.items()]) + def _verify_image_build_artifact(self, template_path, image_function_logical_id, property, image_uri): + self.assertTrue(template_path.exists(), "Build directory should be created") + + build_dir = template_path.parent + build_dir_files = os.listdir(str(build_dir)) + self.assertNotIn(image_function_logical_id, build_dir_files) + + # Make sure the template has correct CodeUri for resource + self._verify_resource_property(str(template_path), image_function_logical_id, property, image_uri) + def _verify_resource_property(self, template_path, logical_id, property, expected_value): with open(template_path, "r") as fp: template_dict = yaml_parse(fp.read()) diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index d8c021f379..4b11c600c6 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -4,6 +4,7 @@ import shutil import sys from pathlib import Path +from typing import Set from unittest import skipIf import pytest @@ -49,24 +50,19 @@ class TestBuildCommand_PythonFunctions_Images(BuildIntegBase): template = "template_image.yaml" - EXPECTED_FILES_PROJECT_MANIFEST = { - "__init__.py", - "main.py", - "numpy", - # 'cryptography', - "requirements.txt", - } + EXPECTED_FILES_PROJECT_MANIFEST: Set[str] = set() FUNCTION_LOGICAL_ID_IMAGE = "ImageFunction" @parameterized.expand([("3.6", False), ("3.7", False), ("3.8", False), ("3.9", False)]) @pytest.mark.flaky(reruns=3) def test_with_default_requirements(self, runtime, use_container): + _tag = f"{random.randint(1,100)}" overrides = { "Runtime": runtime, "Handler": "main.handler", "DockerFile": "Dockerfile", - "Tag": f"{random.randint(1,100)}", + "Tag": _tag, } cmdlist = self.get_command_list(use_container=use_container, parameter_overrides=overrides) @@ -74,6 +70,13 @@ def test_with_default_requirements(self, runtime, use_container): LOG.info(cmdlist) run_command(cmdlist, cwd=self.working_dir) + self._verify_image_build_artifact( + self.built_template, + self.FUNCTION_LOGICAL_ID_IMAGE, + "ImageUri", + f"{self.FUNCTION_LOGICAL_ID_IMAGE.lower()}:{_tag}", + ) + expected = {"pi": "3.14"} self._verify_invoke_built_function( self.built_template, self.FUNCTION_LOGICAL_ID_IMAGE, self._make_parameter_override_arg(overrides), expected diff --git a/tests/unit/commands/_utils/test_template.py b/tests/unit/commands/_utils/test_template.py index c75db92c67..7262ea261c 100644 --- a/tests/unit/commands/_utils/test_template.py +++ b/tests/unit/commands/_utils/test_template.py @@ -7,7 +7,7 @@ from botocore.utils import set_value_from_jmespath from parameterized import parameterized, param -from samcli.lib.utils.resources import AWS_SERVERLESS_FUNCTION, AWS_SERVERLESS_API +from samcli.lib.utils.resources import AWS_SERVERLESS_FUNCTION, AWS_SERVERLESS_API, RESOURCES_WITH_IMAGE_COMPONENT from samcli.commands._utils.template import ( get_template_data, METADATA_WITH_LOCAL_PATHS, @@ -140,6 +140,7 @@ def setUp(self): self.dest = os.path.abspath(os.path.join("src", "destination")) # /path/from/root/src/destination self.expected_result = os.path.join("..", "foo", "bar") + self.image_uri = "func12343:latest" @parameterized.expand([(resource_type, props) for resource_type, props in METADATA_WITH_LOCAL_PATHS.items()]) def test_must_update_relative_metadata_paths(self, resource_type, properties): @@ -199,6 +200,83 @@ def test_must_update_relative_resource_paths(self, resource_type, properties): self.maxDiff = None self.assertEqual(result, expected_template_dict) + @parameterized.expand([(resource_type, props) for resource_type, props in RESOURCES_WITH_IMAGE_COMPONENT.items()]) + def test_must_skip_image_components(self, resource_type, properties): + for propname in properties: + template_dict = { + "Resources": { + "ImageResource": {"Type": resource_type, "Properties": {"PackageType": "Image"}}, + } + } + + set_value_from_jmespath(template_dict, f"Resources.ImageResource.Properties.{propname}", self.image_uri) + + expected_template_dict = copy.deepcopy(template_dict) + + result = _update_relative_paths(template_dict, self.src, self.dest) + + self.maxDiff = None + self.assertEqual(result, expected_template_dict) + + @parameterized.expand( + [ + (image_resource_type, image_props, non_image_resource_type, non_image_props) + for image_resource_type, image_props in RESOURCES_WITH_IMAGE_COMPONENT.items() + for non_image_resource_type, non_image_props in RESOURCES_WITH_LOCAL_PATHS.items() + ] + ) + def test_must_skip_only_image_components_and_update_relative_resource_paths( + self, image_resource_type, image_properties, non_image_resource_type, non_image_properties + ): + for non_image_propname in non_image_properties: + for image_propname in image_properties: + template_dict = { + "Resources": { + "MyResourceWithRelativePath": {"Type": non_image_resource_type, "Properties": {}}, + "MyResourceWithS3Path": { + "Type": non_image_resource_type, + "Properties": {non_image_propname: self.s3path}, + }, + "MyResourceWithAbsolutePath": { + "Type": non_image_resource_type, + "Properties": {non_image_propname: self.abspath}, + }, + "MyResourceWithInvalidPath": { + "Type": non_image_resource_type, + "Properties": { + # Path is not a string + non_image_propname: {"foo": "bar"} + }, + }, + "MyResourceWithoutProperties": {"Type": non_image_resource_type}, + "UnsupportedResourceType": {"Type": "AWS::Ec2::Instance", "Properties": {"Code": "bar"}}, + "ResourceWithoutType": {"foo": "bar"}, + "ImageResource": {"Type": image_resource_type, "Properties": {"PackageType": "Image"}}, + }, + "Parameters": {"a": "b"}, + } + + set_value_from_jmespath( + template_dict, f"Resources.MyResourceWithRelativePath.Properties.{non_image_propname}", self.curpath + ) + + set_value_from_jmespath( + template_dict, f"Resources.ImageResource.Properties.{image_propname}", self.image_uri + ) + + expected_template_dict = copy.deepcopy(template_dict) + + set_value_from_jmespath( + expected_template_dict, + f"Resources.MyResourceWithRelativePath.Properties.{non_image_propname}", + self.expected_result, + ) + + result = _update_relative_paths(template_dict, self.src, self.dest) + + self.maxDiff = None + self.assertEqual(result, expected_template_dict) + def test_must_update_aws_include_also(self): template_dict = { "Resources": {"Fn::Transform": {"Name": "AWS::Include", "Parameters": {"Location": self.curpath}}}, From 5059f374856e93f2a934fa7e33d067eafcba001a Mon Sep 17 00:00:00 2001 From: Jonathan Ifegunni Date: Fri, 17 Dec 2021 14:47:14 -0600 Subject: [PATCH 10/10] update template_does_not_meet_filter_criteria use .get --- samcli/commands/init/init_templates.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samcli/commands/init/init_templates.py b/samcli/commands/init/init_templates.py index ad2164b233..504f8b761b 100644 --- a/samcli/commands/init/init_templates.py +++ b/samcli/commands/init/init_templates.py @@ -238,7 +238,7 @@ def template_does_not_meet_filter_criteria( True if template does not meet filter criteria else False """ return bool( - (app_template and app_template != template["appTemplate"]) - or (package_type and package_type != template["packageType"]) - or (dependency_manager and dependency_manager != template["dependencyManager"]) + (app_template and app_template != template.get("appTemplate")) + or (package_type and package_type != template.get("packageType")) + or (dependency_manager and dependency_manager != template.get("dependencyManager")) )