diff --git a/samcli/__init__.py b/samcli/__init__.py index bd96ddf148..3262170a8a 100644 --- a/samcli/__init__.py +++ b/samcli/__init__.py @@ -2,4 +2,4 @@ SAM CLI version """ -__version__ = "1.47.0" +__version__ = "1.48.0" diff --git a/samcli/commands/init/__init__.py b/samcli/commands/init/__init__.py index e93a0ddd84..5b5022bee9 100644 --- a/samcli/commands/init/__init__.py +++ b/samcli/commands/init/__init__.py @@ -231,6 +231,11 @@ def wrapped(*args, **kwargs): """ """, required=False, ) +@click.option( + "--tracing/--no-tracing", + default=None, + help="Enable AWS X-Ray tracing for your lambda functions", +) @common_options @non_interactive_validation @pass_context @@ -251,6 +256,7 @@ def cli( app_template, no_input, extra_context, + tracing, config_file, config_env, ): @@ -272,6 +278,7 @@ def cli( app_template, no_input, extra_context, + tracing, ) # pragma: no cover @@ -291,6 +298,7 @@ def do_cli( app_template, no_input, extra_context, + tracing, ): """ Implementation of the ``cli`` method @@ -339,6 +347,7 @@ def do_cli( name, no_input, extra_context, + tracing, ) else: if not (pt_explicit or runtime or dependency_manager or base_image or architecture): @@ -357,6 +366,7 @@ def do_cli( name, app_template, no_input, + tracing, ) diff --git a/samcli/commands/init/init_generator.py b/samcli/commands/init/init_generator.py index 65f4cfbc5e..1583eb403c 100644 --- a/samcli/commands/init/init_generator.py +++ b/samcli/commands/init/init_generator.py @@ -7,7 +7,17 @@ from samcli.lib.init.exceptions import InitErrorException -def do_generate(location, package_type, runtime, dependency_manager, output_dir, name, no_input, extra_context): +def do_generate( + location, + package_type, + runtime, + dependency_manager, + output_dir, + name, + no_input, + extra_context, + tracing, +): try: generate_project( location, @@ -18,6 +28,7 @@ def do_generate(location, package_type, runtime, dependency_manager, output_dir, name, no_input, extra_context, + tracing, ) except InitErrorException as e: raise UserException(str(e), wrapped_from=e.__class__.__name__) from e diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index e023e8b8c6..88cc6b4d22 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -45,6 +45,7 @@ def do_interactive( name, app_template, no_input, + tracing, ): """ Implementation of the ``cli`` method when --interactive is provided. @@ -70,6 +71,7 @@ def do_interactive( app_template, no_input, location_opt_choice, + tracing, ) @@ -86,7 +88,40 @@ def generate_application( app_template, no_input, location_opt_choice, + tracing, ): # pylint: disable=too-many-arguments + """ + The method holds the decision logic for generating an application + Parameters + ---------- + location : str + Location to SAM template + pt_explicit : bool + boolean representing if the customer explicitly stated packageType + package_type : str + Zip or Image + runtime : str + AWS Lambda runtime or Custom runtime + architecture : str + The architecture type 'x86_64' and 'arm64' in AWS + base_image : str + AWS Lambda base image + dependency_manager : str + Runtime's Dependency manager + output_dir : str + Project output directory + name : str + name of the project + app_template : str + AWS Serverless Application template + no_input : bool + Whether to prompt for input or to accept default values + (the default is False, which prompts the user for values it doesn't know for baking) + location_opt_choice : int + User input for selecting how to get customer a vended serverless application + tracing : bool + boolen value to determine if X-Ray tracing show be activated or not + """ if location_opt_choice == "1": _generate_from_use_case( location, @@ -99,14 +134,17 @@ def generate_application( name, app_template, architecture, + tracing, ) else: - _generate_from_location(location, package_type, runtime, dependency_manager, output_dir, name, no_input) + _generate_from_location( + location, package_type, runtime, dependency_manager, output_dir, name, no_input, tracing + ) # pylint: disable=too-many-statements -def _generate_from_location(location, package_type, runtime, dependency_manager, output_dir, name, no_input): +def _generate_from_location(location, package_type, runtime, dependency_manager, output_dir, name, no_input, tracing): location = click.prompt("\nTemplate location (git, mercurial, http(s), zip, path)", type=str) summary_msg = """ ----------------------- @@ -118,7 +156,7 @@ def _generate_from_location(location, package_type, runtime, dependency_manager, location=location, output_dir=output_dir ) click.echo(summary_msg) - do_generate(location, package_type, runtime, dependency_manager, output_dir, name, no_input, None) + do_generate(location, package_type, runtime, dependency_manager, output_dir, name, no_input, None, tracing) # pylint: disable=too-many-statements @@ -133,6 +171,7 @@ def _generate_from_use_case( name: Optional[str], app_template: Optional[str], architecture: Optional[str], + tracing: Optional[bool], ) -> None: templates = InitTemplates() runtime_or_base_image = runtime if runtime else base_image @@ -157,6 +196,9 @@ def _generate_from_use_case( ) runtime, base_image, package_type, dependency_manager, template_chosen = chosen_app_template_properties + if tracing is None: + tracing = prompt_user_to_enable_tracing() + app_template = template_chosen["appTemplate"] base_image = ( LAMBDA_IMAGES_RUNTIMES_MAP.get(str(runtime)) if not base_image and package_type == IMAGE else base_image @@ -202,7 +244,15 @@ def _generate_from_use_case( """ click.secho(next_commands_msg, fg="yellow") do_generate( - location, package_type, lambda_supported_runtime, dependency_manager, output_dir, name, no_input, extra_context + location, + package_type, + lambda_supported_runtime, + dependency_manager, + output_dir, + name, + no_input, + extra_context, + tracing, ) # executing event_bridge logic if call is for Schema dynamic template if is_dynamic_schemas_template: @@ -244,7 +294,7 @@ def _generate_default_hello_world_application( """ 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 or dependency_manager): - if click.confirm("\n Use the most popular runtime and package type? (Python and zip)"): + if click.confirm("\nUse the most popular runtime and package type? (Python and zip)"): runtime, package_type, dependency_manager, pt_explicit = "python3.9", ZIP, "pip", True return (runtime, package_type, dependency_manager, pt_explicit) @@ -306,6 +356,17 @@ def _get_app_template_properties( return (runtime, base_image, package_type, dependency_manager, template_chosen) +def prompt_user_to_enable_tracing(): + """ + Prompt user to if X-Ray Tracing should activated for functions in the SAM template and vice versa + """ + if click.confirm("\nWould you like to enable X-Ray tracing on the function(s) in your application? "): + doc_link = "https://aws.amazon.com/xray/pricing/" + click.echo(f"X-Ray will incur an additional cost. View {doc_link} for more details") + return True + return False + + def _get_choice_from_options(chosen, options, question, msg): if chosen: @@ -606,6 +667,7 @@ def generate_summary_message( 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 """ diff --git a/samcli/lib/init/__init__.py b/samcli/lib/init/__init__.py index f19117d954..832b698e2a 100644 --- a/samcli/lib/init/__init__.py +++ b/samcli/lib/init/__init__.py @@ -11,6 +11,7 @@ from cookiecutter.main import cookiecutter from samcli.local.common.runtime_template import RUNTIME_DEP_TEMPLATE_MAPPING, is_custom_runtime +from samcli.lib.init.template_modifiers.xray_tracing_template_modifier import XRayTracingTemplateModifier from samcli.lib.utils.packagetype import ZIP from samcli.lib.utils import osutils from .exceptions import GenerateProjectFailedError, InvalidLocationError @@ -28,6 +29,7 @@ def generate_project( name=None, no_input=False, extra_context=None, + tracing=False, ): """Generates project using cookiecutter and options given @@ -56,6 +58,8 @@ def generate_project( (the default is False, which prompts the user for values it doesn't know for baking) extra_context : Optional[Dict] An optional dictionary, the extra cookiecutter context + tracing: Optional[str] + Enable or disable X-Ray Tracing Raises ------ @@ -111,3 +115,8 @@ def generate_project( raise InvalidLocationError(template=params["template"]) from e except CookiecutterException as e: raise GenerateProjectFailedError(project=name, provider_error=e) from e + + if tracing: + template_file_path = f"{output_dir}/{name}/template.yaml" + template_modifier = XRayTracingTemplateModifier(template_file_path) + template_modifier.modify_template() diff --git a/samcli/lib/init/template_modifiers/__init__.py b/samcli/lib/init/template_modifiers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/lib/init/template_modifiers/cli_template_modifier.py b/samcli/lib/init/template_modifiers/cli_template_modifier.py new file mode 100644 index 0000000000..751c4ba739 --- /dev/null +++ b/samcli/lib/init/template_modifiers/cli_template_modifier.py @@ -0,0 +1,149 @@ +""" +Class used to parse and update template with new field +""" +import logging +from abc import abstractmethod +from copy import deepcopy +from typing import Any, List +from yaml.parser import ParserError +from samcli.yamlhelper import parse_yaml_file + + +LOG = logging.getLogger(__name__) + + +class TemplateModifier: + def __init__(self, location): + self.template_location = location + self.template = self._get_template() + self.copy_of_original_template = deepcopy(self.template) + + def modify_template(self): + """ + This method modifies the template by first added the new field to the template + and then run a sanity check on the template to know if the template matches the + CFN yaml + """ + self._add_new_field_to_template() + self._write(self.template) + if not self._sanity_check(): + self._write(self.copy_of_original_template) + + @abstractmethod + def _add_new_field_to_template(self): + pass + + def _section_position(self, section: str, position: int = 0) -> int: + """ + validate if a section in the template exist + + Parameters + ---------- + section : str + A section in the SAM template + position : int + position to start searching for the section + + Returns + ------- + int + index of section in the template list + """ + template = self.template[position:] + for index, line in enumerate(template): + if line.startswith(section): + section_index = index + position if position else index + return section_index + return -1 + + def _add_fields_to_section(self, position: int, fields: str) -> Any: + """ + Adds fields to section in the template + + Parameters + ---------- + position : int + position to start searching for the section + fields : str + fields to be added to the SAM template + + Returns + ------- + list + array with updated template data + """ + template = self.template[position:] + for index, line in enumerate(template): + if not (line.startswith(" ") or line.startswith("#")): + return self.template[: position + index] + fields + self.template[position + index :] + return self.template + + def _field_position(self, position: int, field: str) -> Any: + """ + Checks if the field needed to be added to the SAM template already exist in the template + + Parameters + ---------- + position : int + section position to start the search + field : str + Field name + + Returns + ------- + int + index of the field if it exist else -1 + """ + template = self.template[position:] + for index, line in enumerate(template): + if field in line: + return position + index + if not (line.startswith(" ") or line.startswith("#")): + break + return -1 + + def _sanity_check(self) -> bool: + """ + Conducts sanity check on template using yaml parser to ensure the updated template meets + CFN template criteria + + Returns + ------- + bool + True if templates passes sanity check else False + """ + try: + parse_template = parse_yaml_file(self.template_location) + return bool(parse_template) + except ParserError: + self._print_sanity_check_error() + return False + + @abstractmethod + def _print_sanity_check_error(self): + pass + + def _write(self, template: list): + """ + write generated template into SAM template + + Parameters + ---------- + template : list + array with updated template data + """ + with open(self.template_location, "w") as file: + for line in template: + file.write(line) + + def _get_template(self) -> List[str]: + """ + Gets data the SAM templates and returns it in a array + + Returns + ------- + list + array with updated template data + """ + with open(self.template_location, "r") as file: + return file.readlines() diff --git a/samcli/lib/init/template_modifiers/xray_tracing_template_modifier.py b/samcli/lib/init/template_modifiers/xray_tracing_template_modifier.py new file mode 100644 index 0000000000..0a3bb68fb5 --- /dev/null +++ b/samcli/lib/init/template_modifiers/xray_tracing_template_modifier.py @@ -0,0 +1,67 @@ +""" +Class used to parse and update template when tracing is enabled +""" +import logging +from samcli.lib.init.template_modifiers.cli_template_modifier import TemplateModifier + +LOG = logging.getLogger(__name__) + + +class XRayTracingTemplateModifier(TemplateModifier): + + FIELD_NAME = "Tracing" + GLOBALS = "Globals:\n" + RESOURCE = "Resources:\n" + FUNCTION = " Function:\n" + TRACING = " Tracing: Active\n" + COMMENT = ( + "# More info about Globals: " + "https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst\n" + ) + + def _add_new_field_to_template(self): + """ + Add new field to SAM template + """ + global_section_position = self._section_position(self.GLOBALS) + + if global_section_position >= 0: + function_section_position = self._section_position(self.FUNCTION, global_section_position) + + if function_section_position >= 0: + field_positon = self._field_position(function_section_position, self.FIELD_NAME) + if field_positon >= 0: + self.template[field_positon] = self.TRACING + return + + new_fields = [self.TRACING] + _section_position = function_section_position + + else: + new_fields = [self.FUNCTION, self.TRACING] + _section_position = global_section_position + 1 + + self.template = self._add_fields_to_section(_section_position, new_fields) + + else: + resource_section_position = self._section_position(self.RESOURCE) + globals_section_data = [ + self.COMMENT, + self.GLOBALS, + self.FUNCTION, + self.TRACING, + "\n", + ] + self.template = ( + self.template[:resource_section_position] + + globals_section_data + + self.template[resource_section_position:] + ) + + def _print_sanity_check_error(self): + link = ( + "https://docs.aws.amazon.com/serverless-application-model/latest" + "/developerguide/sam-resource-function.html#sam-function-tracing" + ) + message = f"Warning: Unable to add Tracing to the project. To learn more about Tracing visit {link}" + LOG.warning(message) diff --git a/samcli/lib/providers/provider.py b/samcli/lib/providers/provider.py index 6888657d41..81f5d886f3 100644 --- a/samcli/lib/providers/provider.py +++ b/samcli/lib/providers/provider.py @@ -483,7 +483,7 @@ def get_all(self) -> Iterator[Api]: raise NotImplementedError("not implemented") -class Stack(NamedTuple): +class Stack: """ A class encapsulate info about a stack/sam-app resource, including its content, parameter overrides, file location, logicalID @@ -504,6 +504,23 @@ class Stack(NamedTuple): # metadata metadata: Optional[Dict] = None + def __init__( + self, + parent_stack_path: str, + name: str, + location: str, + parameters: Optional[Dict], + template_dict: Dict, + metadata: Optional[Dict] = None, + ): + self.parent_stack_path = parent_stack_path + self.name = name + self.location = location + self.parameters = parameters + self.template_dict = template_dict + self.metadata = metadata + self._resources: Optional[Dict] = None + @property def stack_id(self) -> str: _metadata = self.metadata if self.metadata else {} @@ -533,9 +550,11 @@ def resources(self) -> Dict: Return the resources dictionary where SAM plugins have been run and parameter values have been substituted. """ - processed_template_dict: Dict = SamBaseProvider.get_template(self.template_dict, self.parameters) - resources: Dict = processed_template_dict.get("Resources", {}) - return resources + if self._resources is not None: + return self._resources + processed_template_dict: Dict[str, Dict] = SamBaseProvider.get_template(self.template_dict, self.parameters) + self._resources = cast(Dict, processed_template_dict.get("Resources", {})) + return self._resources def get_output_template_path(self, build_root: str) -> str: """ @@ -544,6 +563,21 @@ def get_output_template_path(self, build_root: str) -> str: # stack_path is always posix path, we need to convert it to path that matches the OS return os.path.join(build_root, self.stack_path.replace(posixpath.sep, os.path.sep), "template.yaml") + def __eq__(self, other: Any) -> bool: + if isinstance(other, Stack): + return ( + self.is_root_stack == other.is_root_stack + and self.location == other.location + and self.metadata == other.metadata + and self.name == other.name + and self.parameters == other.parameters + and self.parent_stack_path == other.parent_stack_path + and self.stack_id == other.stack_id + and self.stack_path == other.stack_path + and self.template_dict == other.template_dict + ) + return False + class ResourceIdentifier: """Resource identifier for representing a resource with nested stack support""" diff --git a/samcli/lib/providers/sam_stack_provider.py b/samcli/lib/providers/sam_stack_provider.py index bc5debeea2..8425296c34 100644 --- a/samcli/lib/providers/sam_stack_provider.py +++ b/samcli/lib/providers/sam_stack_provider.py @@ -55,14 +55,14 @@ def __init__( self._resources = self._template_dict.get("Resources", {}) self._global_parameter_overrides = global_parameter_overrides - LOG.debug("%d stacks found in the template", len(self._resources)) - # Store a map of stack name to stack information for quick reference -> self._stacks # and detect remote stacks -> self._remote_stack_full_paths self._stacks: Dict[str, Stack] = {} self.remote_stack_full_paths: List[str] = [] self._extract_stacks() + LOG.debug("%d stacks found in the template", len(self._stacks)) + def get(self, name: str) -> Optional[Stack]: """ Returns the application given name or LogicalId of the application. diff --git a/tests/functional/commands/init/test_interactive_init_flow.py b/tests/functional/commands/init/test_interactive_init_flow.py index 6102db56c6..831dd9ee69 100644 --- a/tests/functional/commands/init/test_interactive_init_flow.py +++ b/tests/functional/commands/init/test_interactive_init_flow.py @@ -58,6 +58,7 @@ def test_unknown_runtime(self, git_repo_mock, requests_mock): name=None, app_template=None, no_input=False, + tracing=False, ) output_files = list(self.output_dir.rglob("*")) self.assertEqual(len(output_files), 8) diff --git a/tests/integration/deploy/deploy_integ_base.py b/tests/integration/deploy/deploy_integ_base.py index c8d77f95c0..cbce4674ea 100644 --- a/tests/integration/deploy/deploy_integ_base.py +++ b/tests/integration/deploy/deploy_integ_base.py @@ -169,7 +169,7 @@ def get_deploy_command_list( return command_list @staticmethod - def get_minimal_build_command_list(self, template_file=None, build_dir=None): + def get_minimal_build_command_list(template_file=None, build_dir=None): command_list = [get_sam_command(), "build"] if template_file: diff --git a/tests/integration/init/schemas/schemas_test_data_setup.py b/tests/integration/init/schemas/schemas_test_data_setup.py index 3a57119d6e..de462c3763 100644 --- a/tests/integration/init/schemas/schemas_test_data_setup.py +++ b/tests/integration/init/schemas/schemas_test_data_setup.py @@ -171,7 +171,7 @@ def _create_3p_schemas(registry_name, schemas_client, no_of_schemas): '{"openapi":"3.0.0","info":{"version":"1.0.0","title":"TicketCreated"},"paths":{},"components":{"schemas":{"AWSEvent":{"type":"object",' '"required":["detail-type","resources","id","source","time","detail","region","version","account"],"x-amazon-events-detail-type":"MongoDB Trigger for ' 'my_store.reviews","x-amazon-events-source":"aws.partner-mongodb.com","properties":{"detail":{' - r'"$ref":"#\/components\/schemas\/aws.partner\/mongodb.com\/Ticket.Created"},"detail-type":{"type":"string"},"resources":{"type":"array",' + r'"$ref":"#/components/schemas/TicketCreated"},"detail-type":{"type":"string"},"resources":{"type":"array",' '"items":{"type":"string"}},"id":{"type":"string"},"source":{"type":"string"},"time":{"type":"string","format":"date-time"},' '"region":{"type":"string","enum":["ap-south-1","eu-west-3","eu-north-1","eu-west-2","eu-west-1","ap-northeast-2","ap-northeast-1","me-south-1",' '"sa-east-1","ca-central-1","ap-east-1","cn-north-1","us-gov-west-1","ap-southeast-1","ap-southeast-2","eu-central-1","us-east-1","us-west-1",' @@ -180,7 +180,7 @@ def _create_3p_schemas(registry_name, schemas_client, no_of_schemas): ) for i in range(0, no_of_schemas): schema_name = "schema_test-%s" % i - _create_schema_if_not_exist(registry_name, schema_name, content, "1", "test-schema", "OpenApi3", schemas_client) + _create_or_recreate_schema(registry_name, schema_name, content, "1", "test-schema", "OpenApi3", schemas_client) def _create_2p_schemas(registry_name, schemas_client): @@ -190,21 +190,27 @@ def _create_2p_schemas(registry_name, schemas_client): ) for i in range(0, 2): schema_name = "schema_test-%s" % i - _create_schema_if_not_exist(registry_name, schema_name, content, "1", "test-schema", "OpenApi3", schemas_client) + _create_or_recreate_schema(registry_name, schema_name, content, "1", "test-schema", "OpenApi3", schemas_client) -def _create_schema_if_not_exist( +def _create_or_recreate_schema( registry_name, schema_name, content, schema_version, schema_description, schema_type, schemas_client ): try: schemas_client.describe_schema(RegistryName=registry_name, SchemaName=schema_name, SchemaVersion=schema_version) + schemas_client.delete_schema(RegistryName=registry_name, SchemaName=schema_name) + _create_schema(registry_name, schema_name, content, schema_description, schema_type, schemas_client) except ClientError as e: if e.response["Error"]["Code"] == "NotFoundException": - schemas_client.create_schema( - RegistryName=registry_name, - SchemaName=schema_name, - Content=content, - Description=schema_description, - Type=schema_type, - ) - time.sleep(SLEEP_TIME) + _create_schema(registry_name, schema_name, content, schema_description, schema_type, schemas_client) + + +def _create_schema(registry_name, schema_name, content, schema_description, schema_type, schemas_client): + schemas_client.create_schema( + RegistryName=registry_name, + SchemaName=schema_name, + Content=content, + Description=schema_description, + Type=schema_type, + ) + time.sleep(SLEEP_TIME) 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 01959f1e18..faf19c0701 100644 --- a/tests/integration/init/schemas/test_init_with_schemas_command.py +++ b/tests/integration/init/schemas/test_init_with_schemas_command.py @@ -1,12 +1,11 @@ import os import tempfile +from pathlib import Path from unittest import skipIf from click.testing import CliRunner -from samcli.commands.init import cli as init_cmd -from pathlib import Path -from samcli.lib.utils.packagetype import ZIP +from samcli.commands.init import cli as init_cmd from tests.integration.init.schemas.schemas_test_data_setup import SchemaTestDataSetup from tests.testing_utils import RUNNING_ON_CI, RUNNING_TEST_FOR_MASTER_ON_CI, RUN_BY_CANARY @@ -23,6 +22,7 @@ def test_init_interactive_with_event_bridge_app_aws_registry(self): # 2: Java Runtime (java11) # 2: Maven # 2: select event-bridge app from scratch + # N: disable adding xray tracing # test-project: response to name # Y: Use default aws configuration # 1: select schema from cli_paginator @@ -35,6 +35,7 @@ def test_init_interactive_with_event_bridge_app_aws_registry(self): 2 2 2 +N eb-app-maven Y 1 @@ -61,6 +62,7 @@ def test_init_interactive_with_event_bridge_app_partner_registry(self): # 2: Java Runtime # 2: Maven # 2: select event-bridge app from scratch + # N: disable adding xray tracing # test-project: response to name # Y: Use default aws configuration # 3: partner registry @@ -72,6 +74,7 @@ def test_init_interactive_with_event_bridge_app_partner_registry(self): 2 2 2 +N eb-app-maven Y 3 @@ -108,6 +111,7 @@ def test_init_interactive_with_event_bridge_app_pagination(self): # 2: Java Runtime # 2: Maven # 2: select event-bridge app from scratch + # N: disable adding xray tracing # eb-app-maven: response to name # Y: Use default aws configuration # 4: select pagination-registry as registries @@ -121,6 +125,7 @@ def test_init_interactive_with_event_bridge_app_pagination(self): 2 2 2 +N eb-app-maven Y 4 @@ -148,6 +153,7 @@ def test_init_interactive_with_event_bridge_app_customer_registry(self): # 2: Java Runtime # 2: Maven # 2: select event-bridge app from scratch + # N: disable adding xray tracing # eb-app-maven: response to name # Y: Use default aws configuration # 2: select 2p-schema other-schema @@ -159,6 +165,7 @@ def test_init_interactive_with_event_bridge_app_customer_registry(self): 2 2 2 +N eb-app-maven Y 2 @@ -194,6 +201,7 @@ def test_init_interactive_with_event_bridge_app_aws_schemas_python(self): # 7: Infrastructure event management - Use case # 6: Python 3.8 # 2: select event-bridge app from scratch + # N: disable adding xray tracing # eb-app-python38: response to name # Y: Use default aws configuration # 4: select aws.events as registries @@ -204,6 +212,7 @@ def test_init_interactive_with_event_bridge_app_aws_schemas_python(self): 7 6 2 +N eb-app-python38 Y 1 @@ -226,6 +235,7 @@ def test_init_interactive_with_event_bridge_app_aws_schemas_go(self): # 7: Infrastructure event management - Use case # 1: Go 1.x # 2: select event-bridge app from scratch + # N: disable adding xray tracing # eb-app-go: response to name # Y: Use default aws configuration # 4: select aws.events as registries @@ -236,6 +246,7 @@ def test_init_interactive_with_event_bridge_app_aws_schemas_go(self): 7 1 2 +N eb-app-go Y 4 @@ -258,6 +269,7 @@ def test_init_interactive_with_event_bridge_app_non_default_profile_selection(se # 3: Infrastructure event management - Use case # 6: Python 3.8 # 2: select event-bridge app from scratch + # N: disable adding xray tracing # eb-app-python38: response to name # N: Use default profile # 2: uses second profile from displayed one (myprofile) @@ -270,6 +282,7 @@ def test_init_interactive_with_event_bridge_app_non_default_profile_selection(se 7 6 2 +N eb-app-python38 3 N @@ -297,6 +310,7 @@ def test_init_interactive_with_event_bridge_app_non_supported_schemas_region(sel # 7: Infrastructure event management - Use case # 6: Python 3.8 # 2: select event-bridge app from scratch + # N: disable adding xray tracing # eb-app-python38: response to name # Y: Use default profile # 1: select aws.events as registries @@ -307,6 +321,7 @@ def test_init_interactive_with_event_bridge_app_non_supported_schemas_region(sel 7 6 2 +N eb-app-python38 Y 1 @@ -317,10 +332,3 @@ def test_init_interactive_with_event_bridge_app_non_supported_schemas_region(sel result = runner.invoke(init_cmd, ["--output-dir", temp], input=user_input) self.assertTrue(result.exception) self._tear_down_custom_config() - - -def _get_command(): - command = "sam" - if os.getenv("SAM_CLI_DEV"): - command = "samdev" - return command diff --git a/tests/integration/init/test_init_command.py b/tests/integration/init/test_init_command.py index 69add26779..7e6d0c878e 100644 --- a/tests/integration/init/test_init_command.py +++ b/tests/integration/init/test_init_command.py @@ -1,8 +1,10 @@ -from samcli.lib.utils.osutils import stderr +from click.testing import CliRunner + +from samcli.commands.init import cli as init_cmd from unittest import TestCase from parameterized import parameterized -from subprocess import STDOUT, Popen, TimeoutExpired, PIPE +from subprocess import Popen, TimeoutExpired, PIPE import os import shutil import tempfile @@ -10,6 +12,8 @@ from pathlib import Path +from tests.testing_utils import get_sam_command + TIMEOUT = 300 COMMIT_ERROR = "WARN: Commit not exist:" @@ -20,7 +24,7 @@ def test_init_command_passes_and_dir_created(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--runtime", "nodejs14.x", @@ -54,7 +58,7 @@ def test_init_command_passes_and_dir_created_image(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--package-type", IMAGE, @@ -82,7 +86,7 @@ def test_init_new_app_template(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--runtime", "nodejs14.x", @@ -114,7 +118,7 @@ def test_init_command_java_maven(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--runtime", "java8", @@ -146,7 +150,7 @@ def test_init_command_java_gradle(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--runtime", "java8", @@ -178,7 +182,7 @@ def test_init_command_with_extra_context_parameter(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--runtime", "java8", @@ -212,7 +216,7 @@ def test_init_command_passes_with_arm_architecture(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--runtime", "nodejs14.x", @@ -246,7 +250,7 @@ def test_init_command_passes_with_x86_64_architecture(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--runtime", "nodejs14.x", @@ -280,7 +284,7 @@ def test_init_command_passes_with_unknown_architecture(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--runtime", "nodejs14.x", @@ -309,6 +313,64 @@ def test_init_command_passes_with_unknown_architecture(self): msg = "Invalid value for '-a' / '--architecture': invalid choice: unknown_arch. (choose from arm64, x86_64)" self.assertIn(capture_output, msg) + def test_init_command_passes_with_enabled_tracing(self): + with tempfile.TemporaryDirectory() as temp: + process = Popen( + [ + get_sam_command(), + "init", + "--runtime", + "nodejs14.x", + "--dependency-manager", + "npm", + "--app-template", + "hello-world", + "--name", + "sam-app", + "--no-interactive", + "-o", + temp, + "--tracing", + ] + ) + try: + process.communicate(timeout=TIMEOUT) + except TimeoutExpired: + process.kill() + raise + + self.assertEqual(process.returncode, 0) + self.assertTrue(Path(temp, "sam-app").is_dir()) + + def test_init_command_passes_with_disabled_tracing(self): + with tempfile.TemporaryDirectory() as temp: + process = Popen( + [ + get_sam_command(), + "init", + "--runtime", + "nodejs14.x", + "--dependency-manager", + "npm", + "--app-template", + "hello-world", + "--name", + "sam-app", + "--no-interactive", + "-o", + temp, + "--no-tracing", + ] + ) + try: + process.communicate(timeout=TIMEOUT) + except TimeoutExpired: + process.kill() + raise + + self.assertEqual(process.returncode, 0) + self.assertTrue(Path(temp, "sam-app").is_dir()) + MISSING_REQUIRED_PARAM_MESSAGE = """Error: Missing required parameters, with --no-interactive set. Must provide one of the following required parameter combinations: @@ -332,7 +394,7 @@ def test_init_command_no_interactive_missing_name(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--runtime", "nodejs14.x", @@ -363,7 +425,7 @@ def test_init_command_no_interactive_apptemplate_location(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--app-template", "hello-world", @@ -395,7 +457,7 @@ def test_init_command_no_interactive_runtime_location(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--runtime", "nodejs14.x", @@ -426,7 +488,7 @@ def test_init_command_no_interactive_base_image_location(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--base-image", "amazon/nodejs14.x-base", @@ -458,7 +520,7 @@ def test_init_command_no_interactive_base_image_no_dependency(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--package-type", IMAGE, @@ -487,7 +549,7 @@ def test_init_command_no_interactive_packagetype_location(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--package-type", ZIP, @@ -519,7 +581,7 @@ def test_init_command_no_interactive_base_image_no_packagetype(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--base-image", "amazon/nodejs14.x-base", @@ -546,7 +608,7 @@ def test_init_command_wrong_packagetype(self): with tempfile.TemporaryDirectory() as temp: process = Popen( [ - _get_command(), + get_sam_command(), "init", "--package-type", "WrongPT", @@ -570,7 +632,7 @@ def test_init_command_wrong_packagetype(self): Error: Invalid value for '-p' / '--package-type': invalid choice: WrongPT. (choose from Zip, Image) """.format( - _get_command() + get_sam_command() ) self.assertEqual(errmsg.strip(), "\n".join(stderr.strip().splitlines())) @@ -594,7 +656,7 @@ def tearDown(self): @parameterized.expand([(None,), ("project_name",)]) def test_arbitrary_project(self, project_name): with tempfile.TemporaryDirectory() as temp: - args = [_get_command(), "init", "--location", self.zip_path, "-o", temp] + args = [get_sam_command(), "init", "--location", self.zip_path, "-o", temp] if project_name: args.extend(["--name", project_name]) @@ -613,7 +675,7 @@ def test_arbitrary_project(self, project_name): def test_zip_not_exists(self): with tempfile.TemporaryDirectory() as temp: - args = [_get_command(), "init", "--location", str(Path("invalid", "zip", "path")), "-o", temp] + args = [get_sam_command(), "init", "--location", str(Path("invalid", "zip", "path")), "-o", temp] process = Popen(args) try: @@ -625,8 +687,44 @@ def test_zip_not_exists(self): self.assertEqual(process.returncode, 1) -def _get_command(): - command = "sam" - if os.getenv("SAM_CLI_DEV"): - command = "samdev" - return command +class TestInteractiveInit(TestCase): + def test_interactive_init(self): + user_input = """ +1 +1 +N +8 +1 +1 +N +sam-interactive-init-app + """ + with tempfile.TemporaryDirectory() as temp: + runner = CliRunner() + result = runner.invoke(init_cmd, ["--output-dir", temp, "--debug"], input=user_input) + + self.assertFalse(result.exception) + expected_output_folder = Path(temp, "sam-interactive-init-app") + self.assertTrue(expected_output_folder.exists) + self.assertTrue(expected_output_folder.is_dir()) + self.assertTrue(Path(expected_output_folder, "hello-world").is_dir()) + self.assertTrue(Path(expected_output_folder, "hello-world", "app.js").is_file()) + + def test_interactive_init_default_runtime(self): + user_input = """ +1 +1 +Y +N +sam-interactive-init-app-default-runtime + """ + with tempfile.TemporaryDirectory() as temp: + runner = CliRunner() + result = runner.invoke(init_cmd, ["--output-dir", temp, "--debug"], input=user_input) + + self.assertFalse(result.exception) + expected_output_folder = Path(temp, "sam-interactive-init-app-default-runtime") + self.assertTrue(expected_output_folder.exists) + self.assertTrue(expected_output_folder.is_dir()) + self.assertTrue(Path(expected_output_folder, "hello_world").is_dir()) + self.assertTrue(Path(expected_output_folder, "hello_world", "app.py").is_file()) diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index b2483c47d3..95269e3b61 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -117,6 +117,7 @@ def test_init_cli(self, generate_project_patch, git_repo_clone_mock): app_template=self.app_template, no_input=self.no_input, extra_context=None, + tracing=False, ) # THEN we should receive no errors @@ -131,6 +132,7 @@ def test_init_cli(self, generate_project_patch, git_repo_clone_mock): self.name, True, self.extra_context_as_json, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -153,6 +155,7 @@ def test_init_image_cli(self, generate_project_patch, git_repo_clone_mock): app_template=None, no_input=self.no_input, extra_context=None, + tracing=False, ) # THEN we should receive no errors @@ -166,6 +169,45 @@ def test_init_image_cli(self, generate_project_patch, git_repo_clone_mock): self.name, True, {"runtime": "nodejs12.x", "project_name": "testing project", "architectures": {"value": [ARM64]}}, + False, + ) + + @patch("samcli.lib.utils.git_repo.GitRepo.clone") + @patch("samcli.commands.init.init_generator.generate_project") + def test_init_cli_with_tracing(self, generate_project_patch, git_repo_clone_mock): + # GIVEN generate_project successfully created a project + # WHEN a project name has been passed + init_cli( + ctx=self.ctx, + no_interactive=self.no_interactive, + location=self.location, + pt_explicit=self.pt_explicit, + package_type=self.package_type, + runtime=self.runtime, + architecture=X86_64, + base_image=self.base_image, + dependency_manager=self.dependency_manager, + output_dir=None, + name=self.name, + app_template=self.app_template, + no_input=self.no_input, + extra_context=None, + tracing=True, + ) + + # THEN we should receive no errors + self.extra_context_as_json["architectures"] = {"value": [X86_64]} + generate_project_patch.assert_called_once_with( + # need to change the location validation check + ANY, + ZIP, + self.runtime, + self.dependency_manager, + self.output_dir, + self.name, + True, + self.extra_context_as_json, + True, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -188,6 +230,7 @@ def test_init_image_java_cli(self, generate_project_patch, git_repo_clone_mock): app_template=None, no_input=self.no_input, extra_context=None, + tracing=False, ) # THEN we should receive no errors @@ -201,6 +244,7 @@ def test_init_image_java_cli(self, generate_project_patch, git_repo_clone_mock): self.name, True, {"runtime": "java11", "project_name": "testing project", "architectures": {"value": [X86_64]}}, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -223,6 +267,7 @@ def test_init_fails_invalid_template(self, git_repo_clone_mock): app_template="wrong-and-bad", no_input=self.no_input, extra_context=None, + tracing=False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -245,6 +290,7 @@ def test_init_fails_invalid_dep_mgr(self, git_repo_clone_mock): app_template=self.app_template, no_input=self.no_input, extra_context=None, + tracing=False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -273,10 +319,17 @@ def test_init_cli_generate_project_fails(self, generate_project_patch, git_repo_ app_template=None, no_input=self.no_input, extra_context=None, + tracing=False, ) generate_project_patch.assert_called_with( - self.location, self.runtime, self.dependency_manager, self.output_dir, self.name, self.no_input + self.location, + self.runtime, + self.dependency_manager, + self.output_dir, + self.name, + self.no_input, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -305,10 +358,17 @@ def test_init_cli_generate_project_image_fails(self, generate_project_patch, git app_template=None, no_input=self.no_input, extra_context=None, + tracing=False, ) generate_project_patch.assert_called_with( - self.location, self.runtime, self.dependency_manager, self.output_dir, self.name, self.no_input + self.location, + self.runtime, + self.dependency_manager, + self.output_dir, + self.name, + self.no_input, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -331,12 +391,13 @@ def test_init_cli_with_extra_context_parameter_not_passed(self, generate_project app_template=self.app_template, no_input=self.no_input, extra_context=None, + tracing=False, ) # THEN we should receive no errors self.extra_context_as_json["architectures"] = {"value": [ARM64]} generate_project_patch.assert_called_once_with( - ANY, ZIP, self.runtime, self.dependency_manager, ".", self.name, True, self.extra_context_as_json + ANY, ZIP, self.runtime, self.dependency_manager, ".", self.name, True, self.extra_context_as_json, False ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -359,6 +420,7 @@ def test_init_cli_with_extra_context_parameter_passed(self, generate_project_pat app_template=self.app_template, no_input=self.no_input, extra_context='{"schema_name":"events", "schema_type":"aws"}', + tracing=False, ) # THEN we should receive no errors and right extra_context should be passed @@ -377,6 +439,7 @@ def test_init_cli_with_extra_context_parameter_passed(self, generate_project_pat "schema_type": "aws", "architectures": {"value": [X86_64]}, }, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -401,6 +464,7 @@ def test_init_cli_with_extra_context_not_overriding_default_parameter( app_template=self.app_template, no_input=self.no_input, extra_context='{"project_name": "my_project", "runtime": "java8", "schema_name":"events", "schema_type": "aws"}', + tracing=False, ) # THEN extra_context should have not overridden default_parameters(name, runtime) @@ -419,6 +483,7 @@ def test_init_cli_with_extra_context_not_overriding_default_parameter( "schema_type": "aws", "architectures": {"value": [ARM64]}, }, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -441,6 +506,7 @@ def test_init_cli_with_extra_context_input_as_wrong_json_raises_exception(self, app_template=self.app_template, no_input=self.no_input, extra_context='{"project_name", "my_project", "runtime": "java8", "schema_name":"events", "schema_type": "aws"}', + tracing=False, ) @patch("samcli.commands.init.init_generator.generate_project") @@ -462,6 +528,7 @@ def test_init_cli_must_set_default_context_when_location_is_provided(self, gener app_template=None, no_input=None, extra_context='{"schema_name":"events", "schema_type": "aws"}', + tracing=False, ) # THEN should set default parameter(name, runtime) as extra_context @@ -480,6 +547,7 @@ def test_init_cli_must_set_default_context_when_location_is_provided(self, gener "project_name": "test-project", "architectures": {"value": [X86_64]}, }, + False, ) @patch("samcli.commands.init.init_generator.generate_project") @@ -501,6 +569,7 @@ def test_init_cli_must_only_set_passed_project_name_when_location_is_provided(se app_template=None, no_input=None, extra_context='{"schema_name":"events", "schema_type": "aws"}', + tracing=False, ) # THEN extra_context should be without runtime @@ -518,6 +587,7 @@ def test_init_cli_must_only_set_passed_project_name_when_location_is_provided(se "project_name": "test-project", "architectures": {"value": [ARM64]}, }, + False, ) @patch("samcli.commands.init.init_generator.generate_project") @@ -539,6 +609,7 @@ def test_init_cli_must_only_set_passed_runtime_when_location_is_provided(self, g app_template=None, no_input=None, extra_context='{"schema_name":"events", "schema_type": "aws"}', + tracing=False, ) # THEN extra_context should be without name @@ -556,6 +627,7 @@ def test_init_cli_must_only_set_passed_runtime_when_location_is_provided(self, g "runtime": "java8", "architectures": {"value": [ARM64]}, }, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -579,6 +651,7 @@ def test_init_cli_with_extra_context_parameter_passed_as_escaped(self, generate_ no_input=self.no_input, # fmt: off extra_context='{\"schema_name\":\"events\", \"schema_type\":\"aws\"}', + tracing=False, # fmt: on ) @@ -598,6 +671,7 @@ def test_init_cli_with_extra_context_parameter_passed_as_escaped(self, generate_ "schema_type": "aws", "architectures": {"value": [X86_64]}, }, + False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -709,6 +783,7 @@ def test_init_cli_int_with_event_bridge_app_template( user_input = """ 1 2 +N test-project Y 1 @@ -737,6 +812,7 @@ def test_init_cli_int_with_event_bridge_app_template( "AWS_Schema_root": "schemas.aws.AWSAPICallViaCloudTrail", "architectures": {"value": [X86_64]}, }, + False, ) get_schemas_client_mock.assert_called_once_with(None, "ap-northeast-1") do_extract_and_merge_schemas_code_mock.do_extract_and_merge_schemas_code_mock( @@ -789,6 +865,7 @@ def test_init_cli_int_with_image_app_template( user_input = """ 1 +N test-project """ runner = CliRunner() @@ -803,6 +880,7 @@ def test_init_cli_int_with_image_app_template( "test-project", True, {"project_name": "test-project", "runtime": "java8", "architectures": {"value": [X86_64]}}, + False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -917,6 +995,7 @@ def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration( user_input = """ 1 2 +N test-project N 1 @@ -947,6 +1026,7 @@ def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration( "AWS_Schema_root": "schemas.aws.AWSAPICallViaCloudTrail", "architectures": {"value": [X86_64]}, }, + False, ) get_schemas_client_mock.assert_called_once_with("default", "us-east-1") do_extract_and_merge_schemas_code_mock.do_extract_and_merge_schemas_code("result.zip", ".", "test-project", ANY) @@ -1042,6 +1122,7 @@ def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration_with_ 2 1 1 +N test-project N 1 @@ -1167,6 +1248,7 @@ def test_init_cli_int_with_download_manager_raises_exception( user_input = """ 1 2 +N test-project Y 1 @@ -1195,6 +1277,7 @@ def test_init_cli_int_with_download_manager_raises_exception( "AWS_Schema_root": "schemas.aws.AWSAPICallViaCloudTrail", "architectures": {"value": [X86_64]}, }, + False, ) get_schemas_client_mock.assert_called_once_with(None, "ap-northeast-1") do_extract_and_merge_schemas_code_mock.do_extract_and_merge_schemas_code_mock( @@ -1307,6 +1390,7 @@ def test_init_cli_int_with_schemas_details_raises_exception( 2 1 1 +N test-project Y 1 @@ -1340,6 +1424,7 @@ def test_init_passes_dynamic_event_bridge_template(self, generate_project_patch, no_input=self.no_input, extra_context=None, architecture=ARM64, + tracing=False, ) self.extra_context_as_json["architectures"] = {"value": [ARM64]} @@ -1353,6 +1438,7 @@ def test_init_passes_dynamic_event_bridge_template(self, generate_project_patch, self.name, True, self.extra_context_as_json, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -1382,6 +1468,7 @@ def test_init_cli_int_from_location(self, generate_project_patch, git_repo_clone None, False, None, + None, ) @patch("samcli.commands.init.init_templates.InitTemplates._get_manifest") @@ -1416,14 +1503,7 @@ def test_init_cli_no_package_type(self, generate_project_patch, git_repo_clone_m # THEN we should receive no errors self.assertFalse(result.exception) generate_project_patch.assert_called_once_with( - ANY, - IMAGE, - "python3.8", - "pip", - ".", - "untitled6", - True, - ANY, + ANY, IMAGE, "python3.8", "pip", ".", "untitled6", True, ANY, False ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1464,6 +1544,7 @@ def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_bu runtime=None, no_input=self.no_input, extra_context=self.extra_context, + tracing=False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1504,6 +1585,7 @@ def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_an runtime=None, no_input=self.no_input, extra_context=self.extra_context, + tracing=False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1545,6 +1627,7 @@ def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_wi runtime=None, no_input=None, extra_context=None, + tracing=False, ) generate_project_patch.assert_called_once_with( ANY, # location @@ -1555,6 +1638,7 @@ def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_wi self.name, True, # no_input ANY, + False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1588,6 +1672,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_does_no runtime=None, no_input=None, extra_context=None, + tracing=False, architecture=None, ) generate_project_patch.assert_called_once_with( @@ -1599,6 +1684,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_does_no self.name, True, # no_input ANY, + False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1632,6 +1718,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_with_pr runtime=None, no_input=None, extra_context=None, + tracing=False, architecture=None, ) generate_project_patch.assert_called_once_with( @@ -1643,6 +1730,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_with_pr self.name, True, # no_input ANY, + False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1677,6 +1765,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_with_pr runtime=None, no_input=None, extra_context=None, + tracing=False, architecture=None, ) @@ -1707,14 +1796,7 @@ def test_init_cli_must_pass_with_architecture_and_base_image(self, generate_proj # THEN we should receive no errors self.assertFalse(result.exception) generate_project_patch.assert_called_once_with( - ANY, - IMAGE, - "java11", - "gradle", - ".", - "untitled6", - True, - ANY, + ANY, IMAGE, "java11", "gradle", ".", "untitled6", True, ANY, None ) PackageType.explicit = ( False # Other tests fail after we pass --packge-type in this test, so let's reset this variable @@ -1787,6 +1869,7 @@ def test_init_cli_generate_default_hello_world_app( user_input = """ 1 y +N test-project """ @@ -1802,6 +1885,7 @@ def test_init_cli_generate_default_hello_world_app( "test-project", True, {"project_name": "test-project", "runtime": "python3.9", "architectures": {"value": ["x86_64"]}}, + False, ) @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @@ -1872,6 +1956,7 @@ def test_init_cli_must_not_generate_default_hello_world_app( 1 n 1 +N test-project """ @@ -1887,6 +1972,7 @@ def test_init_cli_must_not_generate_default_hello_world_app( "test-project", True, {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, + False, ) def test_must_return_runtime_from_base_image_name(self): @@ -2043,6 +2129,7 @@ def test_init_fails_unsupported_dep_mgr_for_runtime(self, git_repo_clone_mock): app_template=self.app_template, no_input=self.no_input, extra_context=None, + tracing=False, architecture=X86_64, ) expected_error_message = ( @@ -2143,6 +2230,7 @@ def test_init_cli_int_with_multiple_app_templates( user_input = """ 1 1 +N test-project """ runner = CliRunner() @@ -2157,6 +2245,7 @@ def test_init_cli_int_with_multiple_app_templates( "test-project", True, {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, + False, ) @patch("samcli.commands.init.init_templates.LOG") @@ -2223,6 +2312,7 @@ def test_init_cli_int_must_raise_for_unsupported_runtime( user_input = """ 2 1 +N test-project """ runner = CliRunner() @@ -2276,6 +2366,7 @@ def test_init_cli_int_must_raise_for_unsupported_dependency( user_input = """ 2 1 +N test-project """ runner = CliRunner() @@ -2346,6 +2437,7 @@ def test_init_cli_generate_hello_world_app_without_default_prompt( # test-project: response to name user_input = """ 1 +N test-project """ @@ -2361,6 +2453,7 @@ def test_init_cli_generate_hello_world_app_without_default_prompt( "test-project", True, {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, + False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -2433,6 +2526,7 @@ def test_init_cli_generate_app_template_provide_via_options( # test-project: response to name user_input = """ 1 +N test-project """ @@ -2448,6 +2542,7 @@ def test_init_cli_generate_app_template_provide_via_options( "test-project", True, {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, + False, ) def does_template_meet_filter_criteria(self): @@ -2512,6 +2607,7 @@ def test_init_cli_generate_app_template_from_local_cli_templates( N 3 2 +N test-project """ @@ -2527,6 +2623,7 @@ def test_init_cli_generate_app_template_from_local_cli_templates( "test-project", True, {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, + False, ) @patch("samcli.local.common.runtime_template.INIT_RUNTIMES") @@ -2597,6 +2694,7 @@ def test_init_cli_generate_app_template_with_custom_runtime( 1 N 2 +N test-project """ @@ -2612,6 +2710,7 @@ def test_init_cli_generate_app_template_with_custom_runtime( "test-project", True, {"project_name": "test-project", "runtime": "provided.al2", "architectures": {"value": ["x86_64"]}}, + False, ) @patch("samcli.commands.init.init_templates.InitTemplates._get_manifest") @@ -2670,6 +2769,7 @@ def test_init_cli_generate_app_template_with_custom_runtime_using_options( # test-project: response to name user_input = """ 1 +N test-project """ args = [ @@ -2689,4 +2789,88 @@ def test_init_cli_generate_app_template_with_custom_runtime_using_options( "test-project", True, {"project_name": "test-project", "runtime": "provided.al2", "architectures": {"value": ["x86_64"]}}, + False, + ) + + @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_tracing_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 +N +1 +test-project + """ + + runner = CliRunner() + result = runner.invoke(init_cmd, ["--tracing"], 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"]}}, + True, ) diff --git a/tests/unit/commands/local/lib/test_provider.py b/tests/unit/commands/local/lib/test_provider.py index b1ceef29b0..49c6482c2f 100644 --- a/tests/unit/commands/local/lib/test_provider.py +++ b/tests/unit/commands/local/lib/test_provider.py @@ -122,6 +122,153 @@ def test_stack_path(self): self.assertEqual(self.expected_stack_path, self.stack.stack_path) +class TestStackEqual(TestCase): + def test_stacks_are_equal(self): + stack1 = Stack( + "stack", + "stackLogicalId", + "/stack", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + stack2 = Stack( + "stack", + "stackLogicalId", + "/stack", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + self.assertTrue(stack1 == stack2) + + def test_stacks_are_not_equal_different_types(self): + stack1 = Stack( + "stack", + "stackLogicalId", + "/stack", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + not_stack = Mock() + self.assertFalse(stack1 == not_stack) + + def test_stacks_are_not_equal_different_parent_stack_path(self): + stack1 = Stack( + "stack1", + "stackLogicalId", + "/stack", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + stack2 = Stack( + "stack2", + "stackLogicalId", + "/stack", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + self.assertFalse(stack1 == stack2) + + def test_stacks_are_not_equal_different_stack_name(self): + stack1 = Stack( + "stack", + "stackLogicalId1", + "/stack", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + stack2 = Stack( + "stack", + "stackLogicalId2", + "/stack", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + self.assertFalse(stack1 == stack2) + + def test_stacks_are_not_equal_different_template_path(self): + stack1 = Stack( + "stack", + "stackLogicalId", + "/stack1", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + stack2 = Stack( + "stack", + "stackLogicalId", + "/stack2", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + self.assertFalse(stack1 == stack2) + + def test_stacks_are_not_equal_different_parameters(self): + stack1 = Stack( + "stack", + "stackLogicalId", + "/stack", + {"key1": "value1"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + stack2 = Stack( + "stack", + "stackLogicalId", + "/stack", + {"key2": "value2"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + self.assertFalse(stack1 == stack2) + + def test_stacks_are_not_equal_different_templates(self): + stack1 = Stack( + "stack", + "stackLogicalId", + "/stack", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId": "stackCustomId"}, + ) + stack2 = Stack( + "stack", + "stackLogicalId", + "/stack", + {"key": "value"}, + {"Resources": {"func2": {"Runtime": "Java"}}}, + {"SamResourceId": "stackCustomId"}, + ) + self.assertFalse(stack1 == stack2) + + def test_stacks_are_not_equal_different_metadata(self): + stack1 = Stack( + "stack", + "stackLogicalId", + "/stack", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId1": "stackCustomId1"}, + ) + stack2 = Stack( + "stack", + "stackLogicalId", + "/stack", + {"key": "value"}, + {"Resources": {"func1": {"Runtime": "Python"}}}, + {"SamResourceId2": "stackCustomId2"}, + ) + self.assertFalse(stack1 == stack2) + + class TestFunction(TestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/unit/commands/samconfig/test_samconfig.py b/tests/unit/commands/samconfig/test_samconfig.py index 93e5104db3..825709593e 100644 --- a/tests/unit/commands/samconfig/test_samconfig.py +++ b/tests/unit/commands/samconfig/test_samconfig.py @@ -80,6 +80,7 @@ def test_init(self, do_cli_mock): "apptemplate", True, '{"key": "value", "key2": "value2"}', + None, ) @patch("samcli.commands.validate.validate.do_cli") diff --git a/tests/unit/lib/init/test_cli_template_modifier.py b/tests/unit/lib/init/test_cli_template_modifier.py new file mode 100644 index 0000000000..f2571d10ab --- /dev/null +++ b/tests/unit/lib/init/test_cli_template_modifier.py @@ -0,0 +1,194 @@ +from unittest import TestCase +from unittest.mock import patch, MagicMock +from yaml.parser import ParserError + +from samcli.lib.init.template_modifiers.cli_template_modifier import TemplateModifier +from samcli.lib.init.template_modifiers.xray_tracing_template_modifier import XRayTracingTemplateModifier + + +class TestTemplateModifier(TestCase): + def setUp(self): + self.location = MagicMock() + self.template_data = [ + "Resources:\n", + " HelloWorldFunction:\n", + " Type: AWS::Serverless::Function\n", + " Properties:\n", + " CodeUri: hello_world/\n", + " Handler: app.lambda_handler\n", + ] + + @patch("samcli.lib.init.template_modifiers.cli_template_modifier.TemplateModifier._get_template") + def test_must_add_new_field_to_template(self, get_template_patch): + get_template_patch.return_value = [ + "Resources:\n", + " HelloWorldFunction:\n", + " Type: AWS::Serverless::Function\n", + " Properties:\n", + " CodeUri: hello_world/\n", + " Handler: app.lambda_handler\n", + ] + + expected_template_data = [ + "# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst\n", + "Globals:\n", + " Function:\n", + " Tracing: Active\n", + "\n", + "Resources:\n", + " HelloWorldFunction:\n", + " Type: AWS::Serverless::Function\n", + " Properties:\n", + " CodeUri: hello_world/\n", + " Handler: app.lambda_handler\n", + ] + + template_modifier = XRayTracingTemplateModifier(self.location) + template_modifier._add_new_field_to_template() + + self.assertEqual(template_modifier.template, expected_template_data) + + @patch("samcli.lib.init.template_modifiers.cli_template_modifier.TemplateModifier._get_template") + def test_must_add_new_function_field_to_template(self, get_template_patch): + get_template_patch.return_value = [ + "# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst\n", + "Globals:\n", + " Api:\n", + " api_field: field_value\n", + "\n", + "Resources:\n", + " HelloWorldFunction:\n", + " Type: AWS::Serverless::Function\n", + " Properties:\n", + " CodeUri: hello_world/\n", + " Handler: app.lambda_handler\n", + ] + + expected_template_data = [ + "# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst\n", + "Globals:\n", + " Api:\n", + " api_field: field_value\n", + " Function:\n", + " Tracing: Active\n", + "\n", + "Resources:\n", + " HelloWorldFunction:\n", + " Type: AWS::Serverless::Function\n", + " Properties:\n", + " CodeUri: hello_world/\n", + " Handler: app.lambda_handler\n", + ] + + template_modifier = XRayTracingTemplateModifier(self.location) + template_modifier._add_new_field_to_template() + + self.assertEqual(template_modifier.template, expected_template_data) + + @patch("samcli.lib.init.template_modifiers.cli_template_modifier.TemplateModifier._get_template") + def test_must_add_new_tracing_field_to_template(self, get_template_patch): + get_template_patch.return_value = [ + "# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst\n", + "Globals:\n", + " Function:\n", + " Timeout: 3\n", + "\n", + "Resources:\n", + " HelloWorldFunction:\n", + " Type: AWS::Serverless::Function\n", + " Properties:\n", + " CodeUri: hello_world/\n", + " Handler: app.lambda_handler\n", + ] + + expected_template_data = [ + "# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst\n", + "Globals:\n", + " Function:\n", + " Timeout: 3\n", + " Tracing: Active\n", + "\n", + "Resources:\n", + " HelloWorldFunction:\n", + " Type: AWS::Serverless::Function\n", + " Properties:\n", + " CodeUri: hello_world/\n", + " Handler: app.lambda_handler\n", + ] + + template_modifier = XRayTracingTemplateModifier(self.location) + template_modifier._add_new_field_to_template() + self.assertEqual(template_modifier.template, expected_template_data) + + @patch("samcli.lib.init.template_modifiers.cli_template_modifier.TemplateModifier._get_template") + def test_must_get_section_position(self, get_template_patch): + get_template_patch.return_value = [ + "# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst\n", + "Globals:\n", + " Function:\n", + " Tracing: Active\n", + "\n", + "Resources:\n", + " HelloWorldFunction:\n", + " Type: AWS::Serverless::Function\n", + " Properties:\n", + " CodeUri: hello_world/\n", + " Handler: app.lambda_handler\n", + ] + + template_modifier = XRayTracingTemplateModifier(self.location) + global_location = template_modifier._section_position("Globals:\n") + function_location = template_modifier._section_position(" Function:\n") + resource_location = template_modifier._section_position("Resources:\n") + + self.assertEqual(global_location, 1) + self.assertEqual(function_location, 2) + self.assertEqual(resource_location, 5) + + @patch("samcli.lib.init.template_modifiers.cli_template_modifier.TemplateModifier._get_template") + def test_must_get_field_position(self, get_template_patch): + get_template_patch.return_value = [ + "Resources:\n", + " HelloWorldFunction:\n", + " Type: AWS::Serverless::Function\n", + " Properties:\n", + " CodeUri: hello_world/\n", + " Handler: app.lambda_handler\n", + ] + + template_modifier = XRayTracingTemplateModifier(self.location) + tracing_location = template_modifier._field_position(0, "Tracing") + + self.assertEqual(tracing_location, -1) + + @patch("samcli.lib.init.template_modifiers.xray_tracing_template_modifier.LOG") + @patch("samcli.lib.init.template_modifiers.cli_template_modifier.parse_yaml_file") + def test_must_fail_sanity_check(self, parse_yaml_file_mock, log_mock): + expected_warning_msg = ( + "Warning: Unable to add Tracing to the project. To learn more about Tracing visit " + "https://docs.aws.amazon.com/serverless-application-model/latest" + "/developerguide/sam-resource-function.html#sam-function-tracing" + ) + template_modifier = XRayTracingTemplateModifier(self.location) + parse_yaml_file_mock.side_effect = ParserError + result = template_modifier._sanity_check() + self.assertFalse(result) + log_mock.warning.assert_called_once_with(expected_warning_msg) + + @patch("samcli.lib.init.template_modifiers.xray_tracing_template_modifier.LOG") + def test_must_log_warning_message(self, log_mock): + expected_warning_msg = ( + "Warning: Unable to add Tracing to the project. To learn more about Tracing visit " + "https://docs.aws.amazon.com/serverless-application-model/latest" + "/developerguide/sam-resource-function.html#sam-function-tracing" + ) + template_modifier = XRayTracingTemplateModifier(self.location) + template_modifier._print_sanity_check_error() + log_mock.warning.assert_called_once_with(expected_warning_msg) + + @patch("samcli.lib.init.template_modifiers.cli_template_modifier.parse_yaml_file") + def test_must_pass_sanity_check(self, parse_yaml_file_mock): + template_modifier = XRayTracingTemplateModifier(self.location) + parse_yaml_file_mock.return_value = {"add: add_value"} + result = template_modifier._sanity_check() + self.assertTrue(result)