diff --git a/docs/globals.rst b/docs/globals.rst index e7c8afa14..07ab036ee 100644 --- a/docs/globals.rst +++ b/docs/globals.rst @@ -68,6 +68,7 @@ Currently, the following resources and properties are being supported: AutoPublishAlias: DeploymentPreference: PermissionsBoundary: + ReservedConcurrentExecutions: Api: # Properties of AWS::Serverless::Api diff --git a/samtranslator/plugins/globals/globals.py b/samtranslator/plugins/globals/globals.py index eb44b26e2..f74029b99 100644 --- a/samtranslator/plugins/globals/globals.py +++ b/samtranslator/plugins/globals/globals.py @@ -30,7 +30,8 @@ class Globals(object): "AutoPublishAlias", "Layers", "DeploymentPreference", - "PermissionsBoundary" + "PermissionsBoundary", + "ReservedConcurrentExecutions" ], # Everything except diff --git a/samtranslator/public/sdk/parameter.py b/samtranslator/public/sdk/parameter.py new file mode 100644 index 000000000..442e90b40 --- /dev/null +++ b/samtranslator/public/sdk/parameter.py @@ -0,0 +1,3 @@ +# flake8: noqa + +from samtranslator.sdk.parameter import SamParameterValues \ No newline at end of file diff --git a/samtranslator/sdk/parameter.py b/samtranslator/sdk/parameter.py new file mode 100644 index 000000000..350e46331 --- /dev/null +++ b/samtranslator/sdk/parameter.py @@ -0,0 +1,67 @@ +import boto3 +import copy + + +class SamParameterValues(object): + """ + Class representing SAM parameter values. + """ + + def __init__(self, parameter_values): + """ + Initialize the object given the parameter values as a dictionary + + :param dict parameter_values: Parameter value dictionary containing parameter name & value + """ + + self.parameter_values = copy.deepcopy(parameter_values) + + def add_default_parameter_values(self, sam_template): + """ + Method to read default values for template parameters and merge with user supplied values. + + Example: + If the template contains the following parameters defined + + Parameters: + Param1: + Type: String + Default: default_value + Param2: + Type: String + Default: default_value + + And, the user explicitly provided the following parameter values: + + { + Param2: "new value" + } + + then, this method will grab default value for Param1 and return the following result: + + { + Param1: "default_value", + Param2: "new value" + } + + + :param dict sam_template: SAM template + :param dict parameter_values: Dictionary of parameter values provided by the user + :return dict: Merged parameter values + """ + + parameter_definition = sam_template.get("Parameters", None) + if not parameter_definition or not isinstance(parameter_definition, dict): + return self.parameter_values + + for param_name, value in parameter_definition.items(): + if param_name not in self.parameter_values and isinstance(value, dict) and "Default" in value: + self.parameter_values[param_name] = value["Default"] + + def add_pseudo_parameter_values(self): + """ + Add pseudo parameter values + :return: parameter values that have pseudo parameter in it + """ + if 'AWS::Region' not in self.parameter_values: + self.parameter_values['AWS::Region'] = boto3.session.Session().region_name diff --git a/samtranslator/translator/translator.py b/samtranslator/translator/translator.py index f14f2d448..a0043ebf1 100644 --- a/samtranslator/translator/translator.py +++ b/samtranslator/translator/translator.py @@ -1,5 +1,4 @@ import copy -import boto3 from samtranslator.model import ResourceTypeResolver, sam_resources from samtranslator.translator.verify_logical_id import verify_unique_logical_id @@ -15,6 +14,7 @@ from samtranslator.plugins.globals.globals_plugin import GlobalsPlugin from samtranslator.plugins.policies.policy_templates_plugin import PolicyTemplatesForFunctionPlugin from samtranslator.policy_template_processor.processor import PolicyTemplatesProcessor +from samtranslator.sdk.parameter import SamParameterValues class Translator: @@ -46,8 +46,10 @@ def translate(self, sam_template, parameter_values): :returns: a copy of the template with SAM resources replaced with the corresponding CloudFormation, which may \ be dumped into a valid CloudFormation JSON or YAML template """ - parameter_values = self._add_default_parameter_values(sam_template, parameter_values) - parameter_values = self._add_pseudo_parameter_values(parameter_values) + sam_parameter_values = SamParameterValues(parameter_values) + sam_parameter_values.add_default_parameter_values(sam_template) + sam_parameter_values.add_pseudo_parameter_values() + parameter_values = sam_parameter_values.parameter_values # Create & Install plugins sam_plugins = prepare_plugins(self.plugins, parameter_values) @@ -157,62 +159,6 @@ def _get_resources_to_iterate(self, sam_template, macro_resolver): return functions + apis + others - # Ideally this should belong to a separate class called "Parameters" or something that knows how to manage - # parameters. An instance of this class should be passed as input to the Translate class. - def _add_default_parameter_values(self, sam_template, parameter_values): - """ - Method to read default values for template parameters and merge with user supplied values. - - Example: - If the template contains the following parameters defined - - Parameters: - Param1: - Type: String - Default: default_value - Param2: - Type: String - Default: default_value - - And, the user explicitly provided the following parameter values: - - { - Param2: "new value" - } - - then, this method will grab default value for Param1 and return the following result: - - { - Param1: "default_value", - Param2: "new value" - } - - - :param dict sam_template: SAM template - :param dict parameter_values: Dictionary of parameter values provided by the user - :return dict: Merged parameter values - """ - - parameter_definition = sam_template.get("Parameters", None) - if not parameter_definition or not isinstance(parameter_definition, dict): - return parameter_values - - default_values = {} - for param_name, value in parameter_definition.items(): - if isinstance(value, dict) and "Default" in value: - default_values[param_name] = value["Default"] - - # Any explicitly provided value must override the default - default_values.update(parameter_values) - - return default_values - - def _add_pseudo_parameter_values(self, parameter_values): - updated_parameter_values = copy.deepcopy(parameter_values) - if 'AWS::Region' not in updated_parameter_values: - updated_parameter_values['AWS::Region'] = boto3.session.Session().region_name - return updated_parameter_values - def prepare_plugins(plugins, parameters={}): """ diff --git a/tests/sdk/test_parameter.py b/tests/sdk/test_parameter.py new file mode 100644 index 000000000..d2eade038 --- /dev/null +++ b/tests/sdk/test_parameter.py @@ -0,0 +1,136 @@ +from parameterized import parameterized, param + +import pytest +from unittest import TestCase +from samtranslator.sdk.parameter import SamParameterValues +from mock import patch + +class TestSAMParameterValues(TestCase): + + def test_add_default_parameter_values_must_merge(self): + parameter_values = { + "Param1": "value1" + } + + sam_template = { + "Parameters": { + "Param2": { + "Type": "String", + "Default": "template default" + } + } + } + + expected = { + "Param1": "value1", + "Param2": "template default" + } + + sam_parameter_values = SamParameterValues(parameter_values) + sam_parameter_values.add_default_parameter_values(sam_template) + self.assertEqual(expected, sam_parameter_values.parameter_values) + + def test_add_default_parameter_values_must_override_user_specified_values(self): + parameter_values = { + "Param1": "value1" + } + + sam_template = { + "Parameters": { + "Param1": { + "Type": "String", + "Default": "template default" + } + } + } + + expected = { + "Param1": "value1" + } + + sam_parameter_values = SamParameterValues(parameter_values) + sam_parameter_values.add_default_parameter_values(sam_template) + self.assertEqual(expected, sam_parameter_values.parameter_values) + + def test_add_default_parameter_values_must_skip_params_without_defaults(self): + parameter_values = { + "Param1": "value1" + } + + sam_template = { + "Parameters": { + "Param1": { + "Type": "String" + }, + "Param2": { + "Type": "String" + } + } + } + + expected = { + "Param1": "value1" + } + + sam_parameter_values = SamParameterValues(parameter_values) + sam_parameter_values.add_default_parameter_values(sam_template) + self.assertEqual(expected, sam_parameter_values.parameter_values) + + + @parameterized.expand([ + # Array + param(["1", "2"]), + + # String + param("something"), + + # Some other non-parameter looking dictionary + param({"Param1": {"Foo": "Bar"}}), + + param(None) + ]) + def test_add_default_parameter_values_must_ignore_invalid_template_parameters(self, template_parameters): + parameter_values = { + "Param1": "value1" + } + + expected = { + "Param1": "value1" + } + + sam_template = { + "Parameters": template_parameters + } + + sam_parameter_values = SamParameterValues(parameter_values) + sam_parameter_values.add_default_parameter_values(sam_template) + self.assertEqual(expected, sam_parameter_values.parameter_values) + + @patch('boto3.session.Session.region_name', 'ap-southeast-1') + def test_add_pseudo_parameter_values_aws_region(self): + parameter_values = { + "Param1": "value1" + } + + expected = { + "Param1": "value1", + "AWS::Region": "ap-southeast-1" + } + + sam_parameter_values = SamParameterValues(parameter_values) + sam_parameter_values.add_pseudo_parameter_values() + self.assertEqual(expected, sam_parameter_values.parameter_values) + + @patch('boto3.session.Session.region_name', 'ap-southeast-1') + def test_add_pseudo_parameter_values_aws_region_not_override(self): + parameter_values = { + "AWS::Region": "value1" + } + + expected = { + "AWS::Region": "value1" + } + + sam_parameter_values = SamParameterValues(parameter_values) + sam_parameter_values.add_pseudo_parameter_values() + self.assertEqual(expected, sam_parameter_values.parameter_values) diff --git a/tests/translator/input/globals_for_function.yaml b/tests/translator/input/globals_for_function.yaml index 97fd5ae30..1490bf245 100644 --- a/tests/translator/input/globals_for_function.yaml +++ b/tests/translator/input/globals_for_function.yaml @@ -19,6 +19,7 @@ Globals: PermissionsBoundary: arn:aws:1234:iam:boundary/CustomerCreatedPermissionsBoundary Layers: - !Sub arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer:1 + ReservedConcurrentExecutions: 50 Resources: MinimalFunction: @@ -45,4 +46,5 @@ Resources: PermissionsBoundary: arn:aws:1234:iam:boundary/OverridePermissionsBoundary Layers: - !Sub arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer2:2 + ReservedConcurrentExecutions: 100 diff --git a/tests/translator/output/aws-cn/globals_for_function.json b/tests/translator/output/aws-cn/globals_for_function.json index e2115176f..a84c1d288 100644 --- a/tests/translator/output/aws-cn/globals_for_function.json +++ b/tests/translator/output/aws-cn/globals_for_function.json @@ -57,6 +57,7 @@ } ], "MemorySize": 512, + "ReservedConcurrentExecutions": 100, "Environment": { "Variables": { "Var1": "value1", @@ -146,6 +147,7 @@ } ], "MemorySize": 1024, + "ReservedConcurrentExecutions": 50, "Environment": { "Variables": { "Var1": "value1", diff --git a/tests/translator/output/aws-us-gov/globals_for_function.json b/tests/translator/output/aws-us-gov/globals_for_function.json index d00126709..bd87cc916 100644 --- a/tests/translator/output/aws-us-gov/globals_for_function.json +++ b/tests/translator/output/aws-us-gov/globals_for_function.json @@ -57,6 +57,7 @@ } ], "MemorySize": 512, + "ReservedConcurrentExecutions": 100, "Environment": { "Variables": { "Var1": "value1", @@ -146,6 +147,7 @@ } ], "MemorySize": 1024, + "ReservedConcurrentExecutions": 50, "Environment": { "Variables": { "Var1": "value1", diff --git a/tests/translator/output/error_globals_unsupported_property.json b/tests/translator/output/error_globals_unsupported_property.json index 4805f3f4e..c814d640a 100644 --- a/tests/translator/output/error_globals_unsupported_property.json +++ b/tests/translator/output/error_globals_unsupported_property.json @@ -1,8 +1,8 @@ { "errors": [ { - "errorMessage": "'Globals' section is invalid. 'SomeKey' is not a supported property of 'Function'. Must be one of the following values - ['Handler', 'Runtime', 'CodeUri', 'DeadLetterQueue', 'Description', 'MemorySize', 'Timeout', 'VpcConfig', 'Environment', 'Tags', 'Tracing', 'KmsKeyArn', 'AutoPublishAlias', 'Layers', 'DeploymentPreference', 'PermissionsBoundary']" + "errorMessage": "'Globals' section is invalid. 'SomeKey' is not a supported property of 'Function'. Must be one of the following values - ['Handler', 'Runtime', 'CodeUri', 'DeadLetterQueue', 'Description', 'MemorySize', 'Timeout', 'VpcConfig', 'Environment', 'Tags', 'Tracing', 'KmsKeyArn', 'AutoPublishAlias', 'Layers', 'DeploymentPreference', 'PermissionsBoundary', 'ReservedConcurrentExecutions']" } ], - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. 'Globals' section is invalid. 'SomeKey' is not a supported property of 'Function'. Must be one of the following values - ['Handler', 'Runtime', 'CodeUri', 'DeadLetterQueue', 'Description', 'MemorySize', 'Timeout', 'VpcConfig', 'Environment', 'Tags', 'Tracing', 'KmsKeyArn', 'AutoPublishAlias', 'Layers', 'DeploymentPreference', 'PermissionsBoundary']" + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. 'Globals' section is invalid. 'SomeKey' is not a supported property of 'Function'. Must be one of the following values - ['Handler', 'Runtime', 'CodeUri', 'DeadLetterQueue', 'Description', 'MemorySize', 'Timeout', 'VpcConfig', 'Environment', 'Tags', 'Tracing', 'KmsKeyArn', 'AutoPublishAlias', 'Layers', 'DeploymentPreference', 'PermissionsBoundary', 'ReservedConcurrentExecutions']" } diff --git a/tests/translator/output/globals_for_function.json b/tests/translator/output/globals_for_function.json index 3006cce73..6d0e58931 100644 --- a/tests/translator/output/globals_for_function.json +++ b/tests/translator/output/globals_for_function.json @@ -57,6 +57,7 @@ } ], "MemorySize": 512, + "ReservedConcurrentExecutions": 100, "Environment": { "Variables": { "Var1": "value1", @@ -146,6 +147,7 @@ } ], "MemorySize": 1024, + "ReservedConcurrentExecutions": 50, "Environment": { "Variables": { "Var1": "value1", diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index a032ede88..62a8bd0ea 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -630,152 +630,6 @@ def _do_transform(self, document, parameter_values=get_template_parameter_values return output_fragment -class TestParameterValuesHandling(TestCase): - """ - Test how user-supplied parameters & default template parameter values from template get merged - """ - - def test_add_default_parameter_values_must_merge(self): - parameter_values = { - "Param1": "value1" - } - - sam_template = { - "Parameters": { - "Param2": { - "Type": "String", - "Default": "template default" - } - } - } - - expected = { - "Param1": "value1", - "Param2": "template default" - } - - sam_parser = Parser() - translator = Translator({}, sam_parser) - result = translator._add_default_parameter_values(sam_template, - parameter_values) - self.assertEqual(expected, result) - - def test_add_default_parameter_values_must_override_user_specified_values(self): - parameter_values = { - "Param1": "value1" - } - - sam_template = { - "Parameters": { - "Param1": { - "Type": "String", - "Default": "template default" - } - } - } - - expected = { - "Param1": "value1" - } - - - sam_parser = Parser() - translator = Translator({}, sam_parser) - result = translator._add_default_parameter_values(sam_template, parameter_values) - self.assertEqual(expected, result) - - def test_add_default_parameter_values_must_skip_params_without_defaults(self): - parameter_values = { - "Param1": "value1" - } - - sam_template = { - "Parameters": { - "Param1": { - "Type": "String" - }, - "Param2": { - "Type": "String" - } - } - } - - expected = { - "Param1": "value1" - } - - sam_parser = Parser() - translator = Translator({}, sam_parser) - result = translator._add_default_parameter_values(sam_template, parameter_values) - self.assertEqual(expected, result) - - - @parameterized.expand([ - # Array - param(["1", "2"]), - - # String - param("something"), - - # Some other non-parameter looking dictionary - param({"Param1": {"Foo": "Bar"}}), - - param(None) - ]) - def test_add_default_parameter_values_must_ignore_invalid_template_parameters(self, template_parameters): - parameter_values = { - "Param1": "value1" - } - - expected = { - "Param1": "value1" - } - - sam_template = { - "Parameters": template_parameters - } - - - sam_parser = Parser() - translator = Translator({}, sam_parser) - result = translator._add_default_parameter_values( - sam_template, parameter_values) - self.assertEqual(expected, result) - - @patch('boto3.session.Session.region_name', 'ap-southeast-1') - def test_add_pseudo_parameter_values_aws_region(self): - parameter_values = { - "Param1": "value1" - } - - expected = { - "Param1": "value1", - "AWS::Region": "ap-southeast-1" - } - - - sam_parser = Parser() - translator = Translator({}, sam_parser) - result = translator._add_pseudo_parameter_values(parameter_values) - self.assertEqual(expected, result) - - @patch('boto3.session.Session.region_name', 'ap-southeast-1') - def test_add_pseudo_parameter_values_aws_region_not_override(self): - parameter_values = { - "AWS::Region": "value1" - } - - expected = { - "AWS::Region": "value1" - } - - - sam_parser = Parser() - translator = Translator({}, sam_parser) - result = translator._add_pseudo_parameter_values(parameter_values) - self.assertEqual(expected, result) - - class TestTemplateValidation(TestCase): @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region)