From 837a7ab44c8e31298666e754994c83eaf290453e Mon Sep 17 00:00:00 2001 From: Vikranth Srivatsa <51216482+viksrivat@users.noreply.github.com> Date: Tue, 13 Aug 2019 20:48:03 -0700 Subject: [PATCH] feat: Resolve Intrinsics Functions in template (#1333) --- samcli/commands/local/lib/api_provider.py | 1 + samcli/commands/local/lib/cfn_api_provider.py | 1 + .../commands/local/lib/sam_base_provider.py | 67 +--- .../intrinsic_property_resolver.py | 70 +++-- .../intrinsics_symbol_table.py | 27 +- .../local/start_api/test_start_api.py | 285 ++++++++++++------ .../testdata/start_api/cdk-sample-output.yaml | 276 +++++++++++++++++ .../start_api/serverless-sample-output.yaml | 206 +++++++++++++ .../local/lib/test_cfn_api_provider.py | 179 +++++++++++ .../local/lib/test_sam_base_provider.py | 194 +----------- .../test_intrinsic_resolver.py | 6 +- 11 files changed, 942 insertions(+), 370 deletions(-) create mode 100644 tests/integration/testdata/start_api/cdk-sample-output.yaml create mode 100644 tests/integration/testdata/start_api/serverless-sample-output.yaml diff --git a/samcli/commands/local/lib/api_provider.py b/samcli/commands/local/lib/api_provider.py index 20d31039f7..7a95959e6d 100644 --- a/samcli/commands/local/lib/api_provider.py +++ b/samcli/commands/local/lib/api_provider.py @@ -65,6 +65,7 @@ def _extract_api(self, resources): --------- An Api from the parsed template """ + collector = ApiCollector() provider = self.find_api_provider(resources) provider.extract_resources(resources, collector, cwd=self.cwd) diff --git a/samcli/commands/local/lib/cfn_api_provider.py b/samcli/commands/local/lib/cfn_api_provider.py index 4d9ba68e13..e6076d359a 100644 --- a/samcli/commands/local/lib/cfn_api_provider.py +++ b/samcli/commands/local/lib/cfn_api_provider.py @@ -43,6 +43,7 @@ def extract_resources(self, resources, collector, cwd=None): ------- Returns a list of routes """ + for logical_id, resource in resources.items(): resource_type = resource.get(CfnBaseApiProvider.RESOURCE_TYPE) if resource_type == CfnApiProvider.APIGATEWAY_RESTAPI: diff --git a/samcli/commands/local/lib/sam_base_provider.py b/samcli/commands/local/lib/sam_base_provider.py index 6d03e5e6f9..d9f279acc6 100644 --- a/samcli/commands/local/lib/sam_base_provider.py +++ b/samcli/commands/local/lib/sam_base_provider.py @@ -4,11 +4,10 @@ import logging -from samtranslator.intrinsics.resolver import IntrinsicsResolver -from samtranslator.intrinsics.actions import RefAction - -from samcli.lib.samlib.wrapper import SamTranslatorWrapper +from samcli.lib.intrinsic_resolver.intrinsic_property_resolver import IntrinsicResolver +from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable from samcli.lib.samlib.resource_metadata_normalizer import ResourceMetadataNormalizer +from samcli.lib.samlib.wrapper import SamTranslatorWrapper LOG = logging.getLogger(__name__) @@ -18,24 +17,6 @@ class SamBaseProvider(object): Base class for SAM Template providers """ - # There is not much benefit in infering real values for these parameters in local development context. These values - # are usually representative of an AWS environment and stack, but in local development scenario they don't make - # sense. If customers choose to, they can always override this value through the CLI interface. - DEFAULT_PSEUDO_PARAM_VALUES = { - "AWS::AccountId": "123456789012", - "AWS::Partition": "aws", - - "AWS::Region": "us-east-1", - - "AWS::StackName": "local", - "AWS::StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/" - "local/51af3dc0-da77-11e4-872e-1234567db123", - "AWS::URLSuffix": "localhost" - } - - # Only Ref is supported when resolving template parameters - _SUPPORTED_INTRINSICS = [RefAction] - @staticmethod def get_template(template_dict, parameter_overrides=None): """ @@ -59,37 +40,19 @@ def get_template(template_dict, parameter_overrides=None): template_dict = template_dict or {} if template_dict: template_dict = SamTranslatorWrapper(template_dict).run_plugins() - - template_dict = SamBaseProvider._resolve_parameters(template_dict, parameter_overrides) ResourceMetadataNormalizer.normalize(template_dict) - return template_dict - - @staticmethod - def _resolve_parameters(template_dict, parameter_overrides): - """ - In the given template, apply parameter values to resolve intrinsic functions - - Parameters - ---------- - template_dict : dict - SAM Template + logical_id_translator = SamBaseProvider._get_parameter_values( + template_dict, parameter_overrides + ) + resolver = IntrinsicResolver( + template=template_dict, + symbol_resolver=IntrinsicsSymbolTable( + logical_id_translator=logical_id_translator, template=template_dict + ), + ) + template_dict = resolver.resolve_template(ignore_errors=True) - parameter_overrides : dict - Values for template parameters provided by user - - Returns - ------- - dict - Resolved SAM template - """ - - parameter_values = SamBaseProvider._get_parameter_values(template_dict, parameter_overrides) - - supported_intrinsics = {action.intrinsic_name: action() for action in SamBaseProvider._SUPPORTED_INTRINSICS} - - # Intrinsics resolver will mutate the original template - return IntrinsicsResolver(parameters=parameter_values, supported_intrinsics=supported_intrinsics) \ - .resolve_parameter_refs(template_dict) + return template_dict @staticmethod def _get_parameter_values(template_dict, parameter_overrides): @@ -116,7 +79,7 @@ def _get_parameter_values(template_dict, parameter_overrides): # NOTE: Ordering of following statements is important. It makes sure that any user-supplied values # override the defaults parameter_values = {} - parameter_values.update(SamBaseProvider.DEFAULT_PSEUDO_PARAM_VALUES) + parameter_values.update(IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES) parameter_values.update(default_values) parameter_values.update(parameter_overrides or {}) diff --git a/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py index fea3985db5..db30bdd3d5 100644 --- a/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py +++ b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py @@ -6,6 +6,7 @@ import base64 import re +from collections import OrderedDict from six import string_types @@ -82,6 +83,7 @@ def __init__(self, template, symbol_resolver): self._mapping = None self._parameters = None self._conditions = None + self._outputs = None self.init_template(template) self._symbol_resolver = symbol_resolver @@ -95,6 +97,7 @@ def init_template(self, template): self._mapping = self._template.get("Mappings", {}) self._parameters = self._template.get("Parameters", {}) self._conditions = self._template.get("Conditions", {}) + self._outputs = self._template.get("Outputs", {}) def default_intrinsic_function_map(self): """ @@ -217,52 +220,65 @@ def intrinsic_property_resolver(self, intrinsic, parent_function="template"): # resolve each of it's sub properties. sanitized_dict = {} for key, val in intrinsic.items(): - sanitized_key = self.intrinsic_property_resolver( - key, parent_function=parent_function - ) - sanitized_val = self.intrinsic_property_resolver( - val, parent_function=parent_function - ) - verify_intrinsic_type_str( - sanitized_key, - message="The keys of the dictionary {} in {} must all resolve to a string".format( - sanitized_key, parent_function - ), - ) + sanitized_key = self.intrinsic_property_resolver(key, parent_function=parent_function) + sanitized_val = self.intrinsic_property_resolver(val, parent_function=parent_function) + verify_intrinsic_type_str(sanitized_key, + message="The keys of the dictionary {} in {} must all resolve to a string".format( + sanitized_key, parent_function + )) sanitized_dict[sanitized_key] = sanitized_val return sanitized_dict def resolve_template(self, ignore_errors=False): """ - This will parse through every entry in a CloudFormation template and resolve them based on the symbol_resolver. + This resolves all the attributes of the CloudFormation dictionary Resources, Outputs, Mappings, Parameters, + Conditions. + + Return + ------- + Return a processed template + """ + processed_template = OrderedDict() + processed_template["Resources"] = self.resolve_attribute(self._resources, ignore_errors) + processed_template["Outputs"] = self.resolve_attribute(self._outputs, ignore_errors) + processed_template["Mappings"] = self.resolve_attribute(self._resources, ignore_errors) + processed_template["Parameters"] = self.resolve_attribute(self._resources, ignore_errors) + processed_template["Conditions"] = self.resolve_attribute(self._resources, ignore_errors) + return processed_template + + def resolve_attribute(self, cloud_formation_property, ignore_errors=False): + """ + This will parse through every entry in a CloudFormation root key and resolve them based on the symbol_resolver. Customers can optionally ignore resource errors and default to whatever the resource provides. Parameters ----------- + cloud_formation_property: dict + A high Level dictionary containg either the Mappings, Resources, Outputs, or Parameters Dictionary ignore_errors: bool An option to ignore errors that are InvalidIntrinsicException and InvalidSymbolException Return ------- A resolved template with all references possible simplified """ - processed_template = {} - for key, val in self._resources.items(): + processed_dict = OrderedDict() + for key, val in cloud_formation_property.items(): processed_key = self._symbol_resolver.get_translation(key) or key try: processed_resource = self.intrinsic_property_resolver(val) - processed_template[processed_key] = processed_resource + processed_dict[processed_key] = processed_resource except (InvalidIntrinsicException, InvalidSymbolException) as e: resource_type = val.get("Type", "") if ignore_errors: LOG.error( "Unable to process properties of %s.%s", key, resource_type ) - processed_template[key] = val + processed_dict[key] = val else: raise InvalidIntrinsicException( "Exception with property of {}.{}".format(key, resource_type) + ": " + str(e.args) ) - return processed_template + return processed_dict def handle_fn_join(self, intrinsic_value): """ @@ -280,21 +296,15 @@ def handle_fn_join(self, intrinsic_value): ------- A string with the resolved attributes """ - arguments = self.intrinsic_property_resolver( - intrinsic_value, parent_function=IntrinsicResolver.FN_JOIN - ) + arguments = self.intrinsic_property_resolver(intrinsic_value, parent_function=IntrinsicResolver.FN_JOIN) verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_JOIN) delimiter = arguments[0] - verify_intrinsic_type_str( - delimiter, IntrinsicResolver.FN_JOIN, position_in_list="first" - ) + verify_intrinsic_type_str(delimiter, IntrinsicResolver.FN_JOIN, position_in_list="first") - value_list = self.intrinsic_property_resolver( - arguments[1], parent_function=IntrinsicResolver.FN_JOIN - ) + value_list = self.intrinsic_property_resolver(arguments[1], parent_function=IntrinsicResolver.FN_JOIN) verify_intrinsic_type_list( value_list, @@ -530,7 +540,7 @@ def handle_fn_get_azs(self, intrinsic_value): verify_intrinsic_type_str(intrinsic_value, IntrinsicResolver.FN_GET_AZS) if intrinsic_value == "": - intrinsic_value = self._symbol_resolver.DEFAULT_REGION + intrinsic_value = self._symbol_resolver.handle_pseudo_region() if intrinsic_value not in self._symbol_resolver.REGIONS: raise InvalidIntrinsicException( @@ -961,7 +971,6 @@ def handle_fn_or(self, intrinsic_value): intrinsic_value, parent_function=IntrinsicResolver.FN_OR ) verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_OR) - for i, argument in enumerate(arguments): if isinstance(argument, dict) and "Condition" in argument: condition_name = argument.get("Condition") @@ -978,7 +987,6 @@ def handle_fn_or(self, intrinsic_value): condition, parent_function=IntrinsicResolver.FN_OR ) verify_intrinsic_type_bool(condition_evaluated, IntrinsicResolver.FN_OR) - if condition_evaluated: return True else: @@ -986,8 +994,6 @@ def handle_fn_or(self, intrinsic_value): argument, parent_function=IntrinsicResolver.FN_OR ) verify_intrinsic_type_bool(condition, IntrinsicResolver.FN_OR) - if condition: return True - return False diff --git a/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py b/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py index 155955964b..081cf3bb17 100644 --- a/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py +++ b/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py @@ -5,7 +5,6 @@ from six import string_types -from samcli.commands.local.lib.sam_base_provider import SamBaseProvider from samcli.lib.intrinsic_resolver.intrinsic_property_resolver import IntrinsicResolver from samcli.lib.intrinsic_resolver.invalid_intrinsic_exception import ( InvalidSymbolException, @@ -32,7 +31,19 @@ class IntrinsicsSymbolTable(object): AWS_NOVALUE, ] - DEFAULT_REGION = "us-east-1" + # There is not much benefit in infering real values for these parameters in local development context. These values + # are usually representative of an AWS environment and stack, but in local development scenario they don't make + # sense. If customers choose to, they can always override this value through the CLI interface. + DEFAULT_PSEUDO_PARAM_VALUES = { + "AWS::AccountId": "123456789012", + "AWS::Partition": "aws", + "AWS::Region": "us-east-1", + "AWS::StackName": "local", + "AWS::StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/" + "local/51af3dc0-da77-11e4-872e-1234567db123", + "AWS::URLSuffix": "localhost", + } + REGIONS = { "us-east-1": [ "us-east-1a", @@ -207,11 +218,11 @@ def resolve_symbols(self, logical_id, resource_attribute, ignore_errors=False): translated = self._parameters.get(logical_id, {}).get("Default") if translated: return translated - # Handle Default Property Type Resolution resource_type = self._resources.get(logical_id, {}).get( IntrinsicsSymbolTable.CFN_RESOURCE_TYPE ) + resolver = ( self.default_type_resolver.get(resource_type, {}).get(resource_attribute) if resource_type @@ -290,7 +301,7 @@ def get_translation(self, logical_id, resource_attributes=IntrinsicResolver.REF) """ logical_id_item = self.logical_id_translator.get(logical_id, {}) - if isinstance(logical_id_item, string_types): + if any(isinstance(logical_id_item, object_type) for object_type in [string_types, list, bool, int]): if ( resource_attributes != IntrinsicResolver.REF and resource_attributes != "" ): @@ -322,7 +333,7 @@ def handle_pseudo_account_id(): ------- A pseudo account id """ - return SamBaseProvider.DEFAULT_PSEUDO_PARAM_VALUES.get( + return IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES.get( IntrinsicsSymbolTable.AWS_ACCOUNT_ID ) @@ -338,7 +349,7 @@ def handle_pseudo_region(self): """ return ( self.logical_id_translator.get(IntrinsicsSymbolTable.AWS_REGION) or os.getenv("AWS_REGION") or - SamBaseProvider.DEFAULT_PSEUDO_PARAM_VALUES.get( + IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES.get( IntrinsicsSymbolTable.AWS_REGION ) ) @@ -385,7 +396,7 @@ def handle_pseudo_stack_id(): ------- A randomized string """ - return SamBaseProvider.DEFAULT_PSEUDO_PARAM_VALUES.get( + return IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES.get( IntrinsicsSymbolTable.AWS_STACK_ID ) @@ -400,7 +411,7 @@ def handle_pseudo_stack_name(): ------- A randomized string """ - return SamBaseProvider.DEFAULT_PSEUDO_PARAM_VALUES.get( + return IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES.get( IntrinsicsSymbolTable.AWS_STACK_NAME ) diff --git a/tests/integration/local/start_api/test_start_api.py b/tests/integration/local/start_api/test_start_api.py index 63763e4282..724eedcb43 100644 --- a/tests/integration/local/start_api/test_start_api.py +++ b/tests/integration/local/start_api/test_start_api.py @@ -10,6 +10,7 @@ class TestParallelRequests(StartApiIntegBaseClass): """ Test Class centered around sending parallel requests to the service `sam local start-api` """ + # This is here so the setUpClass doesn't fail. Set to this something else once the class is implemented template_path = "/testdata/start_api/template.yaml" @@ -25,8 +26,10 @@ def test_same_endpoint(self): start_time = time() thread_pool = ThreadPoolExecutor(number_of_requests) - futures = [thread_pool.submit(requests.get, self.url + "/sleepfortenseconds/function1") - for _ in range(0, number_of_requests)] + futures = [ + thread_pool.submit(requests.get, self.url + "/sleepfortenseconds/function1") + for _ in range(0, number_of_requests) + ] results = [r.result() for r in as_completed(futures)] end_time = time() @@ -37,7 +40,9 @@ def test_same_endpoint(self): for result in results: self.assertEquals(result.status_code, 200) - self.assertEquals(result.json(), {"message": "HelloWorld! I just slept and waking up."}) + self.assertEquals( + result.json(), {"message": "HelloWorld! I just slept and waking up."} + ) def test_different_endpoints(self): """ @@ -48,10 +53,18 @@ def test_different_endpoints(self): start_time = time() thread_pool = ThreadPoolExecutor(10) - test_url_paths = ["/sleepfortenseconds/function0", "/sleepfortenseconds/function1"] - - futures = [thread_pool.submit(requests.get, self.url + test_url_paths[function_num % len(test_url_paths)]) - for function_num in range(0, number_of_requests)] + test_url_paths = [ + "/sleepfortenseconds/function0", + "/sleepfortenseconds/function1", + ] + + futures = [ + thread_pool.submit( + requests.get, + self.url + test_url_paths[function_num % len(test_url_paths)], + ) + for function_num in range(0, number_of_requests) + ] results = [r.result() for r in as_completed(futures)] end_time = time() @@ -62,13 +75,16 @@ def test_different_endpoints(self): for result in results: self.assertEquals(result.status_code, 200) - self.assertEquals(result.json(), {"message": "HelloWorld! I just slept and waking up."}) + self.assertEquals( + result.json(), {"message": "HelloWorld! I just slept and waking up."} + ) class TestServiceErrorResponses(StartApiIntegBaseClass): """ Test Class centered around the Error Responses the Service can return for a given api """ + # This is here so the setUpClass doesn't fail. Set to this something else once the class is implemented. template_path = "/testdata/start_api/template.yaml" @@ -101,6 +117,7 @@ class TestService(StartApiIntegBaseClass): """ Testing general requirements around the Service that powers `sam local start-api` """ + template_path = "/testdata/start_api/template.yaml" def setUp(self): @@ -113,7 +130,7 @@ def test_calling_proxy_endpoint(self): response = requests.get(self.url + "/proxypath/this/is/some/path") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_get_call_with_path_setup_with_any_implicit_api(self): """ @@ -122,7 +139,7 @@ def test_get_call_with_path_setup_with_any_implicit_api(self): response = requests.get(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_post_call_with_path_setup_with_any_implicit_api(self): """ @@ -131,7 +148,7 @@ def test_post_call_with_path_setup_with_any_implicit_api(self): response = requests.post(self.url + "/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_put_call_with_path_setup_with_any_implicit_api(self): """ @@ -140,7 +157,7 @@ def test_put_call_with_path_setup_with_any_implicit_api(self): response = requests.put(self.url + "/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_head_call_with_path_setup_with_any_implicit_api(self): """ @@ -157,7 +174,7 @@ def test_delete_call_with_path_setup_with_any_implicit_api(self): response = requests.delete(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_options_call_with_path_setup_with_any_implicit_api(self): """ @@ -174,7 +191,7 @@ def test_patch_call_with_path_setup_with_any_implicit_api(self): response = requests.patch(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) class TestStartApiWithSwaggerApis(StartApiIntegBaseClass): @@ -191,7 +208,7 @@ def test_get_call_with_path_setup_with_any_swagger(self): response = requests.get(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_post_call_with_path_setup_with_any_swagger(self): """ @@ -200,7 +217,7 @@ def test_post_call_with_path_setup_with_any_swagger(self): response = requests.post(self.url + "/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_put_call_with_path_setup_with_any_swagger(self): """ @@ -209,7 +226,7 @@ def test_put_call_with_path_setup_with_any_swagger(self): response = requests.put(self.url + "/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_head_call_with_path_setup_with_any_swagger(self): """ @@ -226,7 +243,7 @@ def test_delete_call_with_path_setup_with_any_swagger(self): response = requests.delete(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_options_call_with_path_setup_with_any_swagger(self): """ @@ -243,34 +260,38 @@ def test_patch_call_with_path_setup_with_any_swagger(self): response = requests.patch(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_function_not_defined_in_template(self): response = requests.get(self.url + "/nofunctionfound") self.assertEquals(response.status_code, 502) - self.assertEquals(response.json(), {"message": "No function defined for resource method"}) + self.assertEquals( + response.json(), {"message": "No function defined for resource method"} + ) def test_function_with_no_api_event_is_reachable(self): response = requests.get(self.url + "/functionwithnoapievent") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_lambda_function_resource_is_reachable(self): response = requests.get(self.url + "/nonserverlessfunction") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_binary_request(self): """ This tests that the service can accept and invoke a lambda when given binary data in a request """ input_data = self.get_binary_data(self.binary_data_file) - response = requests.post(self.url + '/echobase64eventbody', - headers={"Content-Type": "image/gif"}, - data=input_data) + response = requests.post( + self.url + "/echobase64eventbody", + headers={"Content-Type": "image/gif"}, + data=input_data, + ) self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -282,7 +303,7 @@ def test_binary_response(self): """ expected = self.get_binary_data(self.binary_data_file) - response = requests.get(self.url + '/base64response') + response = requests.get(self.url + "/base64response") self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -303,7 +324,7 @@ def test_get_call_with_path_setup_with_any_swagger(self): response = requests.get(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_post_call_with_path_setup_with_any_swagger(self): """ @@ -312,7 +333,7 @@ def test_post_call_with_path_setup_with_any_swagger(self): response = requests.post(self.url + "/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_put_call_with_path_setup_with_any_swagger(self): """ @@ -321,7 +342,7 @@ def test_put_call_with_path_setup_with_any_swagger(self): response = requests.put(self.url + "/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_head_call_with_path_setup_with_any_swagger(self): """ @@ -338,7 +359,7 @@ def test_delete_call_with_path_setup_with_any_swagger(self): response = requests.delete(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_options_call_with_path_setup_with_any_swagger(self): """ @@ -355,28 +376,32 @@ def test_patch_call_with_path_setup_with_any_swagger(self): response = requests.patch(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_function_not_defined_in_template(self): response = requests.get(self.url + "/nofunctionfound") self.assertEquals(response.status_code, 502) - self.assertEquals(response.json(), {"message": "No function defined for resource method"}) + self.assertEquals( + response.json(), {"message": "No function defined for resource method"} + ) def test_lambda_function_resource_is_reachable(self): response = requests.get(self.url + "/nonserverlessfunction") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_binary_request(self): """ This tests that the service can accept and invoke a lambda when given binary data in a request """ input_data = self.get_binary_data(self.binary_data_file) - response = requests.post(self.url + '/echobase64eventbody', - headers={"Content-Type": "image/gif"}, - data=input_data) + response = requests.post( + self.url + "/echobase64eventbody", + headers={"Content-Type": "image/gif"}, + data=input_data, + ) self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -388,7 +413,7 @@ def test_binary_response(self): """ expected = self.get_binary_data(self.binary_data_file) - response = requests.get(self.url + '/base64response') + response = requests.get(self.url + "/base64response") self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -399,6 +424,7 @@ class TestServiceResponses(StartApiIntegBaseClass): """ Test Class centered around the different responses that can happen in Lambda and pass through start-api """ + template_path = "/testdata/start_api/template.yaml" binary_data_file = "testdata/start_api/binarydata.gif" @@ -410,14 +436,16 @@ def test_multiple_headers_response(self): self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "text/plain") - self.assertEquals(response.headers.get("MyCustomHeader"), 'Value1, Value2') + self.assertEquals(response.headers.get("MyCustomHeader"), "Value1, Value2") def test_multiple_headers_overrides_headers_response(self): response = requests.get(self.url + "/multipleheadersoverridesheaders") self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "text/plain") - self.assertEquals(response.headers.get("MyCustomHeader"), 'Value1, Value2, Custom') + self.assertEquals( + response.headers.get("MyCustomHeader"), "Value1, Value2, Custom" + ) def test_binary_response(self): """ @@ -425,7 +453,7 @@ def test_binary_response(self): """ expected = self.get_binary_data(self.binary_data_file) - response = requests.get(self.url + '/base64response') + response = requests.get(self.url + "/base64response") self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -438,7 +466,7 @@ def test_default_header_content_type(self): response = requests.get(self.url + "/onlysetstatuscode") self.assertEquals(response.status_code, 200) - self.assertEquals(response.content.decode('utf-8'), "no data") + self.assertEquals(response.content.decode("utf-8"), "no data") self.assertEquals(response.headers.get("Content-Type"), "application/json") def test_default_status_code(self): @@ -449,7 +477,7 @@ def test_default_status_code(self): response = requests.get(self.url + "/onlysetbody") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_string_status_code(self): """ @@ -466,31 +494,32 @@ def test_default_body(self): response = requests.get(self.url + "/onlysetstatuscode") self.assertEquals(response.status_code, 200) - self.assertEquals(response.content.decode('utf-8'), "no data") + self.assertEquals(response.content.decode("utf-8"), "no data") def test_function_writing_to_stdout(self): response = requests.get(self.url + "/writetostdout") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_function_writing_to_stderr(self): response = requests.get(self.url + "/writetostderr") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_integer_body(self): response = requests.get(self.url + "/echo_integer_body") self.assertEquals(response.status_code, 200) - self.assertEquals(response.content.decode('utf-8'), '42') + self.assertEquals(response.content.decode("utf-8"), "42") class TestServiceRequests(StartApiIntegBaseClass): """ Test Class centered around the different requests that can happen """ + template_path = "/testdata/start_api/template.yaml" binary_data_file = "testdata/start_api/binarydata.gif" @@ -502,9 +531,11 @@ def test_binary_request(self): This tests that the service can accept and invoke a lambda when given binary data in a request """ input_data = self.get_binary_data(self.binary_data_file) - response = requests.post(self.url + '/echobase64eventbody', - headers={"Content-Type": "image/gif"}, - data=input_data) + response = requests.post( + self.url + "/echobase64eventbody", + headers={"Content-Type": "image/gif"}, + data=input_data, + ) self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -514,15 +545,20 @@ def test_request_with_form_data(self): """ Form-encoded data should be put into the Event to Lambda """ - response = requests.post(self.url + "/echoeventbody", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data='key=value') + response = requests.post( + self.url + "/echoeventbody", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data="key=value", + ) self.assertEquals(response.status_code, 200) response_data = response.json() - self.assertEquals(response_data.get("headers").get("Content-Type"), "application/x-www-form-urlencoded") + self.assertEquals( + response_data.get("headers").get("Content-Type"), + "application/x-www-form-urlencoded", + ) self.assertEquals(response_data.get("body"), "key=value") def test_request_to_an_endpoint_with_two_different_handlers(self): @@ -532,46 +568,55 @@ def test_request_to_an_endpoint_with_two_different_handlers(self): response_data = response.json() - self.assertEquals(response_data.get("handler"), 'echo_event_handler_2') + self.assertEquals(response_data.get("handler"), "echo_event_handler_2") def test_request_with_multi_value_headers(self): - response = requests.get(self.url + "/echoeventbody", - headers={"Content-Type": "application/x-www-form-urlencoded, image/gif"}) + response = requests.get( + self.url + "/echoeventbody", + headers={"Content-Type": "application/x-www-form-urlencoded, image/gif"}, + ) self.assertEquals(response.status_code, 200) response_data = response.json() - self.assertEquals(response_data.get("multiValueHeaders").get("Content-Type"), - ["application/x-www-form-urlencoded, image/gif"]) - self.assertEquals(response_data.get("headers").get("Content-Type"), - "application/x-www-form-urlencoded, image/gif") + self.assertEquals( + response_data.get("multiValueHeaders").get("Content-Type"), + ["application/x-www-form-urlencoded, image/gif"], + ) + self.assertEquals( + response_data.get("headers").get("Content-Type"), + "application/x-www-form-urlencoded, image/gif", + ) def test_request_with_query_params(self): """ Query params given should be put into the Event to Lambda """ - response = requests.get(self.url + "/id/4", - params={"key": "value"}) + response = requests.get(self.url + "/id/4", params={"key": "value"}) self.assertEquals(response.status_code, 200) response_data = response.json() self.assertEquals(response_data.get("queryStringParameters"), {"key": "value"}) - self.assertEquals(response_data.get("multiValueQueryStringParameters"), {"key": ["value"]}) + self.assertEquals( + response_data.get("multiValueQueryStringParameters"), {"key": ["value"]} + ) def test_request_with_list_of_query_params(self): """ Query params given should be put into the Event to Lambda """ - response = requests.get(self.url + "/id/4", - params={"key": ["value", "value2"]}) + response = requests.get(self.url + "/id/4", params={"key": ["value", "value2"]}) self.assertEquals(response.status_code, 200) response_data = response.json() self.assertEquals(response_data.get("queryStringParameters"), {"key": "value2"}) - self.assertEquals(response_data.get("multiValueQueryStringParameters"), {"key": ["value", "value2"]}) + self.assertEquals( + response_data.get("multiValueQueryStringParameters"), + {"key": ["value", "value2"]}, + ) def test_request_with_path_params(self): """ @@ -595,7 +640,9 @@ def test_request_with_many_path_params(self): response_data = response.json() - self.assertEquals(response_data.get("pathParameters"), {"id": "4", "user": "jacob"}) + self.assertEquals( + response_data.get("pathParameters"), {"id": "4", "user": "jacob"} + ) def test_forward_headers_are_added_to_event(self): """ @@ -606,15 +653,22 @@ def test_forward_headers_are_added_to_event(self): response_data = response.json() self.assertEquals(response_data.get("headers").get("X-Forwarded-Proto"), "http") - self.assertEquals(response_data.get("multiValueHeaders").get("X-Forwarded-Proto"), ["http"]) - self.assertEquals(response_data.get("headers").get("X-Forwarded-Port"), self.port) - self.assertEquals(response_data.get("multiValueHeaders").get("X-Forwarded-Port"), [self.port]) + self.assertEquals( + response_data.get("multiValueHeaders").get("X-Forwarded-Proto"), ["http"] + ) + self.assertEquals( + response_data.get("headers").get("X-Forwarded-Port"), self.port + ) + self.assertEquals( + response_data.get("multiValueHeaders").get("X-Forwarded-Port"), [self.port] + ) class TestStartApiWithStage(StartApiIntegBaseClass): """ Test Class centered around the different responses that can happen in Lambda and pass through start-api """ + template_path = "/testdata/start_api/template.yaml" def setUp(self): @@ -636,13 +690,14 @@ def test_global_stage_variables(self): response_data = response.json() - self.assertEquals(response_data.get("stageVariables"), {'VarName': 'varValue'}) + self.assertEquals(response_data.get("stageVariables"), {"VarName": "varValue"}) class TestStartApiWithStageAndSwagger(StartApiIntegBaseClass): """ Test Class centered around the different responses that can happen in Lambda and pass through start-api """ + template_path = "/testdata/start_api/swagger-template.yaml" def setUp(self): @@ -662,13 +717,14 @@ def test_swagger_stage_variable(self): self.assertEquals(response.status_code, 200) response_data = response.json() - self.assertEquals(response_data.get("stageVariables"), {'VarName': 'varValue'}) + self.assertEquals(response_data.get("stageVariables"), {"VarName": "varValue"}) class TestServiceCorsSwaggerRequests(StartApiIntegBaseClass): """ Test to check that the correct headers are being added with Cors with swagger code """ + template_path = "/testdata/start_api/swagger-template.yaml" binary_data_file = "testdata/start_api/binarydata.gif" @@ -679,20 +735,26 @@ def test_cors_swagger_options(self): """ This tests that the Cors are added to option requests in the swagger template """ - response = requests.options(self.url + '/echobase64eventbody') + response = requests.options(self.url + "/echobase64eventbody") self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*") - self.assertEquals(response.headers.get("Access-Control-Allow-Headers"), "origin, x-requested-with") - self.assertEquals(response.headers.get("Access-Control-Allow-Methods"), "GET,OPTIONS") - self.assertEquals(response.headers.get("Access-Control-Max-Age"), '510') + self.assertEquals( + response.headers.get("Access-Control-Allow-Headers"), + "origin, x-requested-with", + ) + self.assertEquals( + response.headers.get("Access-Control-Allow-Methods"), "GET,OPTIONS" + ) + self.assertEquals(response.headers.get("Access-Control-Max-Age"), "510") class TestServiceCorsGlobalRequests(StartApiIntegBaseClass): """ Test to check that the correct headers are being added with Cors with the global property """ + template_path = "/testdata/start_api/template.yaml" def setUp(self): @@ -702,13 +764,15 @@ def test_cors_global(self): """ This tests that the Cors are added to options requests when the global property is set """ - response = requests.options(self.url + '/echobase64eventbody') + response = requests.options(self.url + "/echobase64eventbody") self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*") self.assertEquals(response.headers.get("Access-Control-Allow-Headers"), None) - self.assertEquals(response.headers.get("Access-Control-Allow-Methods"), - ','.join(sorted(Route.ANY_HTTP_METHODS))) + self.assertEquals( + response.headers.get("Access-Control-Allow-Methods"), + ",".join(sorted(Route.ANY_HTTP_METHODS)), + ) self.assertEquals(response.headers.get("Access-Control-Max-Age"), None) def test_cors_global_get(self): @@ -718,7 +782,7 @@ def test_cors_global_get(self): response = requests.get(self.url + "/onlysetstatuscode") self.assertEquals(response.status_code, 200) - self.assertEquals(response.content.decode('utf-8'), "no data") + self.assertEquals(response.content.decode("utf-8"), "no data") self.assertEquals(response.headers.get("Content-Type"), "application/json") self.assertEquals(response.headers.get("Access-Control-Allow-Origin"), None) self.assertEquals(response.headers.get("Access-Control-Allow-Headers"), None) @@ -730,6 +794,7 @@ class TestStartApiWithCloudFormationStage(StartApiIntegBaseClass): """ Test Class centered around the different responses that can happen in Lambda and pass through start-api """ + template_path = "/testdata/start_api/swagger-rest-api-template.yaml" def setUp(self): @@ -750,7 +815,7 @@ def test_global_stage_variables(self): response_data = response.json() - self.assertEquals(response_data.get("stageVariables"), {'Stack': 'Dev'}) + self.assertEquals(response_data.get("stageVariables"), {"Stack": "Dev"}) class TestStartApiWithMethodsAndResources(StartApiIntegBaseClass): @@ -767,7 +832,7 @@ def test_get_call_with_path_setup_with_any_swagger(self): response = requests.get(self.url + "/root/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_post_call_with_path_setup_with_any_swagger(self): """ @@ -776,7 +841,7 @@ def test_post_call_with_path_setup_with_any_swagger(self): response = requests.post(self.url + "/root/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_put_call_with_path_setup_with_any_swagger(self): """ @@ -785,7 +850,7 @@ def test_put_call_with_path_setup_with_any_swagger(self): response = requests.put(self.url + "/root/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_head_call_with_path_setup_with_any_swagger(self): """ @@ -802,7 +867,7 @@ def test_delete_call_with_path_setup_with_any_swagger(self): response = requests.delete(self.url + "/root/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_options_call_with_path_setup_with_any_swagger(self): """ @@ -819,28 +884,32 @@ def test_patch_call_with_path_setup_with_any_swagger(self): response = requests.patch(self.url + "/root/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_function_not_defined_in_template(self): response = requests.get(self.url + "/root/nofunctionfound") self.assertEquals(response.status_code, 502) - self.assertEquals(response.json(), {"message": "No function defined for resource method"}) + self.assertEquals( + response.json(), {"message": "No function defined for resource method"} + ) def test_lambda_function_resource_is_reachable(self): response = requests.get(self.url + "/root/nonserverlessfunction") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_binary_request(self): """ This tests that the service can accept and invoke a lambda when given binary data in a request """ input_data = self.get_binary_data(self.binary_data_file) - response = requests.post(self.url + '/root/echobase64eventbody', - headers={"Content-Type": "image/gif"}, - data=input_data) + response = requests.post( + self.url + "/root/echobase64eventbody", + headers={"Content-Type": "image/gif"}, + data=input_data, + ) self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -852,7 +921,7 @@ def test_binary_response(self): """ expected = self.get_binary_data(self.binary_data_file) - response = requests.get(self.url + '/root/base64response') + response = requests.get(self.url + "/root/base64response") self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -864,5 +933,37 @@ def test_proxy_response(self): """ response = requests.get(self.url + "/root/v1/test") + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + +class TestCDKApiGateway(StartApiIntegBaseClass): + template_path = "/testdata/start_api/cdk-sample-output.yaml" + + def setUp(self): + self.url = "http://127.0.0.1:{}".format(self.port) + + def test_get_with_cdk(self): + """ + Get Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.get(self.url + "/hello-world") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {'hello': 'world'}) + + +class TestServerlessApiGateway(StartApiIntegBaseClass): + template_path = "/testdata/start_api/serverless-sample-output.yaml" + + def setUp(self): + self.url = "http://127.0.0.1:{}".format(self.port) + + def test_get_with_serverless(self): + """ + Get Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.get(self.url + "/hello-world") + self.assertEquals(response.status_code, 200) self.assertEquals(response.json(), {'hello': 'world'}) diff --git a/tests/integration/testdata/start_api/cdk-sample-output.yaml b/tests/integration/testdata/start_api/cdk-sample-output.yaml new file mode 100644 index 0000000000..203ed94906 --- /dev/null +++ b/tests/integration/testdata/start_api/cdk-sample-output.yaml @@ -0,0 +1,276 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Resources: + HelloHandlerServiceRole11EF7C63: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Version: "2012-10-17" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Metadata: + aws:cdk:path: CdkWorkshopStack/HelloHandler/ServiceRole/Resource + + HelloHandler2E4FBA4D: + Type: AWS::Lambda::Function + Properties: + Code: '.' + Handler: main.handler + Runtime: python3.6 + + HelloHandlerApiPermissionANYAC4E141E: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: HelloHandler2E4FBA4D + Principal: apigateway.amazonaws.com + SourceArn: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":execute-api:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - ":" + - Ref: EndpointEEF1FD8F + - / + - Ref: EndpointDeploymentStageprodB78BEEA0 + - /*/ + Metadata: + aws:cdk:path: CdkWorkshopStack/HelloHandler/ApiPermission.ANY.. + HelloHandlerApiPermissionTestANYDDD56D72: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: HelloHandler2E4FBA4D + Principal: apigateway.amazonaws.com + SourceArn: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":execute-api:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - ":" + - Ref: EndpointEEF1FD8F + - /test-invoke-stage/*/ + Metadata: + aws:cdk:path: CdkWorkshopStack/HelloHandler/ApiPermission.Test.ANY.. + HelloHandlerApiPermissionANYproxy90E90CD6: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: HelloHandler2E4FBA4D + Principal: apigateway.amazonaws.com + SourceArn: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":execute-api:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - ":" + - Ref: EndpointEEF1FD8F + - / + - Ref: EndpointDeploymentStageprodB78BEEA0 + - /*/{proxy+} + Metadata: + aws:cdk:path: CdkWorkshopStack/HelloHandler/ApiPermission.ANY..{proxy+} + HelloHandlerApiPermissionTestANYproxy9803526C: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: HelloHandler2E4FBA4D + Principal: apigateway.amazonaws.com + SourceArn: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":execute-api:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - ":" + - Ref: EndpointEEF1FD8F + - /test-invoke-stage/*/{proxy+} + Metadata: + aws:cdk:path: CdkWorkshopStack/HelloHandler/ApiPermission.Test.ANY..{proxy+} + + EndpointEEF1FD8F: + Type: AWS::ApiGateway::RestApi + Properties: + Name: Endpoint + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/Resource + + EndpointDeployment318525DA37c0e38727e25b4317827bf43e918fbf: + Type: AWS::ApiGateway::Deployment + Properties: + RestApiId: + Ref: EndpointEEF1FD8F + Description: Automatically created by the RestApi construct + DependsOn: + - Endpointproxy39E2174E + - EndpointANY485C938B + - EndpointproxyANYC09721C5 + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/Deployment/Resource + + + EndpointDeploymentStageprodB78BEEA0: + Type: AWS::ApiGateway::Stage + Properties: + RestApiId: + Ref: EndpointEEF1FD8F + DeploymentId: + Ref: EndpointDeployment318525DA37c0e38727e25b4317827bf43e918fbf + StageName: prod + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/DeploymentStage.prod/Resource + + EndpointCloudWatchRoleC3C64E0F: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: apigateway.amazonaws.com + Version: "2012-10-17" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/CloudWatchRole/Resource + + + EndpointAccountB8304247: + Type: AWS::ApiGateway::Account + Properties: + CloudWatchRoleArn: + Fn::GetAtt: + - EndpointCloudWatchRoleC3C64E0F + - Arn + DependsOn: + - EndpointEEF1FD8F + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/Account + + Endpointproxy39E2174E: + Type: AWS::ApiGateway::Resource + Properties: + ParentId: + Fn::GetAtt: + - EndpointEEF1FD8F + - RootResourceId + PathPart: "{proxy+}" + RestApiId: + Ref: EndpointEEF1FD8F + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/{proxy+}/Resource + EndpointproxyANYC09721C5: + Type: AWS::ApiGateway::Method + Properties: + HttpMethod: ANY + ResourceId: + Ref: Endpointproxy39E2174E + RestApiId: + Ref: EndpointEEF1FD8F + AuthorizationType: NONE + Integration: + IntegrationHttpMethod: POST + Type: AWS_PROXY + Uri: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":apigateway:" + - Ref: AWS::Region + - :lambda:path/2015-03-31/functions/ + - Fn::GetAtt: + - HelloHandler2E4FBA4D + - Arn + - /invocations + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/{proxy+}/ANY/Resource + EndpointANY485C938B: + Type: AWS::ApiGateway::Method + Properties: + HttpMethod: ANY + ResourceId: + Fn::GetAtt: + - EndpointEEF1FD8F + - RootResourceId + RestApiId: + Ref: EndpointEEF1FD8F + AuthorizationType: NONE + Integration: + IntegrationHttpMethod: POST + Type: AWS_PROXY + Uri: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":apigateway:" + - Ref: AWS::Region + - :lambda:path/2015-03-31/functions/ + - Fn::GetAtt: + - HelloHandler2E4FBA4D + - Arn + - /invocations + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/ANY/Resource + CDKMetadata: + Type: AWS::CDK::Metadata + Properties: + Modules: aws-cdk=0.22.0,jsii-runtime=node.js/v12.4.0 +Parameters: + HelloHandlerCodeS3Bucket4359A483: + Type: String + Description: S3 bucket for asset "CdkWorkshopStack/HelloHandler/Code" + HelloHandlerCodeS3VersionKey07D12610: + Type: String + Description: S3 key for asset version "CdkWorkshopStack/HelloHandler/Code" +Outputs: + Endpoint8024A810: + Value: + Fn::Join: + - "" + - - https:// + - Ref: EndpointEEF1FD8F + - .execute-api. + - Ref: AWS::Region + - "." + - Ref: AWS::URLSuffix + - / + - Ref: EndpointDeploymentStageprodB78BEEA0 + - / + Export: + Name: CdkWorkshopStack:Endpoint8024A810 + diff --git a/tests/integration/testdata/start_api/serverless-sample-output.yaml b/tests/integration/testdata/start_api/serverless-sample-output.yaml new file mode 100644 index 0000000000..e181ab7de2 --- /dev/null +++ b/tests/integration/testdata/start_api/serverless-sample-output.yaml @@ -0,0 +1,206 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Description: The AWS CloudFormation template for this Serverless application +Resources: + ServerlessDeploymentBucket: + Type: AWS::S3::Bucket + Properties: + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 + HelloWorldLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: "/aws/lambda/serverless-hello-world-dev-helloWorld" + IamRoleLambdaExecution: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: + Fn::Join: + - "-" + - - dev + - serverless-hello-world + - lambda + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogStream + Resource: + - Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/serverless-hello-world-dev*:* + - Effect: Allow + Action: + - logs:PutLogEvents + Resource: + - Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/serverless-hello-world-dev*:*:* + Path: "/" + RoleName: + Fn::Join: + - "-" + - - serverless-hello-world + - dev + - Ref: AWS::Region + - lambdaRole + HelloWorldLambdaFunction: + Type: AWS::Lambda::Function + Properties: + Code: "." + FunctionName: serverless-hello-world-dev-helloWorld + Handler: main.handler + MemorySize: 1024 + Role: + Fn::GetAtt: + - IamRoleLambdaExecution + - Arn + Runtime: python3.6 + DependsOn: + - HelloWorldLogGroup + - IamRoleLambdaExecution + HelloWorldLambdaVersionkbuu03utDK7jANXe4ADsn4Jcw0Gci6s02eSd52Kg: + Type: AWS::Lambda::Version + DeletionPolicy: Retain + Properties: + FunctionName: + Ref: HelloWorldLambdaFunction + CodeSha256: 2huiVVXNNgaCeFoyZScNWyGKnIMkvxfLD5+hjaVF6sM= + ApiGatewayRestApi: + Type: AWS::ApiGateway::RestApi + Properties: + Name: dev-serverless-hello-world + EndpointConfiguration: + Types: + - EDGE + ApiGatewayResourceHelloDashworld: + Type: AWS::ApiGateway::Resource + Properties: + ParentId: + Fn::GetAtt: + - ApiGatewayRestApi + - RootResourceId + PathPart: hello-world + RestApiId: + Ref: ApiGatewayRestApi + ApiGatewayMethodHelloDashworldOptions: + Type: AWS::ApiGateway::Method + Properties: + AuthorizationType: NONE + HttpMethod: OPTIONS + MethodResponses: + - StatusCode: '200' + ResponseParameters: + method.response.header.Access-Control-Allow-Origin: true + method.response.header.Access-Control-Allow-Headers: true + method.response.header.Access-Control-Allow-Methods: true + method.response.header.Access-Control-Allow-Credentials: true + ResponseModels: {} + RequestParameters: {} + Integration: + Type: MOCK + RequestTemplates: + application/json: "{statusCode:200}" + ContentHandling: CONVERT_TO_TEXT + IntegrationResponses: + - StatusCode: '200' + ResponseParameters: + method.response.header.Access-Control-Allow-Origin: "'*'" + method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'" + method.response.header.Access-Control-Allow-Methods: "'OPTIONS,GET'" + method.response.header.Access-Control-Allow-Credentials: "'false'" + ResponseTemplates: + application/json: |- + #set($origin = $input.params("Origin")) + #if($origin == "") #set($origin = $input.params("origin")) #end + #if($origin.matches(".*")) #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin) #end + ResourceId: + Ref: ApiGatewayResourceHelloDashworld + RestApiId: + Ref: ApiGatewayRestApi + ApiGatewayMethodHelloDashworldGet: + Type: AWS::ApiGateway::Method + Properties: + HttpMethod: GET + RequestParameters: {} + ResourceId: + Ref: ApiGatewayResourceHelloDashworld + RestApiId: + Ref: ApiGatewayRestApi + ApiKeyRequired: false + AuthorizationType: NONE + Integration: + IntegrationHttpMethod: POST + Type: AWS_PROXY + Uri: + Fn::Join: + - '' + - - 'arn:' + - Ref: AWS::Partition + - ":apigateway:" + - Ref: AWS::Region + - ":lambda:path/2015-03-31/functions/" + - Fn::GetAtt: + - HelloWorldLambdaFunction + - Arn + - "/invocations" + MethodResponses: [] + ApiGatewayDeployment1561844009303: + Type: AWS::ApiGateway::Deployment + Properties: + RestApiId: + Ref: ApiGatewayRestApi + StageName: dev + DependsOn: + - ApiGatewayMethodHelloDashworldOptions + - ApiGatewayMethodHelloDashworldGet + HelloWorldLambdaPermissionApiGateway: + Type: AWS::Lambda::Permission + Properties: + FunctionName: + Fn::GetAtt: + - HelloWorldLambdaFunction + - Arn + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + SourceArn: + Fn::Join: + - '' + - - 'arn:' + - Ref: AWS::Partition + - ":execute-api:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - ":" + - Ref: ApiGatewayRestApi + - "/*/*" +Outputs: + ServerlessDeploymentBucketName: + Value: + Ref: ServerlessDeploymentBucket + HelloWorldLambdaFunctionQualifiedArn: + Description: Current Lambda function version + Value: + Ref: HelloWorldLambdaVersionkbuu03utDK7jANXe4ADsn4Jcw0Gci6s02eSd52Kg + ServiceEndpoint: + Description: URL of the service endpoint + Value: + Fn::Join: + - '' + - - https:// + - Ref: ApiGatewayRestApi + - ".execute-api." + - Ref: AWS::Region + - "." + - Ref: AWS::URLSuffix + - "/dev" \ No newline at end of file diff --git a/tests/unit/commands/local/lib/test_cfn_api_provider.py b/tests/unit/commands/local/lib/test_cfn_api_provider.py index 840e835d67..85388467b3 100644 --- a/tests/unit/commands/local/lib/test_cfn_api_provider.py +++ b/tests/unit/commands/local/lib/test_cfn_api_provider.py @@ -739,3 +739,182 @@ def test_binary_media_types_method(self): provider = ApiProvider(template) assertCountEqual(self, provider.api.binary_media_types, ["image/png", "image/jpg"]) + + def test_cdk(self): + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "HelloHandler2E4FBA4D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": ".", + "Handler": "main.handler", + "Runtime": "python3.6", + }, + "DependsOn": [ + "HelloHandlerServiceRole11EF7C63" + ], + }, + "EndpointEEF1FD8F": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "Endpoint" + } + }, + "EndpointDeploymentStageprodB78BEEA0": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + }, + "DeploymentId": { + "Ref": "EndpointDeployment318525DA37c0e38727e25b4317827bf43e918fbf" + }, + "StageName": "prod" + } + }, + "Endpointproxy39E2174E": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "EndpointEEF1FD8F", + "RootResourceId" + ] + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + } + } + }, + "EndpointproxyANYC09721C5": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Ref": "Endpointproxy39E2174E" + }, + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + "lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "HelloHandler2E4FBA4D", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "EndpointANY485C938B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "EndpointEEF1FD8F", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + "lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "HelloHandler2E4FBA4D", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + } + }, + "Parameters": { + "HelloHandlerCodeS3Bucket4359A483": { + "Type": "String", + "Description": "S3 bucket for asset \"CdkWorkshopStack/HelloHandler/Code\"" + }, + "HelloHandlerCodeS3VersionKey07D12610": { + "Type": "String", + "Description": "S3 key for asset version \"CdkWorkshopStack/HelloHandler/Code\"" + } + }, + "Outputs": { + "Endpoint8024A810": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "EndpointEEF1FD8F" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "EndpointDeploymentStageprodB78BEEA0" + }, + "/" + ] + ] + }, + "Export": { + "Name": "CdkWorkshopStack:Endpoint8024A810" + } + } + } + } + provider = ApiProvider(template) + proxy_paths = [Route(path="/{proxy+}", methods=Route.ANY_HTTP_METHODS, function_name="HelloHandler2E4FBA4D")] + root_paths = [Route(path="/", methods=Route.ANY_HTTP_METHODS, function_name="HelloHandler2E4FBA4D")] + assertCountEqual(self, provider.routes, proxy_paths + root_paths) diff --git a/tests/unit/commands/local/lib/test_sam_base_provider.py b/tests/unit/commands/local/lib/test_sam_base_provider.py index d3f16edfdc..b516437948 100644 --- a/tests/unit/commands/local/lib/test_sam_base_provider.py +++ b/tests/unit/commands/local/lib/test_sam_base_provider.py @@ -1,199 +1,27 @@ from unittest import TestCase from mock import Mock, patch -from nose_parameterized import parameterized - from samcli.commands.local.lib.sam_base_provider import SamBaseProvider - - -class TestSamBaseProvider_resolve_parameters(TestCase): - - @parameterized.expand([ - ("AWS::AccountId", "123456789012"), - ("AWS::Partition", "aws"), - ("AWS::Region", "us-east-1"), - ("AWS::StackName", "local"), - ("AWS::StackId", "arn:aws:cloudformation:us-east-1:123456789012:stack/" - "local/51af3dc0-da77-11e4-872e-1234567db123"), - ("AWS::URLSuffix", "localhost"), - ]) - def test_with_pseudo_parameters(self, parameter, expected_value): - template_dict = { - "Key": { - "Ref": parameter - } - } - - expected_template = { - "Key": expected_value - } - - result = SamBaseProvider._resolve_parameters(template_dict, {}) - self.assertEquals(result, expected_template) - - def test_override_pseudo_parameters(self): - template = { - "Key": { - "Ref": "AWS::Region" - } - } - - override = { - "AWS::Region": "someregion" - } - - expected_template = { - "Key": "someregion" - } - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) - - def test_parameter_with_defaults(self): - override = {} # No overrides - - template = { - "Parameters": { - "Key1": { - "Default": "Value1" - }, - "Key2": { - "Default": "Value2" - }, - "NoDefaultKey3": {} # No Default - }, - - "Resources": { - "R1": {"Ref": "Key1"}, - "R2": {"Ref": "Key2"}, - "R3": {"Ref": "NoDefaultKey3"} - } - } - - expected_template = { - "Parameters": { - "Key1": { - "Default": "Value1" - }, - "Key2": { - "Default": "Value2" - }, - "NoDefaultKey3": {} # No Default - }, - - "Resources": { - "R1": "Value1", - "R2": "Value2", - "R3": {"Ref": "NoDefaultKey3"} # No default value. so no subsitution - } - } - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) - - def test_override_parameters(self): - template = { - "Parameters": { - "Key1": { - "Default": "Value1" - }, - "Key2": { - "Default": "Value2" - }, - "NoDefaultKey3": {}, - - "NoOverrideKey4": {} # No override Value provided - }, - - "Resources": { - "R1": {"Ref": "Key1"}, - "R2": {"Ref": "Key2"}, - "R3": {"Ref": "NoDefaultKey3"}, - "R4": {"Ref": "NoOverrideKey4"} - } - } - - override = { - "Key1": "OverrideValue1", - "Key2": "OverrideValue2", - "NoDefaultKey3": "OverrideValue3" - } - - expected_template = { - "Parameters": { - "Key1": { - "Default": "Value1" - }, - "Key2": { - "Default": "Value2" - }, - "NoDefaultKey3": {}, - "NoOverrideKey4": {} # No override Value provided - }, - - "Resources": { - "R1": "OverrideValue1", - "R2": "OverrideValue2", - "R3": "OverrideValue3", - "R4": {"Ref": "NoOverrideKey4"} - } - } - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) - - def test_must_skip_non_ref_intrinsics(self): - template = { - "Key1": {"Fn::Sub": ["${AWS::Region}"]}, # Sub is not implemented - "Key2": {"Ref": "MyParam"} - } - - override = {"MyParam": "MyValue"} - - expected_template = { - "Key1": {"Fn::Sub": ["${AWS::Region}"]}, - "Key2": "MyValue" - } - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) - - def test_must_skip_empty_overrides(self): - template = {"Key": {"Ref": "Param"}} - override = None - expected_template = {"Key": {"Ref": "Param"}} - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) - - def test_must_skip_empty_template(self): - template = {} - override = None - expected_template = {} - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) +from samcli.lib.intrinsic_resolver.intrinsic_property_resolver import IntrinsicResolver class TestSamBaseProvider_get_template(TestCase): - @patch("samcli.commands.local.lib.sam_base_provider.ResourceMetadataNormalizer") @patch("samcli.commands.local.lib.sam_base_provider.SamTranslatorWrapper") - @patch.object(SamBaseProvider, "_resolve_parameters") - def test_must_run_translator_plugins(self, - resolve_params_mock, - SamTranslatorWrapperMock, - resource_metadata_normalizer_patch): + @patch.object(IntrinsicResolver, "resolve_template") + def test_must_run_translator_plugins( + self, + resolve_template_mock, + SamTranslatorWrapperMock, + resource_metadata_normalizer_patch, + ): + resource_metadata_normalizer_patch.normalize.return_value = True + resolve_template_mock.return_value = {} translator_instance = SamTranslatorWrapperMock.return_value = Mock() - parameter_resolved_template = {"Key": "Value", "Parameter": "Resolved"} - resolve_params_mock.return_value = parameter_resolved_template - template = {"Key": "Value"} - overrides = {'some': 'value'} + overrides = {"some": "value"} SamBaseProvider.get_template(template, overrides) SamTranslatorWrapperMock.assert_called_once_with(template) translator_instance.run_plugins.assert_called_once() - resolve_params_mock.assert_called_once() - resource_metadata_normalizer_patch.normalize.assert_called_once_with(parameter_resolved_template) diff --git a/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py index 7f280b31e3..5b73a304dc 100644 --- a/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py +++ b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py @@ -1225,7 +1225,7 @@ def setUp(self): ) def test_basic_template_resolution(self): - resolved_template = self.resolver.resolve_template(ignore_errors=False) + resolved_template = self.resolver.resolve_attribute(self.resources, ignore_errors=False) expected_resources = { "HelloHandler2E4FBA4D": { "Properties": {"handler": "main.handle"}, @@ -1270,7 +1270,7 @@ def test_template_fail_errors(self): ) resolver = IntrinsicResolver(template=template, symbol_resolver=symbol_resolver) with self.assertRaises(InvalidIntrinsicException, msg="Invalid Find In Map"): - resolver.resolve_template(ignore_errors=False) + resolver.resolve_attribute(resources, ignore_errors=False) def test_template_ignore_errors(self): resources = deepcopy(self.resources) @@ -1286,7 +1286,7 @@ def test_template_ignore_errors(self): template=template, logical_id_translator=self.logical_id_translator ) resolver = IntrinsicResolver(template=template, symbol_resolver=symbol_resolver) - result = resolver.resolve_template(ignore_errors=True) + result = resolver.resolve_attribute(resources, ignore_errors=True) expected_template = { "HelloHandler2E4FBA4D": { "Properties": {"handler": "main.handle"},