From 7b4c7b03b662e2bcfebe14441a142ec19d63bd36 Mon Sep 17 00:00:00 2001 From: Keeton Hodgson Date: Thu, 29 Nov 2018 09:42:44 -0800 Subject: [PATCH] feat: add AWS::Serverless::LayerVersion and AWS::Serverless::Application (#688) --- docs/cloudformation_compatibility.rst | 15 + docs/globals.rst | 1 + examples/2016-10-31/nested_app/README.md | 8 + examples/2016-10-31/nested_app/src/app.py | 7 + examples/2016-10-31/nested_app/template.yaml | 56 ++++ samtranslator/__init__.py | 2 +- samtranslator/intrinsics/actions.py | 185 ++++++++++- samtranslator/intrinsics/resolver.py | 40 ++- samtranslator/model/__init__.py | 40 +++ samtranslator/model/cloudformation.py | 19 ++ samtranslator/model/lambda_.py | 20 ++ samtranslator/model/s3_utils/uri_parser.py | 36 ++ samtranslator/model/sam_resources.py | 213 ++++++++---- samtranslator/plugins/__init__.py | 20 +- samtranslator/plugins/application/__init__.py | 0 .../application/serverless_app_plugin.py | 311 ++++++++++++++++++ samtranslator/plugins/exceptions.py | 14 + samtranslator/plugins/globals/globals.py | 1 + samtranslator/sdk/resource.py | 2 + samtranslator/translator/arn_generator.py | 1 - samtranslator/translator/translator.py | 20 +- samtranslator/translator/verify_logical_id.py | 4 +- tests/intrinsics/test_actions.py | 232 +++++++++++++ tests/plugins/application/__init__.py | 0 .../application/test_serverless_app_plugin.py | 246 ++++++++++++++ tests/sdk/test_template.py | 18 +- .../input/application_preparing_state.yaml | 7 + tests/translator/input/basic_application.yaml | 28 ++ tests/translator/input/basic_layer.yaml | 26 ++ .../error_application_does_not_exist.yaml | 7 + .../input/error_application_no_access.yaml | 28 ++ .../error_application_preparing_timeout.yaml | 7 + .../input/error_application_properties.yaml | 36 ++ .../input/error_function_invalid_layer.yaml | 8 + .../input/error_layer_invalid_properties.yaml | 46 +++ .../input/error_reserved_sam_tag.yaml | 9 +- .../input/function_with_global_layers.yaml | 19 ++ .../input/function_with_layers.yaml | 42 +++ .../input/function_with_many_layers.yaml | 18 + .../input/globals_for_function.yaml | 4 + .../input/layers_all_properties.yaml | 40 +++ .../input/layers_with_intrinsics.yaml | 20 ++ .../output/application_preparing_state.json | 24 ++ .../aws-cn/application_preparing_state.json | 24 ++ .../output/aws-cn/basic_application.json | 73 ++++ .../translator/output/aws-cn/basic_layer.json | 44 +++ .../aws-cn/function_with_global_layers.json | 58 ++++ .../output/aws-cn/function_with_layers.json | 216 ++++++++++++ .../aws-cn/function_with_many_layers.json | 69 ++++ .../output/aws-cn/globals_for_function.json | 11 +- .../output/aws-cn/layers_all_properties.json | 122 +++++++ .../output/aws-cn/layers_with_intrinsics.json | 41 +++ .../application_preparing_state.json | 24 ++ .../output/aws-us-gov/basic_application.json | 73 ++++ .../output/aws-us-gov/basic_layer.json | 44 +++ .../function_with_global_layers.json | 58 ++++ .../aws-us-gov/function_with_layers.json | 216 ++++++++++++ .../aws-us-gov/function_with_many_layers.json | 69 ++++ .../aws-us-gov/globals_for_function.json | 11 +- .../aws-us-gov/layers_all_properties.json | 122 +++++++ .../aws-us-gov/layers_with_intrinsics.json | 41 +++ .../translator/output/basic_application.json | 73 ++++ tests/translator/output/basic_layer.json | 44 +++ .../error_application_does_not_exist.json | 8 + .../output/error_application_no_access.json | 8 + .../error_application_preparing_timeout.json | 8 + .../output/error_application_properties.json | 8 + .../output/error_function_invalid_layer.json | 8 + .../error_globals_unsupported_property.json | 4 +- .../error_layer_invalid_properties.json | 8 + .../output/error_reserved_sam_tag.json | 4 +- .../output/function_with_global_layers.json | 58 ++++ .../output/function_with_layers.json | 216 ++++++++++++ .../output/function_with_many_layers.json | 69 ++++ .../output/globals_for_function.json | 11 +- .../output/layers_all_properties.json | 122 +++++++ .../output/layers_with_intrinsics.json | 41 +++ tests/translator/test_api_resource.py | 5 +- tests/translator/test_function_resources.py | 1 + tests/translator/test_translator.py | 84 ++++- versions/2016-10-31.md | 106 +++++- 81 files changed, 3984 insertions(+), 98 deletions(-) create mode 100644 examples/2016-10-31/nested_app/README.md create mode 100644 examples/2016-10-31/nested_app/src/app.py create mode 100644 examples/2016-10-31/nested_app/template.yaml create mode 100644 samtranslator/model/cloudformation.py create mode 100644 samtranslator/plugins/application/__init__.py create mode 100644 samtranslator/plugins/application/serverless_app_plugin.py create mode 100644 samtranslator/plugins/exceptions.py create mode 100644 tests/plugins/application/__init__.py create mode 100644 tests/plugins/application/test_serverless_app_plugin.py create mode 100644 tests/translator/input/application_preparing_state.yaml create mode 100644 tests/translator/input/basic_application.yaml create mode 100644 tests/translator/input/basic_layer.yaml create mode 100644 tests/translator/input/error_application_does_not_exist.yaml create mode 100644 tests/translator/input/error_application_no_access.yaml create mode 100644 tests/translator/input/error_application_preparing_timeout.yaml create mode 100644 tests/translator/input/error_application_properties.yaml create mode 100644 tests/translator/input/error_function_invalid_layer.yaml create mode 100644 tests/translator/input/error_layer_invalid_properties.yaml create mode 100644 tests/translator/input/function_with_global_layers.yaml create mode 100644 tests/translator/input/function_with_layers.yaml create mode 100644 tests/translator/input/function_with_many_layers.yaml create mode 100644 tests/translator/input/layers_all_properties.yaml create mode 100644 tests/translator/input/layers_with_intrinsics.yaml create mode 100644 tests/translator/output/application_preparing_state.json create mode 100644 tests/translator/output/aws-cn/application_preparing_state.json create mode 100644 tests/translator/output/aws-cn/basic_application.json create mode 100644 tests/translator/output/aws-cn/basic_layer.json create mode 100644 tests/translator/output/aws-cn/function_with_global_layers.json create mode 100644 tests/translator/output/aws-cn/function_with_layers.json create mode 100644 tests/translator/output/aws-cn/function_with_many_layers.json create mode 100644 tests/translator/output/aws-cn/layers_all_properties.json create mode 100644 tests/translator/output/aws-cn/layers_with_intrinsics.json create mode 100644 tests/translator/output/aws-us-gov/application_preparing_state.json create mode 100644 tests/translator/output/aws-us-gov/basic_application.json create mode 100644 tests/translator/output/aws-us-gov/basic_layer.json create mode 100644 tests/translator/output/aws-us-gov/function_with_global_layers.json create mode 100644 tests/translator/output/aws-us-gov/function_with_layers.json create mode 100644 tests/translator/output/aws-us-gov/function_with_many_layers.json create mode 100644 tests/translator/output/aws-us-gov/layers_all_properties.json create mode 100644 tests/translator/output/aws-us-gov/layers_with_intrinsics.json create mode 100644 tests/translator/output/basic_application.json create mode 100644 tests/translator/output/basic_layer.json create mode 100644 tests/translator/output/error_application_does_not_exist.json create mode 100644 tests/translator/output/error_application_no_access.json create mode 100644 tests/translator/output/error_application_preparing_timeout.json create mode 100644 tests/translator/output/error_application_properties.json create mode 100644 tests/translator/output/error_function_invalid_layer.json create mode 100644 tests/translator/output/error_layer_invalid_properties.json create mode 100644 tests/translator/output/function_with_global_layers.json create mode 100644 tests/translator/output/function_with_layers.json create mode 100644 tests/translator/output/function_with_many_layers.json create mode 100644 tests/translator/output/layers_all_properties.json create mode 100644 tests/translator/output/layers_with_intrinsics.json diff --git a/docs/cloudformation_compatibility.rst b/docs/cloudformation_compatibility.rst index 968819e1b..c4ad64b2f 100644 --- a/docs/cloudformation_compatibility.rst +++ b/docs/cloudformation_compatibility.rst @@ -60,6 +60,7 @@ Tracing All KmsKeyArn All DeadLetterQueue All DeploymentPreference All +Layers All AutoPublishAlias Ref of a CloudFormation Parameter Alias resources created by SAM uses a LocicalId . So SAM either needs a string for alias name, or a Ref to template Parameter that SAM can resolve into a string. ReservedConcurrentExecutions All ============================ ================================== ======================== @@ -171,6 +172,20 @@ Cors All ================================== ======================== ======================== +AWS::Serverless::Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +================================== ======================== ======================== + Property Name Intrinsic(s) Supported Reasons +================================== ======================== ======================== +Location None SAM expects exact values for the Location property +Parameters All +NotificationArns All +Tags All +TimeoutInMinutes All +================================== ======================== ======================== + + AWS::Serverless::SimpleTable ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/globals.rst b/docs/globals.rst index b6eb15388..34db17c8f 100644 --- a/docs/globals.rst +++ b/docs/globals.rst @@ -64,6 +64,7 @@ Currently, the following resources and properties are being supported: Tags: Tracing: KmsKeyArn: + Layers: AutoPublishAlias: DeploymentPreference: diff --git a/examples/2016-10-31/nested_app/README.md b/examples/2016-10-31/nested_app/README.md new file mode 100644 index 000000000..dda03072b --- /dev/null +++ b/examples/2016-10-31/nested_app/README.md @@ -0,0 +1,8 @@ +## Nested App Example + +This app uses the [twitter event source app](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:077246666028:applications~aws-serverless-twitter-event-source) as a nested app and logs the tweets received from the nested app. + +All you need to do is supply the desired parameters for this app and deploy. SAM will create a nested stack for any nested app inside of your template with all of the parameters that are passed to it. + +## Installation Instructions + Please refer to the Installation Steps section of the [twitter-event-source application](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:077246666028:applications~aws-serverless-twitter-event-source) for detailed information regarding how to obtain and use the tokens and secrets for this application. \ No newline at end of file diff --git a/examples/2016-10-31/nested_app/src/app.py b/examples/2016-10-31/nested_app/src/app.py new file mode 100644 index 000000000..93ef77ec4 --- /dev/null +++ b/examples/2016-10-31/nested_app/src/app.py @@ -0,0 +1,7 @@ +import logging + +LOGGER = logging.getLogger() +LOGGER.setLevel(logging.INFO) + +def process_tweets(tweets, context): + LOGGER.info("Received tweets: {}".format(tweets)) diff --git a/examples/2016-10-31/nested_app/template.yaml b/examples/2016-10-31/nested_app/template.yaml new file mode 100644 index 000000000..c3b562ab6 --- /dev/null +++ b/examples/2016-10-31/nested_app/template.yaml @@ -0,0 +1,56 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Description: This example imports the aws-serverless-twitter-event-source serverless app as a nested app in this serverless application and connects it to a function that will log the tweets sent for the given Twitter search text. +Parameters: + EncryptedAccessToken: + Type: String + Description: Twitter API Access Token encrypted ciphertext blob as a base64-encoded string. + EncryptedAccessTokenSecret: + Type: String + Description: Twitter API Access Token Secret ciphertext blob as a base64-encoded string. + EncryptedConsumerKey: + Type: String + Description: Twitter API Consumer Key encrypted ciphertext blob as a base64-encoded string. + EncryptedConsumerSecret: + Type: String + Description: Twitter API Consumer Secret encrypted ciphertext blob as a base64-encoded string. + DecryptionKeyName: + Type: String + Description: KMS key name of the key used to encrypt the Twitter API parameters. Note, this must be just the key name (UUID), not the full key ARN. It's assumed the key is owned by the same account, in the same region as the app. + SearchText: + Type: String + Description: Non-URL-encoded search text poller should use when querying Twitter Search API. + Default: AWS + +Resources: + TweetLogger: + Type: 'AWS::Serverless::Function' + Properties: + Handler: app.process_tweets + Runtime: python3.6 + MemorySize: 128 + Timeout: 10 + CodeUri: src/ + TwitterEventSourceApp: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/aws-serverless-twitter-event-source + SemanticVersion: 1.1.0 + Parameters: # Using default value for PollingFrequencyInMinutes (1) + TweetProcessorFunctionName: !Ref TweetLogger + BatchSize: 20 + DecryptionKeyName: !Ref DecryptionKeyName + EncryptedAccessToken: !Ref EncryptedAccessToken + EncryptedAccessTokenSecret: !Ref EncryptedAccessTokenSecret + EncryptedConsumerKey: !Ref EncryptedConsumerKey + EncryptedConsumerSecret: !Ref EncryptedConsumerSecret + SearchText: !Sub '${SearchText} -filter:nativeretweets' # filter out retweet records from search results + TimeoutInMinutes: 20 + +Outputs: + TweetProcessorFunctionArn: + Value: !GetAtt TweetProcessorFunction.Arn + TwitterSearchPollerFunctionArn: + # Reference an output from the nested stack: + Value: !GetAtt TwitterEventSourceApp.Outputs.TwitterSearchPollerFunctionArn \ No newline at end of file diff --git a/samtranslator/__init__.py b/samtranslator/__init__.py index b28097579..e5102d301 100644 --- a/samtranslator/__init__.py +++ b/samtranslator/__init__.py @@ -1 +1 @@ -__version__ = '1.8.0' +__version__ = '1.9.0' diff --git a/samtranslator/intrinsics/actions.py b/samtranslator/intrinsics/actions.py index 645acda70..a151096d5 100644 --- a/samtranslator/intrinsics/actions.py +++ b/samtranslator/intrinsics/actions.py @@ -30,6 +30,12 @@ def resolve_resource_refs(self, input_dict, supported_resource_refs): """ raise NotImplementedError("Subclass must implement this method") + def resolve_resource_id_refs(self, input_dict, supported_resource_id_refs): + """ + Subclass must implement this method to resolve resource references + """ + raise NotImplementedError("Subclass must implement this method") + def can_handle(self, input_dict): """ Validates that the input dictionary contains only one key and is of the given intrinsic_name @@ -127,6 +133,35 @@ def resolve_resource_refs(self, input_dict, supported_resource_refs): self.intrinsic_name: resolved_value } + def resolve_resource_id_refs(self, input_dict, supported_resource_id_refs): + """ + Updates references to the old logical id of a resource to the new (generated) logical id. + + Example: + {"Ref": "MyLayer"} => {"Ref": "MyLayerABC123"} + + :param dict input_dict: Dictionary representing the Ref function to be resolved. + :param dict supported_resource_id_refs: Dictionary that maps old logical ids to new ones. + :return dict: Dictionary with resource references resolved. + """ + + if not self.can_handle(input_dict): + return input_dict + + ref_value = input_dict[self.intrinsic_name] + if not isinstance(ref_value, string_types) or self._resource_ref_separator in ref_value: + return input_dict + + logical_id = ref_value + + resolved_value = supported_resource_id_refs.get(logical_id) + if not resolved_value: + return input_dict + + return { + self.intrinsic_name: resolved_value + } + class SubAction(Action): intrinsic_name = "Fn::Sub" @@ -140,11 +175,6 @@ def resolve_parameter_refs(self, input_dict, parameters): :param parameters: Dictionary of parameter values for substitution :return: Resolved """ - if not self.can_handle(input_dict): - return input_dict - - key = self.intrinsic_name - value = input_dict[key] def do_replacement(full_ref, prop_name): """ @@ -157,9 +187,8 @@ def do_replacement(full_ref, prop_name): """ return parameters.get(prop_name, full_ref) - input_dict[key] = self._handle_sub_value(value, do_replacement) + return self._handle_sub_action(input_dict, do_replacement) - return input_dict def resolve_resource_refs(self, input_dict, supported_resource_refs): """ @@ -187,12 +216,6 @@ def resolve_resource_refs(self, input_dict, supported_resource_refs): :return: Resolved dictionary """ - if not self.can_handle(input_dict): - return input_dict - - key = self.intrinsic_name - sub_value = input_dict[key] - def do_replacement(full_ref, ref_value): """ Perform the appropriate replacement to handle ${LogicalId.Property} type references inside a Sub. @@ -224,7 +247,83 @@ def do_replacement(full_ref, ref_value): replacement = self._resource_ref_separator.join([logical_id, property]) return full_ref.replace(replacement, resolved_value) - input_dict[key] = self._handle_sub_value(sub_value, do_replacement) + return self._handle_sub_action(input_dict, do_replacement) + + + def resolve_resource_id_refs(self, input_dict, supported_resource_id_refs): + """ + Resolves reference to some property of a resource. Inside string to be substituted, there could be either a + "Ref" or a "GetAtt" usage of this property. They have to be handled differently. + + Ref usages are directly converted to a Ref on the resolved value. GetAtt usages are split under the assumption + that there can be only one property of resource referenced here. Everything else is an attribute reference. + + Example: + + Let's say `LogicalId` will be resolved to `NewLogicalId` + + Ref usage: + ${LogicalId} => ${NewLogicalId} + + GetAtt usage: + ${LogicalId.Arn} => ${NewLogicalId.Arn} + ${LogicalId.Attr1.Attr2} => {NewLogicalId.Attr1.Attr2} + + + :param input_dict: Dictionary to be resolved + :param dict supported_resource_id_refs: Dictionary that maps old logical ids to new ones. + :return: Resolved dictionary + """ + + def do_replacement(full_ref, ref_value): + """ + Perform the appropriate replacement to handle ${LogicalId} type references inside a Sub. + This method is called to get the replacement string for each reference within Sub's value + + :param full_ref: Entire reference string such as "${LogicalId.Property}" + :param ref_value: Just the value of the reference such as "LogicalId.Property" + :return: Resolved reference of the structure "${SomeOtherLogicalId}". Result should always include the + ${} structure since we are not resolving to final value, but just converting one reference to another + """ + + # Split the value by separator, expecting to separate out LogicalId + splits = ref_value.split(self._resource_ref_separator) + + # If we don't find at least one part, there is nothing to resolve + if len(splits) < 1: + return full_ref + + logical_id = splits[0] + resolved_value = supported_resource_id_refs.get(logical_id) + if not resolved_value: + # This ID/property combination is not in the supported references + return full_ref + + # We found a LogicalId.Property combination that can be resolved. Construct the output by replacing + # the part of the reference string and not constructing a new ref. This allows us to support GetAtt-like + # syntax and retain other attributes. Ex: ${LogicalId.Property.Arn} => ${SomeOtherLogicalId.Arn} + return full_ref.replace(logical_id, resolved_value) + + return self._handle_sub_action(input_dict, do_replacement) + + + def _handle_sub_action(self, input_dict, handler): + """ + Handles resolving replacements in the Sub action based on the handler that is passed as an input. + + :param input_dict: Dictionary to be resolved + :param supported_values: One of several different objects that contain the supported values that need to be changed. + See each method above for specifics on these objects. + :param handler: handler that is specific to each implementation. + :return: Resolved value of the Sub dictionary + """ + if not self.can_handle(input_dict): + return input_dict + + key = self.intrinsic_name + sub_value = input_dict[key] + + input_dict[key] = self._handle_sub_value(sub_value, handler) return input_dict @@ -345,9 +444,65 @@ def resolve_resource_refs(self, input_dict, supported_resource_refs): remaining = splits[2:] # if any resolved_value = supported_resource_refs.get(logical_id, property) + return self._get_resolved_dictionary(input_dict, key, resolved_value, remaining) + + + def resolve_resource_id_refs(self, input_dict, supported_resource_id_refs): + """ + Resolve resource references within a GetAtt dict. + + Example: + { "Fn::GetAtt": ["LogicalId", "Arn"] } => {"Fn::GetAtt": ["ResolvedLogicalId", "Arn"]} + + + Theoretically, only the first element of the array can contain reference to SAM resources. The second element + is name of an attribute (like Arn) of the resource. + + However tools like AWS CLI apply the assumption that first element of the array is a LogicalId and cannot + contain a 'dot'. So they break at the first dot to convert YAML tag to JSON map like this: + + `!GetAtt LogicalId.Arn` => {"Fn::GetAtt": [ "LogicalId", "Arn" ] } + + Therefore to resolve the reference, we join the array into a string, break it back up to check if it contains + a known reference, and resolve it if we can. + + :param input_dict: Dictionary to be resolved + :param dict supported_resource_id_refs: Dictionary that maps old logical ids to new ones. + :return: Resolved dictionary + """ + + if not self.can_handle(input_dict): + return input_dict + + key = self.intrinsic_name + value = input_dict[key] + + # Value must be an array with *at least* two elements. If not, this is invalid GetAtt syntax. We just pass along + # the input to CFN for it to do the "official" validation. + if not isinstance(value, list) or len(value) < 2: + return input_dict + + value_str = self._resource_ref_separator.join(value) + splits = value_str.split(self._resource_ref_separator) + logical_id = splits[0] + remaining = splits[1:] # if any + + resolved_value = supported_resource_id_refs.get(logical_id) + return self._get_resolved_dictionary(input_dict, key, resolved_value, remaining) + + + def _get_resolved_dictionary(self, input_dict, key, resolved_value, remaining): + """ + Resolves the function and returns the updated dictionary + + :param input_dict: Dictionary to be resolved + :param key: Name of this intrinsic. + :param resolved_value: Resolved or updated value for this action. + :param remaining: Remaining sections for the GetAtt action. + """ if resolved_value: # We resolved to a new resource logicalId. Use this as the first element and keep remaining elements intact # This is the new value of Fn::GetAtt input_dict[key] = [resolved_value] + remaining - return input_dict + return input_dict \ No newline at end of file diff --git a/samtranslator/intrinsics/resolver.py b/samtranslator/intrinsics/resolver.py index b50b28e2e..bcf6360e7 100644 --- a/samtranslator/intrinsics/resolver.py +++ b/samtranslator/intrinsics/resolver.py @@ -64,6 +64,29 @@ def resolve_sam_resource_refs(self, input, supported_resource_refs): """ return self._traverse(input, supported_resource_refs, self._try_resolve_sam_resource_refs) + def resolve_sam_resource_id_refs(self, input, supported_resource_id_refs): + """ + Some SAM resources have their logical ids mutated from the original id that the customer writes in the + template. This method recursively walks the tree and updates these logical ids from the old value + to the new value that is generated by SAM. + + Example: + {"Ref": "MyLayer"} -> {"Ref": "MyLayerABC123"} + + This method does not attempt to validate a reference. If it is invalid or non-resolvable, it skips the + occurrence and continues with the rest. It is recommended that you have an external process that detects and + surfaces invalid references. + + For first call, it is recommended that `template` is the entire CFN template in order to handle + references in Mapping or Output sections. + + :param dict input: CFN template that needs resolution. This method will modify the input + directly resolving references. In subsequent recursions, this will be a fragment of the CFN template. + :param dict supported_resource_id_refs: Dictionary that maps old logical ids to new ones. + :return list errors: List of dictionary containing information about invalid reference. Empty list otherwise + """ + return self._traverse(input, supported_resource_id_refs, self._try_resolve_sam_resource_id_refs) + def _traverse(self, input, resolution_data, resolver_method): """ Driver method that performs the actual traversal of input and calls the appropriate `resolver_method` when @@ -164,13 +187,28 @@ def _try_resolve_sam_resource_refs(self, input, supported_resource_refs): resource references and the values they resolve to. :return: Modified input dictionary with references resolved """ - if not self._is_intrinsic_dict(input): return input function_type = list(input.keys())[0] return self.supported_intrinsics[function_type].resolve_resource_refs(input, supported_resource_refs) + def _try_resolve_sam_resource_id_refs(self, input, supported_resource_id_refs): + """ + Try to resolve SAM resource id references on the given template. If the given object looks like one of the + supported intrinsics, it calls the appropriate resolution on it. If not, this method returns the original input + unmodified. + + :param dict input: Dictionary that may represent an intrinsic function + :param dict supported_resource_id_refs: Dictionary that maps old logical ids to new ones. + :return: Modified input dictionary with id references resolved + """ + if not self._is_intrinsic_dict(input): + return input + + function_type = list(input.keys())[0] + return self.supported_intrinsics[function_type].resolve_resource_id_refs(input, supported_resource_id_refs) + def _is_intrinsic_dict(self, input): """ Can the input represent an intrinsic function in it? diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index ecc354d6f..1834e4b73 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -3,6 +3,7 @@ import inspect from samtranslator.model.exceptions import InvalidResourceException from samtranslator.plugins import LifeCycleEvents +from samtranslator.model.tags.resource_tagging import get_tag_list class PropertyType(object): @@ -349,6 +350,21 @@ class SamResourceMacro(ResourceMacro): referable_properties = {} + # Each resource can optionally override this tag: + _SAM_KEY = 'lambda:createdBy' + _SAM_VALUE = 'SAM' + + # Tags reserved by the serverless application repo + _SAR_APP_KEY = 'serverlessrepo:applicationId' + _SAR_SEMVER_KEY = 'serverlessrepo:semanticVersion' + + # Aggregate list of all reserved tags + _RESERVED_TAGS = [ + _SAM_KEY, + _SAR_APP_KEY, + _SAR_SEMVER_KEY + ] + def get_resource_references(self, generated_cfn_resources, supported_resource_refs): """ Constructs the list of supported resource references by going through the list of CFN resources generated @@ -373,6 +389,30 @@ def get_resource_references(self, generated_cfn_resources, supported_resource_re return supported_resource_refs + def _construct_tag_list(self, tags, additional_tags=None): + if not bool(tags): + tags = {} + + if additional_tags == None: + additional_tags = {} + + for tag in self._RESERVED_TAGS: + self._check_tag(tag, tags) + + sam_tag = {self._SAM_KEY: self._SAM_VALUE} + + # To maintain backwards compatibility with previous implementation, we *must* append SAM tag to the start of the + # tags list. Changing this ordering will trigger a update on Lambda Function resource. Even though this + # does not change the actual content of the tags, we don't want to trigger update of a resource without + # customer's knowledge. + return get_tag_list(sam_tag) + get_tag_list(additional_tags) + get_tag_list(tags) + + def _check_tag(self, reserved_tag_name, tags): + if reserved_tag_name in tags: + raise InvalidResourceException(self.logical_id, reserved_tag_name + " is a reserved Tag key name and " + "cannot be set on your resource. " + "Please change the tag key in the input.") + class ResourceTypeResolver(object): """ResourceTypeResolver maps Resource Types to Resource classes, e.g. AWS::Serverless::Function to diff --git a/samtranslator/model/cloudformation.py b/samtranslator/model/cloudformation.py new file mode 100644 index 000000000..9edbcbb7e --- /dev/null +++ b/samtranslator/model/cloudformation.py @@ -0,0 +1,19 @@ +from samtranslator.model import PropertyType, Resource +from samtranslator.model.types import is_type, one_of, is_str, list_of, any_type +from samtranslator.model.intrinsics import fnGetAtt, ref + + +class NestedStack(Resource): + resource_type = 'AWS::CloudFormation::Stack' + # TODO: support passthrough parameters for stacks (Conditions, etc) + property_types = { + 'TemplateURL': PropertyType(True, is_str()), + 'Parameters': PropertyType(False, is_type(dict)), + 'NotificationArns': PropertyType(False, list_of(is_str())), + 'Tags': PropertyType(False, list_of(is_type(dict))), + 'TimeoutInMinutes': PropertyType(False, is_type(int)) + } + + runtime_attrs = { + "stack_id": lambda self: ref(self.logical_id) + } \ No newline at end of file diff --git a/samtranslator/model/lambda_.py b/samtranslator/model/lambda_.py index 6b7f4e5cb..750acbbb6 100644 --- a/samtranslator/model/lambda_.py +++ b/samtranslator/model/lambda_.py @@ -20,6 +20,7 @@ class LambdaFunction(Resource): 'Tags': PropertyType(False, list_of(is_type(dict))), 'TracingConfig': PropertyType(False, is_type(dict)), 'KmsKeyArn': PropertyType(False, one_of(is_type(dict), is_str())), + 'Layers': PropertyType(False, list_of(one_of(is_str(), is_type(dict)))), 'ReservedConcurrentExecutions': PropertyType(False, any_type()) } @@ -78,3 +79,22 @@ class LambdaPermission(Resource): 'SourceArn': PropertyType(False, is_str()), 'EventSourceToken': PropertyType(False, is_str()) } + +class LambdaLayerVersion(Resource): + """ Lambda layer version resource + """ + + resource_type = 'AWS::Lambda::LayerVersion' + property_types = { + 'Content': PropertyType(True, is_type(dict)), + 'Description': PropertyType(False, is_str()), + 'LayerName': PropertyType(False, is_str()), + 'CompatibleRuntimes': PropertyType(False, list_of(is_str())), + 'LicenseInfo': PropertyType(False, is_str()) + } + + runtime_attrs = { + "name": lambda self: ref(self.logical_id), + "arn": lambda self: fnGetAtt(self.logical_id, "Arn") + } + diff --git a/samtranslator/model/s3_utils/uri_parser.py b/samtranslator/model/s3_utils/uri_parser.py index 5452444fc..886e6b60d 100644 --- a/samtranslator/model/s3_utils/uri_parser.py +++ b/samtranslator/model/s3_utils/uri_parser.py @@ -1,5 +1,6 @@ from six import string_types from six.moves.urllib.parse import urlparse, parse_qs +from samtranslator.model.exceptions import InvalidResourceException def parse_s3_uri(uri): @@ -45,3 +46,38 @@ def to_s3_uri(code_dict): uri += "?versionId=" + version return uri + +def construct_s3_location_object(location_uri, logical_id, property_name): + """Constructs a Lambda `Code` or `Content` property, from the SAM `CodeUri` or `ContentUri` property. + This follows the current scheme for Lambda Functions and LayerVersions. + + :param dict or string location_uri: s3 location dict or string + :param string logical_id: logical_id of the resource calling this function + :param string property_name: name of the property which is used as an input to this function. + :returns: a Code dict, containing the S3 Bucket, Key, and Version of the Lambda layer code + :rtype: dict + """ + if isinstance(location_uri, dict): + if not location_uri.get("Bucket") or not location_uri.get("Key"): + # location_uri is a dictionary but does not contain Bucket or Key property + raise InvalidResourceException(logical_id, + "'{}' requires Bucket and Key properties to be specified".format(property_name)) + + s3_pointer = location_uri + + else: + # location_uri is NOT a dictionary. Parse it as a string + s3_pointer = parse_s3_uri(location_uri) + + if s3_pointer is None: + raise InvalidResourceException(logical_id, + '\'{}\' is not a valid S3 Uri of the form ' + '"s3://bucket/key" with optional versionId query parameter.'.format(property_name)) + + code = { + 'S3Bucket': s3_pointer['Bucket'], + 'S3Key': s3_pointer['Key'] + } + if 'Version' in s3_pointer: + code['S3ObjectVersion'] = s3_pointer['Version'] + return code diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 4e8c537a3..026d4343e 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -6,17 +6,19 @@ import samtranslator.model.eventsources.push import samtranslator.model.eventsources.cloudwatchlogs from .api.api_generator import ApiGenerator -from .s3_utils.uri_parser import parse_s3_uri +from .s3_utils.uri_parser import parse_s3_uri, construct_s3_location_object from .tags.resource_tagging import get_tag_list from samtranslator.model import (PropertyType, SamResourceMacro, ResourceTypeResolver) from samtranslator.model.apigateway import ApiGatewayDeployment, ApiGatewayStage +from samtranslator.model.cloudformation import NestedStack from samtranslator.model.dynamodb import DynamoDBTable from samtranslator.model.exceptions import (InvalidEventException, InvalidResourceException) from samtranslator.model.function_policies import FunctionPolicies, PolicyTypes from samtranslator.model.iam import IAMRole, IAMRolePolicies -from samtranslator.model.lambda_ import LambdaFunction, LambdaVersion, LambdaAlias +from samtranslator.model.lambda_ import (LambdaFunction, LambdaVersion, LambdaAlias, + LambdaLayerVersion) from samtranslator.model.types import dict_of, is_str, is_type, list_of, one_of, any_type from samtranslator.translator import logical_id_generator from samtranslator.translator.arn_generator import ArnGenerator @@ -26,10 +28,6 @@ class SamFunction(SamResourceMacro): """SAM function macro. """ - # Constants for Tagging - _SAM_KEY = "lambda:createdBy" - _SAM_VALUE = "SAM" - resource_type = 'AWS::Serverless::Function' property_types = { 'FunctionName': PropertyType(False, one_of(is_str(), is_type(dict))), @@ -51,6 +49,7 @@ class SamFunction(SamResourceMacro): 'KmsKeyArn': PropertyType(False, one_of(is_type(dict), is_str())), 'DeploymentPreference': PropertyType(False, is_type(dict)), 'ReservedConcurrentExecutions': PropertyType(False, any_type()), + 'Layers': PropertyType(False, list_of(one_of(is_str(), is_type(dict)))), # Intrinsic functions in value of Alias property are not supported, yet 'AutoPublishAlias': PropertyType(False, one_of(is_str())) @@ -170,7 +169,8 @@ def _construct_lambda_function(self): lambda_function.Code = self._construct_code_dict() lambda_function.KmsKeyArn = self.KmsKeyArn lambda_function.ReservedConcurrentExecutions = self.ReservedConcurrentExecutions - lambda_function.Tags = self._contruct_tag_list() + lambda_function.Tags = self._construct_tag_list(self.Tags) + lambda_function.Layers = self.Layers if self.Tracing: lambda_function.TracingConfig = {"Mode": self.Tracing} @@ -180,22 +180,6 @@ def _construct_lambda_function(self): return lambda_function - def _contruct_tag_list(self): - if not bool(self.Tags): - self.Tags = {} - - if self._SAM_KEY in self.Tags: - raise InvalidResourceException(self.logical_id, self._SAM_KEY + " is a reserved Tag key name and " - "cannot be set on your function. " - "Please change they tag key in the input.") - sam_tag = {self._SAM_KEY: self._SAM_VALUE} - - # To maintain backwards compatibility with previous implementation, we *must* append SAM tag to the start of the - # tags list. Changing this ordering will trigger a update on Lambda Function resource. Even though this - # does not change the actual content of the tags, we don't want to trigger update of a resource without - # customer's knowledge. - return get_tag_list(sam_tag) + get_tag_list(self.Tags) - def _construct_role(self, managed_policy_map): """Constructs a Lambda execution role based on this SAM function's Policies property. @@ -320,44 +304,10 @@ def _construct_code_dict(self): "ZipFile": self.InlineCode } elif self.CodeUri: - return self._construct_code_dict_code_uri() + return construct_s3_location_object(self.CodeUri, self.logical_id, 'CodeUri') else: raise InvalidResourceException(self.logical_id, "Either 'InlineCode' or 'CodeUri' must be set") - def _construct_code_dict_code_uri(self): - """Constructs the Lambda function's `Code property`_, from the SAM function's CodeUri property. - - .. _Code property: \ - http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html - - :returns: a Code dict, containing the S3 Bucket, Key, and Version of the Lambda function code - :rtype: dict - """ - if isinstance(self.CodeUri, dict): - if not self.CodeUri.get("Bucket", None) or not self.CodeUri.get("Key", None): - # CodeUri is a dictionary but does not contain Bucket or Key property - raise InvalidResourceException(self.logical_id, - "'CodeUri' requires Bucket and Key properties to be specified") - - s3_pointer = self.CodeUri - - else: - # CodeUri is NOT a dictionary. Parse it as a string - s3_pointer = parse_s3_uri(self.CodeUri) - - if s3_pointer is None: - raise InvalidResourceException(self.logical_id, - '\'CodeUri\' is not a valid S3 Uri of the form ' - '"s3://bucket/key" with optional versionId query parameter.') - - code = { - 'S3Bucket': s3_pointer['Bucket'], - 'S3Key': s3_pointer['Key'] - } - if 'Version' in s3_pointer: - code['S3ObjectVersion'] = s3_pointer['Version'] - return code - def _construct_version(self, function, intrinsics_resolver): """Constructs a Lambda Version resource that will be auto-published when CodeUri of the function changes. Old versions will not be deleted without a direct reference from the CloudFormation template. @@ -581,3 +531,150 @@ def _convert_attribute_type(self, attribute_type): if attribute_type in self.attribute_type_conversions: return self.attribute_type_conversions[attribute_type] raise InvalidResourceException(self.logical_id, 'Invalid \'Type\' "{actual}".'.format(actual=attribute_type)) + + +class SamApplication(SamResourceMacro): + """SAM application macro. + """ + + APPLICATION_ID_KEY = 'ApplicationId' + SEMANTIC_VERSION_KEY = 'SemanticVersion' + + resource_type = 'AWS::Serverless::Application' + + # The plugin will always insert the TemplateUrl parameter + property_types = { + 'Location': PropertyType(True, one_of(is_str(), is_type(dict))), + 'TemplateUrl': PropertyType(False, is_str()), + 'Parameters': PropertyType(False, is_type(dict)), + 'NotificationArns': PropertyType(False, list_of(is_str())), + 'Tags': PropertyType(False, is_type(dict)), + 'TimeoutInMinutes': PropertyType(False, is_type(int)) + } + + def to_cloudformation(self, **kwargs): + """Returns the stack with the proper parameters for this application + """ + nested_stack = self._construct_nested_stack() + return [nested_stack] + + def _construct_nested_stack(self): + """Constructs a AWS::CloudFormation::Stack resource + """ + nested_stack = NestedStack(self.logical_id, depends_on=self.depends_on) + nested_stack.Parameters = self.Parameters + nested_stack.NotificationArns = self.NotificationArns + application_tags = self._get_application_tags() + nested_stack.Tags = self._construct_tag_list(self.Tags, application_tags) + nested_stack.TimeoutInMinutes = self.TimeoutInMinutes + nested_stack.TemplateURL = self.TemplateUrl if self.TemplateUrl else "" + + return nested_stack + + def _get_application_tags(self): + """Adds tags to the stack if this resource is using the serverless app repo + """ + application_tags = {} + if isinstance(self.Location, dict): + if (self.APPLICATION_ID_KEY in self.Location.keys() + and self.Location[self.APPLICATION_ID_KEY] is not None): + application_tags[self._SAR_APP_KEY] = self.Location[self.APPLICATION_ID_KEY] + if (self.SEMANTIC_VERSION_KEY in self.Location.keys() + and self.Location[self.SEMANTIC_VERSION_KEY] is not None): + application_tags[self._SAR_SEMVER_KEY] = self.Location[self.SEMANTIC_VERSION_KEY] + return application_tags + + +class SamLayerVersion(SamResourceMacro): + """ SAM Layer macro + """ + resource_type = 'AWS::Serverless::LayerVersion' + property_types = { + 'LayerName': PropertyType(False, one_of(is_str(), is_type(dict))), + 'Description': PropertyType(False, is_str()), + 'ContentUri': PropertyType(True, one_of(is_str(), is_type(dict))), + 'CompatibleRuntimes': PropertyType(False, list_of(is_str())), + 'LicenseInfo': PropertyType(False, is_str()), + 'RetentionPolicy': PropertyType(False, is_str()) + } + + RETAIN = 'Retain' + DELETE = 'Delete' + retention_policy_options = [ RETAIN.lower(), DELETE.lower() ] + + def to_cloudformation(self, **kwargs): + """Returns the Lambda layer to which this SAM Layer corresponds. + + :param dict kwargs: already-converted resources that may need to be modified when converting this \ + macro to pure CloudFormation + :returns: a list of vanilla CloudFormation Resources, to which this Function expands + :rtype: list + """ + resources = [] + + # Append any CFN resources: + intrinsics_resolver = kwargs["intrinsics_resolver"] + resources.append(self._construct_lambda_layer(intrinsics_resolver)) + + return resources + + def _construct_lambda_layer(self, intrinsics_resolver): + """Constructs and returns the Lambda function. + + :returns: a list containing the Lambda function and execution role resources + :rtype: list + """ + retention_policy_value = self._get_retention_policy_value(intrinsics_resolver) + + retention_policy = { + 'DeletionPolicy': retention_policy_value + } + + old_logical_id = self.logical_id + new_logical_id = logical_id_generator.LogicalIdGenerator(old_logical_id, self.to_dict()).gen() + self.logical_id = new_logical_id + + lambda_layer = LambdaLayerVersion(self.logical_id, depends_on=self.depends_on, attributes=retention_policy) + + # Changing the LayerName property: when a layer is published, it is given an Arn + # example: arn:aws:lambda:us-west-2:123456789012:layer:MyLayer:1 + # where MyLayer is the LayerName property if it exists; otherwise, it is the + # LogicalId of this resource. Since a LayerVersion is an immutable resource, when + # CloudFormation updates this resource, it will ALWAYS create a new version then + # delete the old version if the logical ids match. What this does is change the + # logical id of every layer (so a `DeletionPolicy: Retain` can work) and set the + # LayerName property of the layer so that the Arn will still always be the same + # with the exception of an incrementing version number. + if not self.LayerName: + self.LayerName = old_logical_id + + lambda_layer.LayerName = self.LayerName + lambda_layer.Description = self.Description + lambda_layer.Content = construct_s3_location_object(self.ContentUri, self.logical_id, 'ContentUri') + lambda_layer.CompatibleRuntimes = self.CompatibleRuntimes + lambda_layer.LicenseInfo = self.LicenseInfo + + return lambda_layer + + def _get_retention_policy_value(self, intrinsics_resolver): + """ + Sets the deletion policy on this resource. The default is 'Retain'. + + :return: value for the DeletionPolicy attribute. + """ + if isinstance(self.RetentionPolicy, dict): + self.RetentionPolicy = intrinsics_resolver.resolve_parameter_refs(self.RetentionPolicy) + # If it's still not a string, throw an exception + if not isinstance(self.RetentionPolicy, string_types): + raise InvalidResourceException(self.logical_id, + "Could not resolve parameter for '{}' or parameter is not a String." + .format('RetentionPolicy')) + + if self.RetentionPolicy is None or self.RetentionPolicy.lower() == self.RETAIN.lower(): + return self.RETAIN + elif self.RetentionPolicy.lower() == self.DELETE.lower(): + return self.DELETE + elif self.RetentionPolicy.lower() not in self.retention_policy_options: + raise InvalidResourceException(self.logical_id, + "'{}' must be one of the following options: {}." + .format('RetentionPolicy', [self.RETAIN, self.DELETE])) diff --git a/samtranslator/plugins/__init__.py b/samtranslator/plugins/__init__.py index bc891ea8b..63d4c09c5 100644 --- a/samtranslator/plugins/__init__.py +++ b/samtranslator/plugins/__init__.py @@ -17,7 +17,7 @@ class SamPlugins(object): **Template Level** - before_transform_template - - [Coming Soon] after_transform_template + - after_transform_template When a life cycle event happens in the translator, this class will invoke the corresponding "hook" method on the each of the registered plugins to process. Plugins are free to modify internal state of the template or resources @@ -145,6 +145,7 @@ class LifeCycleEvents(Enum): """ before_transform_template = "before_transform_template" before_transform_resource = "before_transform_resource" + after_transform_template = "after_transform_template" class BasePlugin(object): @@ -205,3 +206,20 @@ def on_before_transform_template(self, template): :raises InvalidDocumentException: If the hook decides that the SAM template is invalid. """ pass + + def on_after_transform_template(self, template): + """ + Hook method to execute on "after_transform_template" life cycle event. Plugins may further modify + the template. Warning: any changes made in this lifecycle action by a plugin will not be + validated and may cause the template to fail deployment with hard-to-understand error messages + for customers. + + This method is called after the template passes all other template transform actions, right before + the resources are resolved to their final logical ID names. + + :param dict template: Entire SAM template as a dictionary. + :return: nothing + :raises InvalidDocumentException: If the hook decides that the SAM template is invalid. + :raises InvalidResourceException: If the hook decides that a SAM resource is invalid. + """ + pass \ No newline at end of file diff --git a/samtranslator/plugins/application/__init__.py b/samtranslator/plugins/application/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/samtranslator/plugins/application/serverless_app_plugin.py b/samtranslator/plugins/application/serverless_app_plugin.py new file mode 100644 index 000000000..cb73d38dd --- /dev/null +++ b/samtranslator/plugins/application/serverless_app_plugin.py @@ -0,0 +1,311 @@ +import boto3 +from botocore.exceptions import ClientError, EndpointConnectionError +import logging +from time import sleep, time + +from samtranslator.model.exceptions import InvalidResourceException, InvalidDocumentException +from samtranslator.plugins import BasePlugin +from samtranslator.plugins.exceptions import InvalidPluginException +from samtranslator.public.sdk.resource import SamResourceType +from samtranslator.public.sdk.template import SamTemplate + + +class ServerlessAppPlugin(BasePlugin): + """ + Resolves all of the ApplicationId and Semantic Version pairs + for AWS::Serverless::Application to template URLs. + + To retrieve a template from the Serverless Application Repository (SAR), + this plugin needs to call the CreateCloudFormationTemplate API, which + initiates the process of creating and copying the application template and + all of its assets from the region it is in to the current region. This + API returns a pre-signed S3 url that can be passed to CFN. When the template + reaches ACTIVE status, all assets have been successfully copied and are + ready to be deployed. This plugin verfies that applications are in an + ACTIVE state by calling the GetCloudFormation API from SAR. + """ + + SUPPORTED_RESOURCE_TYPE = "AWS::Serverless::Application" + SLEEP_TIME_SECONDS = 2 + # CloudFormation times out on transforms after 2 minutes, so setting this + # timeout below that to leave some buffer + TEMPLATE_WAIT_TIMEOUT_SECONDS = 105 + APPLICATION_ID_KEY = 'ApplicationId' + SEMANTIC_VERSION_KEY = 'SemanticVersion' + LOCATION_KEY = 'Location' + TEMPLATE_URL_KEY = 'TemplateUrl' + + + def __init__(self, sar_client=None, wait_for_template_active_status=False, validate_only=False): + """ + Initialize the plugin. + + Explain that Validate_only uses a different API call, and does not produce a valid template. + :param boto3.client sar_client: The boto3 client to use to access the Serverless Application Repository + :param bool wait_for_template_active_status: Flag to turn on the option to wait for all templates to become active + :param bool validate_only: Flag to only validate application access (uses get_application API instead) + """ + super(ServerlessAppPlugin, self).__init__(ServerlessAppPlugin.__name__) + self._applications = {} + self._in_progress_templates = [] + if sar_client: + self._sar_client = sar_client + else: + self._sar_client = boto3.client('serverlessrepo') + self._wait_for_template_active_status = wait_for_template_active_status + self._validate_only = validate_only + + # make sure the flag combination makes sense + if self._validate_only == True and self._wait_for_template_active_status == True: + message = "Cannot set both validate_only and wait_for_template_active_status flags to True." + raise InvalidPluginException(ServerlessAppPlugin.__name__, message) + + + def on_before_transform_template(self, template_dict): + """ + Hook method that gets called before the SAM template is processed. + The template has passed the validation and is guaranteed to contain a non-empty "Resources" section. + + This plugin needs to run as soon as possible to allow some time for templates to become available. + This verifies that the user has access to all specified applications. + + :param dict template_dict: Dictionary of the SAM template + :return: Nothing + """ + template = SamTemplate(template_dict) + + service_call = None + if self._validate_only: + service_call = self._handle_get_application_request + else: + service_call = self._handle_create_cfn_template_request + for logical_id, app in template.iterate(SamResourceType.Application.value): + if not self._can_process_application(app): + # Handle these cases in the on_before_transform_resource event + continue + + app_id = app.properties[self.LOCATION_KEY].get(self.APPLICATION_ID_KEY) + semver = app.properties[self.LOCATION_KEY].get(self.SEMANTIC_VERSION_KEY) + key = (app_id, semver) + if key not in self._applications: + try: + service_call(app_id, semver, key, logical_id) + except InvalidResourceException as e: + # Catch all InvalidResourceExceptions, raise those in the before_resource_transform target. + self._applications[key] = e + + + def _can_process_application(self, app): + """ + Determines whether or not the on_before_transform_template event can process this application + + :param dict app: the application and its properties + """ + return (self.LOCATION_KEY in app.properties + and isinstance(app.properties[self.LOCATION_KEY], dict) + and self.APPLICATION_ID_KEY in app.properties[self.LOCATION_KEY] + and self.SEMANTIC_VERSION_KEY in app.properties[self.LOCATION_KEY]) + + + def _handle_get_application_request(self, app_id, semver, key, logical_id): + """ + Method that handles the get_application API call to the serverless application repo + + This method puts something in the `_applications` dictionary because the plugin expects + something there in a later event. + + :param string app_id: ApplicationId + :param string semver: SemanticVersion + :param string key: The dictionary key consisting of (ApplicationId, SemanticVersion) + :param string logical_id: the logical_id of this application resource + """ + get_application = lambda app_id, semver: self._sar_client.get_application( + ApplicationId=self._sanitize_sar_str_param(app_id), + SemanticVersion=self._sanitize_sar_str_param(semver) + ) + try: + response = self._sar_service_call(get_application, logical_id, app_id, semver) + self._applications[key] = { 'Available' } + except EndpointConnectionError as e: + # No internet connection. Don't break verification, but do show a warning. + warning_message = "{}. Unable to verify access to {}/{}.".format(e, app_id, semver) + logging.warning(warning_message) + self._applications[key] = { 'Unable to verify' } + + + def _handle_create_cfn_template_request(self, app_id, semver, key, logical_id): + """ + Method that handles the create_cloud_formation_template API call to the serverless application repo + + :param string app_id: ApplicationId + :param string semver: SemanticVersion + :param string key: The dictionary key consisting of (ApplicationId, SemanticVersion) + :param string logical_id: the logical_id of this application resource + """ + create_cfn_template = lambda app_id, semver: self._sar_client.create_cloud_formation_template( + ApplicationId=self._sanitize_sar_str_param(app_id), + SemanticVersion=self._sanitize_sar_str_param(semver) + ) + response = self._sar_service_call(create_cfn_template, logical_id, app_id, semver) + self._applications[key] = response[self.TEMPLATE_URL_KEY] + if response['Status'] != "ACTIVE": + self._in_progress_templates.append((response[self.APPLICATION_ID_KEY], response['TemplateId'])) + + + def _sanitize_sar_str_param(self, param): + """ + Sanitize SAR API parameter expected to be a string. + + If customer passes something like 1.0 as SemanticVersion, python + converts it to a float instead of a basestring, so need to explicitly + convert it for API calls to SAR that expect a string input. + + :param object param: Parameter to sanitize + """ + if param is None: + # str(None) returns 'None' so need to explicitly handle this case + return None + return str(param) + + + def on_before_transform_resource(self, logical_id, resource_type, resource_properties): + """ + Hook method that gets called before "each" SAM resource gets processed + + Replaces the ApplicationId and Semantic Version pairs with a TemplateUrl. + + :param string logical_id: Logical ID of the resource being processed + :param string resource_type: Type of the resource being processed + :param dict resource_properties: Properties of the resource + :return: Nothing + """ + + if not self._resource_is_supported(resource_type): + return + + # Sanitize properties + self._check_for_dictionary_key(logical_id, resource_properties, [self.LOCATION_KEY]) + + # If location isn't a dictionary, don't modify the resource. + if not isinstance(resource_properties[self.LOCATION_KEY], dict): + resource_properties[self.TEMPLATE_URL_KEY] = resource_properties[self.LOCATION_KEY] + return + + # If it is a dictionary, check for other required parameters + self._check_for_dictionary_key(logical_id, resource_properties[self.LOCATION_KEY], [self.APPLICATION_ID_KEY, self.SEMANTIC_VERSION_KEY]) + + app_id = resource_properties[self.LOCATION_KEY].get(self.APPLICATION_ID_KEY) + if not app_id: + raise InvalidResourceException(logical_id, "Property 'ApplicationId' cannot be blank.") + semver = resource_properties[self.LOCATION_KEY].get(self.SEMANTIC_VERSION_KEY) + if not semver: + raise InvalidResourceException(logical_id, "Property 'SemanticVersion cannot be blank.") + key = (app_id, semver) + + # Throw any resource exceptions saved from the before_transform_template event + if isinstance(self._applications[key], InvalidResourceException): + raise self._applications[key] + + # validation does not resolve an actual template url + if not self._validate_only: + resource_properties[self.TEMPLATE_URL_KEY] = self._applications[key] + + def _check_for_dictionary_key(self, logical_id, dictionary, keys): + """ + Checks a dictionary to make sure it has a specific key. If it does not, an + InvalidResourceException is thrown. + + :param string logical_id: logical id of this resource + :param dict dictionary: the dictionary to check + :param list keys: list of keys that should exist in the dictionary + """ + for key in keys: + if key not in dictionary: + raise InvalidResourceException(logical_id, 'Resource is missing the required [{}] property.'.format(key)) + + + def on_after_transform_template(self, template): + """ + Hook method that gets called after the template is processed + + Go through all the stored applications and make sure they're all ACTIVE. + + :param dict template: Dictionary of the SAM template + :return: Nothing + """ + if self._wait_for_template_active_status and not self._validate_only: + start_time = time() + while (time() - start_time) < self.TEMPLATE_WAIT_TIMEOUT_SECONDS: + temp = self._in_progress_templates + self._in_progress_templates = [] + + # Check each resource to make sure it's active + for application_id, template_id in temp: + get_cfn_template = lambda application_id, template_id: self._sar_client.get_cloud_formation_template( + ApplicationId=self._sanitize_sar_str_param(application_id), + TemplateId=self._sanitize_sar_str_param(template_id) + ) + response = self._sar_service_call(get_cfn_template, application_id, application_id, template_id) + self._handle_get_cfn_template_response(response, application_id, template_id) + + # Don't sleep if there are no more templates with PREPARING status + if len(self._in_progress_templates) == 0: + break + + # Sleep a little so we don't spam service calls + sleep(self.SLEEP_TIME_SECONDS) + + # Not all templates reached active status + if len(self._in_progress_templates) != 0: + application_ids = [items[0] for items in self._in_progress_templates] + raise InvalidResourceException(application_ids, "Timed out waiting for nested stack templates to reach ACTIVE status.") + + + def _handle_get_cfn_template_response(self, response, application_id, template_id): + """ + Handles the response from the SAR service call + + :param dict response: the response dictionary from the app repo + :param string application_id: the ApplicationId + :param string template_id: the unique TemplateId for this application + """ + status = response['Status'] + if status != "ACTIVE": + # Other options are PREPARING and EXPIRED. + if status == 'EXPIRED': + message = "Template for {} with id {} returned status: {}. Cannot access an expired template.".format(application_id, template_id, status) + raise InvalidResourceException(application_id, message) + self._in_progress_templates.append((application_id, template_id)) + + + def _sar_service_call(self, service_call_lambda, logical_id, *args): + """ + Handles service calls and exception management for service calls + to the Serverless Application Repository. + + :param lambda service_call_lambda: lambda function that contains the service call + :param string logical_id: Logical ID of the resource being processed + :param list *args: arguments for the service call lambda + """ + try: + response = service_call_lambda(*args) + logging.info(response) + return response + except ClientError as e: + error_code = e.response['Error']['Code'] + if error_code in ('AccessDeniedException','NotFoundException'): + raise InvalidResourceException(logical_id, e.response['Error']['Message']) + + # 'ForbiddenException'- SAR rejects connection + logging.exception(e) + raise e + + + def _resource_is_supported(self, resource_type): + """ + Is this resource supported by this plugin? + + :param string resource_type: Type of the resource + :return: True, if this plugin supports this resource. False otherwise + """ + return resource_type == self.SUPPORTED_RESOURCE_TYPE diff --git a/samtranslator/plugins/exceptions.py b/samtranslator/plugins/exceptions.py new file mode 100644 index 000000000..4d4dc6c30 --- /dev/null +++ b/samtranslator/plugins/exceptions.py @@ -0,0 +1,14 @@ +class InvalidPluginException(Exception): + """Exception raised when the provided plugin configuration is not valid. + + Attributes: + plugin_name -- name of the plugin that caused this error + message -- explanation of the error + """ + def __init__(self, plugin_name, message): + self._plugin_name = plugin_name + self._message = message + + @property + def message(self): + return 'The {} plugin is invalid. {}'.format(self._plugin_name, self._message) \ No newline at end of file diff --git a/samtranslator/plugins/globals/globals.py b/samtranslator/plugins/globals/globals.py index 5c94009f5..a35c5929d 100644 --- a/samtranslator/plugins/globals/globals.py +++ b/samtranslator/plugins/globals/globals.py @@ -28,6 +28,7 @@ class Globals(object): "Tracing", "KmsKeyArn", "AutoPublishAlias", + "Layers", "DeploymentPreference" ], diff --git a/samtranslator/sdk/resource.py b/samtranslator/sdk/resource.py index d6eab6423..9b786b6d9 100644 --- a/samtranslator/sdk/resource.py +++ b/samtranslator/sdk/resource.py @@ -49,6 +49,8 @@ class SamResourceType(Enum): Api = "AWS::Serverless::Api" Function = "AWS::Serverless::Function" SimpleTable = "AWS::Serverless::SimpleTable" + Application = "AWS::Serverless::Application" + LambdaLayerVersion = "AWS::Serverless::LayerVersion" @classmethod def has_value(cls, value): diff --git a/samtranslator/translator/arn_generator.py b/samtranslator/translator/arn_generator.py index 0769520d4..db3005786 100644 --- a/samtranslator/translator/arn_generator.py +++ b/samtranslator/translator/arn_generator.py @@ -41,7 +41,6 @@ def get_partition_name(cls, region=None): :return: Partition name """ - if region is None: # Use Boto3 to get the region where code is running. This uses Boto's regular region resolution # mechanism, starting from AWS_DEFAULT_REGION environment variable. diff --git a/samtranslator/translator/translator.py b/samtranslator/translator/translator.py index 11adf850e..0dbb8372c 100644 --- a/samtranslator/translator/translator.py +++ b/samtranslator/translator/translator.py @@ -8,6 +8,8 @@ from samtranslator.intrinsics.resolver import IntrinsicsResolver from samtranslator.intrinsics.resource_refs import SupportedResourceReferences from samtranslator.plugins.api.default_definition_body_plugin import DefaultDefinitionBodyPlugin +from samtranslator.plugins.application.serverless_app_plugin import ServerlessAppPlugin +from samtranslator.plugins import LifeCycleEvents from samtranslator.plugins import SamPlugins from samtranslator.plugins.globals.globals_plugin import GlobalsPlugin from samtranslator.plugins.policies.policy_templates_plugin import PolicyTemplatesForFunctionPlugin @@ -58,6 +60,7 @@ def translate(self, sam_template, parameter_values): deployment_preference_collection = DeploymentPreferenceCollection() supported_resource_refs = SupportedResourceReferences() document_errors = [] + changed_logical_ids = {} for logical_id, resource_dict in self._get_resources_to_iterate(sam_template, macro_resolver): try: @@ -73,6 +76,10 @@ def translate(self, sam_template, parameter_values): supported_resource_refs = macro.get_resource_references(translated, supported_resource_refs) + # Some resources mutate their logical ids. Track those to change all references to them: + if logical_id != macro.logical_id: + changed_logical_ids[logical_id] = macro.logical_id + del template['Resources'][logical_id] for resource in translated: if verify_unique_logical_id(resource, sam_template['Resources']): @@ -91,12 +98,19 @@ def translate(self, sam_template, parameter_values): for logical_id in deployment_preference_collection.enabled_logical_ids(): template['Resources'].update(deployment_preference_collection.deployment_group(logical_id).to_dict()) - + + # Run the after-transform plugin target + try: + sam_plugins.act(LifeCycleEvents.after_transform_template, template) + except (InvalidDocumentException, InvalidResourceException) as e: + document_errors.append(e) + # Cleanup if 'Transform' in template: del template['Transform'] if len(document_errors) is 0: + template = intrinsics_resolver.resolve_sam_resource_id_refs(template, changed_logical_ids) template = intrinsics_resolver.resolve_sam_resource_refs(template, supported_resource_refs) return template else: @@ -207,6 +221,10 @@ def prepare_plugins(plugins): plugins = [] if not plugins else plugins + # If a ServerlessAppPlugin does not yet exist, create one and add to the beginning of the required plugins list. + if not any(isinstance(plugin, ServerlessAppPlugin) for plugin in plugins): + required_plugins.insert(0, ServerlessAppPlugin()) + # Execute customer's plugins first before running SAM plugins. It is very important to retain this order because # other plugins will be dependent on this ordering. return SamPlugins(plugins + required_plugins) diff --git a/samtranslator/translator/verify_logical_id.py b/samtranslator/translator/verify_logical_id.py index fffe56cbf..bf1ab6e58 100644 --- a/samtranslator/translator/verify_logical_id.py +++ b/samtranslator/translator/verify_logical_id.py @@ -1,10 +1,12 @@ do_not_verify = { # type_after_transform: type_before_transform 'AWS::Lambda::Function': 'AWS::Serverless::Function', + 'AWS::Lambda::LayerVersion': 'AWS::Serverless::LayerVersion', 'AWS::ApiGateway::RestApi': 'AWS::Serverless::Api', 'AWS::S3::Bucket': 'AWS::S3::Bucket', 'AWS::SNS::Topic': 'AWS::SNS::Topic', - 'AWS::DynamoDB::Table': 'AWS::Serverless::SimpleTable' + 'AWS::DynamoDB::Table': 'AWS::Serverless::SimpleTable', + 'AWS::CloudFormation::Stack': 'AWS::Serverless::Application' } diff --git a/tests/intrinsics/test_actions.py b/tests/intrinsics/test_actions.py index 8e4b3a642..06f42222e 100644 --- a/tests/intrinsics/test_actions.py +++ b/tests/intrinsics/test_actions.py @@ -24,6 +24,9 @@ class MyAction(Action): with self.assertRaises(NotImplementedError): MyAction().resolve_resource_refs({}, {}) + with self.assertRaises(NotImplementedError): + MyAction().resolve_resource_id_refs({}, {}) + def test_can_handle_input(self): class MyAction(Action): intrinsic_name = "foo" @@ -247,6 +250,55 @@ def test_return_value_if_cannot_handle(self, can_handle_mock): can_handle_mock.return_value = False # Simulate failure to handle the input. Result should be same as input self.assertEquals(expected, ref.resolve_resource_refs(input, self.supported_resource_refs_mock)) + +class TestRefCanResolveResourceIdRefs(TestCase): + + def setUp(self): + self.supported_resource_id_refs_mock = Mock() + self.ref = RefAction() + + def test_must_replace_refs(self): + resolved_value = "NewLogicalId" + input = { + "Ref": "LogicalId" + } + expected = { + "Ref": resolved_value + } + self.supported_resource_id_refs_mock.get.return_value = resolved_value + + output = self.ref.resolve_resource_id_refs(input, self.supported_resource_id_refs_mock) + + self.assertEquals(expected, output) + self.supported_resource_id_refs_mock.get.assert_called_once_with("LogicalId") + + def test_handle_unsupported_references(self): + input = { + "Ref": "OtherLogicalId.Property" + } + expected = { + "Ref": "OtherLogicalId.Property" + } + + self.supported_resource_id_refs_mock.get.return_value = None + + output = self.ref.resolve_resource_id_refs(input, self.supported_resource_id_refs_mock) + self.assertEquals(expected, output) + self.supported_resource_id_refs_mock.get.assert_not_called() + + @patch.object(RefAction, "can_handle") + def test_return_value_if_cannot_handle(self, can_handle_mock): + input = { + "Ref": "key" + } + expected = { + "Ref": "key" + } + + ref = RefAction() + can_handle_mock.return_value = False # Simulate failure to handle the input. Result should be same as input + self.assertEquals(expected, ref.resolve_resource_id_refs(input, self.supported_resource_id_refs_mock)) + class TestSubCanResolveParameterRefs(TestCase): def test_must_resolve_string_value(self): @@ -523,6 +575,95 @@ def test_return_value_if_cannot_handle(self, can_handle_mock): can_handle_mock.return_value = False # Simulate failure to handle the input. Result should be same as input self.assertEquals(expected, sub.resolve_resource_refs(input, parameters)) +class TestSubCanResolveResourceIdRefs(TestCase): + + def setUp(self): + self.supported_resource_id_refs = {} + self.supported_resource_id_refs["id1"] = "newid1" + self.supported_resource_id_refs["id2"] = "newid2" + self.supported_resource_id_refs["id3"] = "newid3" + + self.input_sub_value = "Hello ${id1} ${id2}${id3} ${id1.arn} ${id2.arn.name.foo} ${!id1.prop1} ${unknown} ${some.arn} World" + self.expected_output_sub_value = "Hello ${newid1} ${newid2}${newid3} ${newid1.arn} ${newid2.arn.name.foo} ${!id1.prop1} ${unknown} ${some.arn} World" + + def test_must_resolve_string_value(self): + + input = { + "Fn::Sub": self.input_sub_value + } + expected = { + "Fn::Sub": self.expected_output_sub_value + } + + sub = SubAction() + result = sub.resolve_resource_id_refs(input, self.supported_resource_id_refs) + + self.assertEqual(expected, result) + + def test_must_resolve_array_value(self): + input = { + "Fn::Sub": [self.input_sub_value, {"unknown":"a"}] + } + + expected = { + "Fn::Sub": [self.expected_output_sub_value, {"unknown": "a"}] + } + + sub = SubAction() + result = sub.resolve_resource_id_refs(input, self.supported_resource_id_refs) + + self.assertEqual(expected, result) + + def test_sub_all_refs_with_list_input(self): + parameters = { + "key1": "value1", + "key2": "value2" + } + input = { + "Fn::Sub": ["key1", "key2"] + } + expected = { + "Fn::Sub": ["key1", "key2"] + } + + sub = SubAction() + result = sub.resolve_resource_id_refs(input, parameters) + + self.assertEqual(expected, result) + + def test_sub_all_refs_with_dict_input(self): + parameters = { + "key1": "value1", + "key2": "value2" + } + input = { + "Fn::Sub": {"a": "key1", "b": "key2"} + } + expected = { + "Fn::Sub": {"a": "key1", "b": "key2"} + } + + sub = SubAction() + result = sub.resolve_resource_id_refs(input, parameters) + + self.assertEqual(expected, result) + + @patch.object(SubAction, "can_handle") + def test_return_value_if_cannot_handle(self, can_handle_mock): + parameters = { + "key": "value" + } + input = { + "Fn::Sub": "${key}" + } + expected = { + "Fn::Sub": "${key}" + } + + sub = SubAction() + can_handle_mock.return_value = False # Simulate failure to handle the input. Result should be same as input + self.assertEquals(expected, sub.resolve_resource_id_refs(input, parameters)) + class TestGetAttCanResolveParameterRefs(TestCase): @@ -683,3 +824,94 @@ def test_return_value_if_cannot_handle(self, can_handle_mock): getatt = GetAttAction() can_handle_mock.return_value = False # Simulate failure to handle the input. Result should be same as input self.assertEquals(expected, getatt.resolve_resource_refs(input, self.supported_resource_refs)) + + +class TestGetAttCanResolveResourceIdRefs(TestCase): + + def setUp(self): + self.supported_resource_id_refs = {} + self.supported_resource_id_refs['id1'] = "value1" + + def test_must_resolve_simple_refs(self): + input = { + "Fn::GetAtt": ["id1", "Arn"] + } + + expected = { + "Fn::GetAtt": ["value1", "Arn"] + } + + getatt = GetAttAction() + output = getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs) + + self.assertEquals(expected, output) + + def test_must_resolve_refs_with_many_attributes(self): + input = { + "Fn::GetAtt": ["id1", "Arn1", "Arn2", "Arn3"] + } + + expected = { + "Fn::GetAtt": ["value1", "Arn1", "Arn2", "Arn3"] + } + + getatt = GetAttAction() + output = getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs) + + self.assertEquals(expected, output) + + def test_must_ignore_invalid_value_array(self): + input = { + # No actual attributes + "Fn::GetAtt": ["id1"] + } + + expected = { + "Fn::GetAtt": ["id1"] + } + + getatt = GetAttAction() + output = getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs) + + self.assertEquals(expected, output) + + def test_must_ignore_invalid_value_type(self): + input = { + # No actual attributes + "Fn::GetAtt": {"a": "b"} + } + + expected = { + "Fn::GetAtt": {"a": "b"} + } + + getatt = GetAttAction() + output = getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs) + + self.assertEquals(expected, output) + + def test_must_ignore_missing_properties_with_dot_before(self): + input = { + "Fn::GetAtt": [".id1", "foo"] + } + expected = { + "Fn::GetAtt": [".id1", "foo"] + } + + getatt = GetAttAction() + output = getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs) + + self.assertEquals(expected, output) + + @patch.object(GetAttAction, "can_handle") + def test_return_value_if_cannot_handle(self, can_handle_mock): + input = { + "Fn::GetAtt": ["id1", "Arn"] + } + expected = { + "Fn::GetAtt": ["id1", "Arn"] + } + + getatt = GetAttAction() + can_handle_mock.return_value = False # Simulate failure to handle the input. Result should be same as input + self.assertEquals(expected, getatt.resolve_resource_id_refs(input, self.supported_resource_id_refs)) diff --git a/tests/plugins/application/__init__.py b/tests/plugins/application/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/plugins/application/test_serverless_app_plugin.py b/tests/plugins/application/test_serverless_app_plugin.py new file mode 100644 index 000000000..ed91db756 --- /dev/null +++ b/tests/plugins/application/test_serverless_app_plugin.py @@ -0,0 +1,246 @@ +import boto3 +import itertools + +from mock import Mock, patch +from unittest import TestCase +from parameterized import parameterized, param + +from samtranslator.plugins.application.serverless_app_plugin import ServerlessAppPlugin +from samtranslator.plugins.exceptions import InvalidPluginException + +# TODO: run tests when AWS CLI is not configured (so they can run in brazil) + +MOCK_TEMPLATE_URL = 'https://awsserverlessrepo-changesets-xxx.s3.amazonaws.com/pre-signed-url' +MOCK_TEMPLATE_ID = 'id-xx-xx' +STATUS_ACTIVE = 'ACTIVE' +STATUS_PREPARING = 'PREPARING' +STATUS_EXPIRED = 'EXPIRED' + +def mock_create_cloud_formation_template(ApplicationId=None, SemanticVersion=None): + message = { + 'ApplicationId': ApplicationId, + 'SemanticVersion': SemanticVersion, + 'Status': STATUS_ACTIVE, + 'TemplateId': MOCK_TEMPLATE_ID, + 'TemplateUrl': MOCK_TEMPLATE_URL + } + return message + + +def mock_get_application(ApplicationId=None, SemanticVersion=None): + message = { + 'ApplicationId': ApplicationId, + 'Author': 'AWS', + 'Description': 'Application description', + 'Name': 'application-name', + 'ParameterDefinitions': [{ + 'Name': 'Parameter1', + 'ReferencedByResources': ['resource1'], + 'Type': 'String' + }], + 'SemanticVersion': SemanticVersion + } + return message + + +def mock_get_cloud_formation_template(ApplicationId=None, TemplateId=None): + message = { + 'ApplicationId': ApplicationId, + 'SemanticVersion': '1.0.0', + 'Status': STATUS_ACTIVE, + 'TemplateId': TemplateId, + 'TemplateUrl': MOCK_TEMPLATE_URL + } + return message + +def mock_get_region(self, service_name, region_name): + return 'us-east-1' + +class TestServerlessAppPlugin_init(TestCase): + + def setUp(self): + client = boto3.client('serverlessrepo', region_name='us-east-1') + self.plugin = ServerlessAppPlugin(sar_client=client) + + def test_plugin_must_setup_correct_name(self): + # Name is the class name + expected_name = "ServerlessAppPlugin" + self.assertEquals(self.plugin.name, expected_name) + + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) + def test_plugin_default_values(self): + self.assertEquals(self.plugin._wait_for_template_active_status, False) + self.assertEquals(self.plugin._validate_only, False) + self.assertTrue(self.plugin._sar_client is not None) + # For some reason, `isinstance` or comparing classes did not work here + self.assertEquals(str(self.plugin._sar_client.__class__), str(boto3.client('serverlessrepo').__class__)) + + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) + def test_plugin_accepts_different_sar_client(self): + client = boto3.client('serverlessrepo', endpoint_url = 'https://example.com') + self.plugin = ServerlessAppPlugin(sar_client=client) + self.assertEquals(self.plugin._sar_client, client) + self.assertEquals(self.plugin._sar_client._endpoint, client._endpoint) + + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) + def test_plugin_accepts_flags(self): + self.plugin = ServerlessAppPlugin(wait_for_template_active_status=True) + self.assertEquals(self.plugin._wait_for_template_active_status, True) + + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) + def test_plugin_invalid_configuration_raises_exception(self): + with self.assertRaises(InvalidPluginException): + plugin = ServerlessAppPlugin(wait_for_template_active_status=True, validate_only=True) + + +class TestServerlessAppPlugin_on_before_transform_template_translate(TestCase): + + + def setUp(self): + client = boto3.client('serverlessrepo', region_name='us-east-1') + self.plugin = ServerlessAppPlugin(sar_client=client) + + @patch("samtranslator.plugins.application.serverless_app_plugin.SamTemplate") + @patch('botocore.client.BaseClient._make_api_call', mock_create_cloud_formation_template) + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) + def test_must_process_applications(self, SamTemplateMock): + + self.plugin = ServerlessAppPlugin(sar_client=boto3.client('serverlessrepo')) + template_dict = {"a": "b"} + app_resources = [("id1", ApplicationResource(app_id = 'id1')), ("id2", ApplicationResource(app_id='id2')), ("id3", ApplicationResource())] + + sam_template = Mock() + SamTemplateMock.return_value = sam_template + sam_template.iterate = Mock() + sam_template.iterate.return_value = app_resources + + self.plugin.on_before_transform_template(template_dict) + + SamTemplateMock.assert_called_with(template_dict) + + # Make sure this is called only for Apis + sam_template.iterate.assert_called_with("AWS::Serverless::Application") + + + @patch("samtranslator.plugins.application.serverless_app_plugin.SamTemplate") + @patch('botocore.client.BaseClient._make_api_call', mock_get_application) + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) + def test_must_process_applications_validate(self, SamTemplateMock): + + self.plugin = ServerlessAppPlugin(validate_only=True) + template_dict = {"a": "b"} + app_resources = [("id1", ApplicationResource(app_id = 'id1')), ("id2", ApplicationResource(app_id='id2')), ("id3", ApplicationResource())] + + sam_template = Mock() + SamTemplateMock.return_value = sam_template + sam_template.iterate = Mock() + sam_template.iterate.return_value = app_resources + + self.plugin.on_before_transform_template(template_dict) + + SamTemplateMock.assert_called_with(template_dict) + + # Make sure this is called only for Apis + sam_template.iterate.assert_called_with("AWS::Serverless::Application") + + + @patch("samtranslator.plugins.application.serverless_app_plugin.SamTemplate") + @patch('botocore.client.BaseClient._make_api_call', mock_create_cloud_formation_template) + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) + def test_process_invalid_applications(self, SamTemplateMock): + self.plugin = ServerlessAppPlugin(sar_client=boto3.client('serverlessrepo', region_name='us-east-1')) + template_dict = {"a": "b"} + app_resources = [("id1", ApplicationResource(app_id = '')), ("id2", ApplicationResource(app_id=None))] + + sam_template = Mock() + SamTemplateMock.return_value = sam_template + sam_template.iterate = Mock() + sam_template.iterate.return_value = app_resources + + self.plugin.on_before_transform_template(template_dict) + + SamTemplateMock.assert_called_with(template_dict) + + # Make sure this is called only for Apis + sam_template.iterate.assert_called_with("AWS::Serverless::Application") + + + @patch("samtranslator.plugins.application.serverless_app_plugin.SamTemplate") + @patch('botocore.client.BaseClient._make_api_call', mock_get_application) + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) + def test_process_invalid_applications_validate(self, SamTemplateMock): + self.plugin = ServerlessAppPlugin(validate_only=True) + template_dict = {"a": "b"} + app_resources = [("id1", ApplicationResource(app_id = '')), ("id2", ApplicationResource(app_id=None))] + + sam_template = Mock() + SamTemplateMock.return_value = sam_template + sam_template.iterate = Mock() + sam_template.iterate.return_value = app_resources + + self.plugin.on_before_transform_template(template_dict) + + SamTemplateMock.assert_called_with(template_dict) + + # Make sure this is called only for Apis + sam_template.iterate.assert_called_with("AWS::Serverless::Application") + + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) + def test_sar_service_calls(self): + service_call_lambda = mock_get_application + logical_id = 'logical_id' + app_id = 'app_id' + semver = '1.0.0' + response = self.plugin._sar_service_call(service_call_lambda, logical_id, app_id, semver) + self.assertEquals(app_id, response['ApplicationId']) + + +class ApplicationResource(object): + def __init__(self, app_id='app_id', semver='1.3.5'): + self.properties = { + 'ApplicationId': app_id, + 'SemanticVersion': semver + } + + + + + + + + +#class TestServerlessAppPlugin_on_before_transform_resource(TestCase): + +# def setUp(self): +# self.plugin = ServerlessAppPlugin() + + # TODO: test this lifecycle event + + # @parameterized.expand( + # itertools.product([ + # ServerlessAppPlugin(), + # ServerlessAppPlugin(wait_for_template_active_status=True), + # ]) + # ) + # @patch("samtranslator.plugins.application.serverless_app_plugin.SamTemplate") + # @patch('botocore.client.BaseClient._make_api_call', mock_create_cloud_formation_template) + # def test_process_invalid_applications(self, plugin, SamTemplateMock): + # self.plugin = plugin + # template_dict = {"a": "b"} + # app_resources = [("id1", ApplicationResource(app_id = '')), ("id2", ApplicationResource(app_id=None))] + + # sam_template = Mock() + # SamTemplateMock.return_value = sam_template + # sam_template.iterate = Mock() + # sam_template.iterate.return_value = app_resources + + # self.plugin.on_before_transform_template(template_dict) + + # self.plugin.on_before_transform_resource(app_resources[0][0], 'AWS::Serverless::Application', app_resources[0][1].properties) + +# class TestServerlessAppPlugin_on_after_transform_template(TestCase): + +# def setUp(self): +# self.plugin = SeverlessAppPlugin() + +# # TODO: test this lifecycle event diff --git a/tests/sdk/test_template.py b/tests/sdk/test_template.py index e5677993d..0d2902889 100644 --- a/tests/sdk/test_template.py +++ b/tests/sdk/test_template.py @@ -1,4 +1,5 @@ from unittest import TestCase +from six import assertCountEqual from samtranslator.sdk.template import SamTemplate from samtranslator.sdk.resource import SamResource @@ -26,6 +27,9 @@ def setUp(self): "Api": { "Type": "AWS::Serverless::Api" }, + "Layer": { + "Type": "AWS::Serverless::LayerVersion" + }, "NonSam": { "Type": "AWS::Lambda::Function" } @@ -39,10 +43,11 @@ def test_iterate_must_yield_sam_resources_only(self): ("Function1", {"Type": "AWS::Serverless::Function", "DependsOn": "SomeOtherResource", "Properties": {}}), ("Function2", {"Type": "AWS::Serverless::Function", "a": "b", "Properties": {}}), ("Api", {"Type": "AWS::Serverless::Api", "Properties": {}}), + ("Layer", {"Type": "AWS::Serverless::LayerVersion", "Properties": {}}), ] actual = [(id, resource.to_dict()) for id, resource in template.iterate()] - self.assertEquals(expected, actual) + assertCountEqual(self, expected, actual) def test_iterate_must_filter_by_resource_type(self): @@ -57,6 +62,17 @@ def test_iterate_must_filter_by_resource_type(self): actual = [(id, resource.to_dict()) for id, resource in template.iterate(type)] self.assertEquals(expected, actual) + def test_iterate_must_filter_by_layers_resource_type(self): + template = SamTemplate(self.template_dict) + + type = "AWS::Serverless::LayerVersion" + expected = [ + ("Layer", {"Type": "AWS::Serverless::LayerVersion", "Properties": {}}), + ] + + actual = [(id, resource.to_dict()) for id, resource in template.iterate(type)] + self.assertEquals(expected, actual) + def test_iterate_must_not_return_non_sam_resources_with_filter(self): template = SamTemplate(self.template_dict) diff --git a/tests/translator/input/application_preparing_state.yaml b/tests/translator/input/application_preparing_state.yaml new file mode 100644 index 000000000..91feca133 --- /dev/null +++ b/tests/translator/input/application_preparing_state.yaml @@ -0,0 +1,7 @@ +Resources: + PreparingApplication: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: preparing + SemanticVersion: 1.0.2 diff --git a/tests/translator/input/basic_application.yaml b/tests/translator/input/basic_application.yaml new file mode 100644 index 000000000..b68508814 --- /dev/null +++ b/tests/translator/input/basic_application.yaml @@ -0,0 +1,28 @@ +Resources: + BasicApplication: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: arn:aws:serverlessrepo:us-east-1:123456789012:applications/hello-world + SemanticVersion: 1.0.2 + + NormalApplication: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: arn:aws:serverlessrepo:us-east-1:123456789012:applications/hello-world + SemanticVersion: 1.0.2 + Tags: + TagName: TagValue + Parameters: + IdentityNameParameter: IdentityName + NotificationArns: + - arn:aws:sns:us-east-1:123456789012:sns-arn + TimeoutInMinutes: 15 + + ApplicationWithLocationUrl: + Type: 'AWS::Serverless::Application' + Properties: + Location: https://s3-us-east-1.amazonaws.com/demo-bucket/template.yaml + Tags: + TagName2: TagValue2 \ No newline at end of file diff --git a/tests/translator/input/basic_layer.yaml b/tests/translator/input/basic_layer.yaml new file mode 100644 index 000000000..2e7c95cd5 --- /dev/null +++ b/tests/translator/input/basic_layer.yaml @@ -0,0 +1,26 @@ +Resources: + MinimalLayer: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + + LayerWithContentUriObject: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: + Bucket: somebucket + Key: somekey + Version: 1 + RetentionPolicy: Delete + + CompleteLayer: + Type: 'AWS::Serverless::LayerVersion' + Properties: + LayerName: MyAwesomeLayer + ContentUri: s3://sam-demo-bucket/layer.zip + Description: Starter Lambda Layer + CompatibleRuntimes: + - python3.6 + - python2.7 + LicenseInfo: "License information" + RetentionPolicy: Retain diff --git a/tests/translator/input/error_application_does_not_exist.yaml b/tests/translator/input/error_application_does_not_exist.yaml new file mode 100644 index 000000000..35a9f3af0 --- /dev/null +++ b/tests/translator/input/error_application_does_not_exist.yaml @@ -0,0 +1,7 @@ +Resources: + MyApplication: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: non-existent + SemanticVersion: 1.0.0 \ No newline at end of file diff --git a/tests/translator/input/error_application_no_access.yaml b/tests/translator/input/error_application_no_access.yaml new file mode 100644 index 000000000..fbdae4666 --- /dev/null +++ b/tests/translator/input/error_application_no_access.yaml @@ -0,0 +1,28 @@ +Resources: + NoAccess: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: no-access + SemanticVersion: 1.0.0 + + NonExistent: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: non-existent + SemanticVersion: 1.0.0 + + InvalidSemver: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: invalid-semver + SemanticVersion: 1.0.0 + + ExpiredApplication: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: expired + SemanticVersion: 1.0.0 \ No newline at end of file diff --git a/tests/translator/input/error_application_preparing_timeout.yaml b/tests/translator/input/error_application_preparing_timeout.yaml new file mode 100644 index 000000000..ecefd392b --- /dev/null +++ b/tests/translator/input/error_application_preparing_timeout.yaml @@ -0,0 +1,7 @@ +Resources: + MyApplication: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: preparing-never-ready + SemanticVersion: 1.0.0 \ No newline at end of file diff --git a/tests/translator/input/error_application_properties.yaml b/tests/translator/input/error_application_properties.yaml new file mode 100644 index 000000000..8914e099d --- /dev/null +++ b/tests/translator/input/error_application_properties.yaml @@ -0,0 +1,36 @@ +Resources: + NormalApplication: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: 1 + SemanticVersion: 2.0.0 + + MissingApplicationId: + Type: 'AWS::Serverless::Application' + Properties: + Location: + SemanticVersion: 2.0.0 + + MissingSemanticVersion: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: application-id + + MissingLocation: + Type: 'AWS::Serverless::Application' + Properties: + TimeoutInMinutes: 10 + + UnsupportedProperty: + Type: 'AWS::Serverless::Application' + Properties: + TemplateUrl: template-url + + BlankProperties: + Type: 'AWS::Serverless::Application' + Properties: + Location: + ApplicationId: + SemanticVersion: \ No newline at end of file diff --git a/tests/translator/input/error_function_invalid_layer.yaml b/tests/translator/input/error_function_invalid_layer.yaml new file mode 100644 index 000000000..f290bd56d --- /dev/null +++ b/tests/translator/input/error_function_invalid_layer.yaml @@ -0,0 +1,8 @@ +Resources: + FunctionWithLayersString: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Layers: arn:aws:lambda:us-east-1:123456789101:layer:CorpXLayer:1 \ No newline at end of file diff --git a/tests/translator/input/error_layer_invalid_properties.yaml b/tests/translator/input/error_layer_invalid_properties.yaml new file mode 100644 index 000000000..606530d6a --- /dev/null +++ b/tests/translator/input/error_layer_invalid_properties.yaml @@ -0,0 +1,46 @@ +Parameters: + DeletePolicy: + Default: Keep + Type: String + DeleteList: + Default: + - Retain + Type: List + +Resources: + LayerWithRuntimesString: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + CompatibleRuntimes: "strings are not allowed" + + LayerWithLicenseInfoList: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + LicenseInfo: + - "lists also" + - "are not allowed" + + LayerWithNoContentUri: + Type: 'AWS::Serverless::LayerVersion' + Properties: + Description: No content uri + + LayerWithRetentionPolicy: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + RetentionPolicy: ErrorParam + + LayerWithRetentionPolicyParam: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + RetentionPolicy: !Ref DeletePolicy + + LayerWithRetentionPolicyListParam: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + RetentionPolicy: !Ref DeleteList \ No newline at end of file diff --git a/tests/translator/input/error_reserved_sam_tag.yaml b/tests/translator/input/error_reserved_sam_tag.yaml index c9ea74288..d3e29df60 100644 --- a/tests/translator/input/error_reserved_sam_tag.yaml +++ b/tests/translator/input/error_reserved_sam_tag.yaml @@ -19,4 +19,11 @@ Resources: Tags: lambda:createdBy: "blah" TagKey2: "" - + SomeApplication: + Type: AWS::Serverless::Application + Properties: + Location: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world + SemanticVersion: 1.0.2 + Tags: + lambda:createdBy: "me" diff --git a/tests/translator/input/function_with_global_layers.yaml b/tests/translator/input/function_with_global_layers.yaml new file mode 100644 index 000000000..2b4d6eb32 --- /dev/null +++ b/tests/translator/input/function_with_global_layers.yaml @@ -0,0 +1,19 @@ +Globals: + Function: + Layers: + - arn:aws:lambda:us-east-1:123456789101:layer:layer1:1 + - arn:aws:lambda:us-east-1:123456789101:layer:layer2:1 + +# Note: there is a limit to the number of layers that a function can reference. +Resources: + ManyLayersFunc: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.6 + Layers: + - arn:aws:lambda:us-east-1:123456789101:layer:layer3:1 + - arn:aws:lambda:us-east-1:123456789101:layer:layer4:1 + - arn:aws:lambda:us-east-1:123456789101:layer:layer5:1 + diff --git a/tests/translator/input/function_with_layers.yaml b/tests/translator/input/function_with_layers.yaml new file mode 100644 index 000000000..e9b0b9a4e --- /dev/null +++ b/tests/translator/input/function_with_layers.yaml @@ -0,0 +1,42 @@ +Resources: + MinimalLayerFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Layers: + - arn:aws:lambda:us-east-1:123456789101:layer:CorpXLayer:1 + + FunctionNoLayerVersion: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Layers: + - arn:aws:lambda:us-east-1:123456789101:layer:CorpXLayer:1 + + FunctionLayerWithSubIntrinsic: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Layers: + - !Sub arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:CorpXLayer:1 + - Fn::Sub: arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:CorpYLayer:1 + + FunctionReferencesLayer: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Layers: + - !Ref MyLayer + + MyLayer: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip diff --git a/tests/translator/input/function_with_many_layers.yaml b/tests/translator/input/function_with_many_layers.yaml new file mode 100644 index 000000000..0aff4d87a --- /dev/null +++ b/tests/translator/input/function_with_many_layers.yaml @@ -0,0 +1,18 @@ +Resources: + ManyLayersFunc: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Layers: + - arn:aws:lambda:us-east-1:123456789101:layer:z:1 + - !Sub arn:aws:lambda:${AWS:Region}:123456789101:layer:a:1 + - arn:aws:lambda:us-east-1:123456789101:layer:d12345678:1 + - !Sub arn:${AWS:Partition}:lambda:${AWS:Region}:123456789101:layer:c:1 + - !Ref MyLayer + + MyLayer: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip diff --git a/tests/translator/input/globals_for_function.yaml b/tests/translator/input/globals_for_function.yaml index 29085280e..202c32eac 100644 --- a/tests/translator/input/globals_for_function.yaml +++ b/tests/translator/input/globals_for_function.yaml @@ -16,6 +16,8 @@ Globals: tag1: value1 Tracing: Active AutoPublishAlias: live + Layers: + - !Sub arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer:1 Resources: MinimalFunction: @@ -39,4 +41,6 @@ Resources: newtag1: newvalue1 Tracing: PassThrough AutoPublishAlias: prod + Layers: + - !Sub arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer2:2 diff --git a/tests/translator/input/layers_all_properties.yaml b/tests/translator/input/layers_all_properties.yaml new file mode 100644 index 000000000..a9cd53a73 --- /dev/null +++ b/tests/translator/input/layers_all_properties.yaml @@ -0,0 +1,40 @@ +Parameters: + LayerDeleteParam: + Type: String + Default: Delete + +Resources: + MyLayer: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://bucket/key + RetentionPolicy: !Ref LayerDeleteParam + + MyLayerWithAName: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://bucket/key + LayerName: DifferentLayerName + + MyFunction: + Type: "AWS::Serverless::Function" + Properties: + CodeUri: s3://bucket/key + Handler: app.handler + Runtime: python3.6 + Layers: + - !Ref MyLayer + +Outputs: + LayerName: + Value: !Ref MyLayer + FunctionName: + Value: !Ref MyFunction + LayerAtt: # For testing purposes only + Value: !GetAtt MyLayer.Arn + FunctionAtt: + Value: !GetAtt MyFunction.Arn + LayerSub: + Value: !Sub ${MyLayer} + FunctionSub: + Value: !Sub ${MyFunction} diff --git a/tests/translator/input/layers_with_intrinsics.yaml b/tests/translator/input/layers_with_intrinsics.yaml new file mode 100644 index 000000000..5a5978d24 --- /dev/null +++ b/tests/translator/input/layers_with_intrinsics.yaml @@ -0,0 +1,20 @@ +Parameters: + LayerLicenseInfo: + Type: String + Default: MIT-0 License + LayerRuntimeList: + Type: CommaDelimitedList + +Resources: + LayerWithLicenseIntrinsic: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + LicenseInfo: !Ref LayerLicenseInfo + + LayerWithRuntimesIntrinsic: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + CompatibleRuntimes: + Ref: LayerRuntimeList \ No newline at end of file diff --git a/tests/translator/output/application_preparing_state.json b/tests/translator/output/application_preparing_state.json new file mode 100644 index 000000000..92fe7cc8d --- /dev/null +++ b/tests/translator/output/application_preparing_state.json @@ -0,0 +1,24 @@ +{ + "Resources": { + "PreparingApplication": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://awsserverlessrepo-changesets-xxx.s3.amazonaws.com/signed-url", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "preparing", + "Key": "serverlessrepo:applicationId" + }, + { + "Value": "1.0.2", + "Key": "serverlessrepo:semanticVersion" + } + ] + } + } + } +} diff --git a/tests/translator/output/aws-cn/application_preparing_state.json b/tests/translator/output/aws-cn/application_preparing_state.json new file mode 100644 index 000000000..92fe7cc8d --- /dev/null +++ b/tests/translator/output/aws-cn/application_preparing_state.json @@ -0,0 +1,24 @@ +{ + "Resources": { + "PreparingApplication": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://awsserverlessrepo-changesets-xxx.s3.amazonaws.com/signed-url", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "preparing", + "Key": "serverlessrepo:applicationId" + }, + { + "Value": "1.0.2", + "Key": "serverlessrepo:semanticVersion" + } + ] + } + } + } +} diff --git a/tests/translator/output/aws-cn/basic_application.json b/tests/translator/output/aws-cn/basic_application.json new file mode 100644 index 000000000..bbdb760ad --- /dev/null +++ b/tests/translator/output/aws-cn/basic_application.json @@ -0,0 +1,73 @@ +{ + "Resources": { + "BasicApplication": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://awsserverlessrepo-changesets-xxx.s3.amazonaws.com/signed-url", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "arn:aws:serverlessrepo:us-east-1:123456789012:applications/hello-world", + "Key": "serverlessrepo:applicationId" + }, + { + "Value": "1.0.2", + "Key": "serverlessrepo:semanticVersion" + } + ] + } + }, + "NormalApplication": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://awsserverlessrepo-changesets-xxx.s3.amazonaws.com/signed-url", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "arn:aws:serverlessrepo:us-east-1:123456789012:applications/hello-world", + "Key": "serverlessrepo:applicationId" + }, + { + "Value": "1.0.2", + "Key": "serverlessrepo:semanticVersion" + }, + { + "Value": "TagValue", + "Key": "TagName" + } + ], + "Parameters": + { + "IdentityNameParameter": "IdentityName" + }, + "NotificationArns": + [ + "arn:aws:sns:us-east-1:123456789012:sns-arn" + ], + "TimeoutInMinutes": 15 + } + }, + "ApplicationWithLocationUrl": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://s3-us-east-1.amazonaws.com/demo-bucket/template.yaml", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "TagValue2", + "Key": "TagName2" + } + ] + } + } + } +} diff --git a/tests/translator/output/aws-cn/basic_layer.json b/tests/translator/output/aws-cn/basic_layer.json new file mode 100644 index 000000000..af2794614 --- /dev/null +++ b/tests/translator/output/aws-cn/basic_layer.json @@ -0,0 +1,44 @@ +{ + "Resources": { + "MinimalLayer0c7f96cce7": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "LayerName": "MinimalLayer", + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + } + } + }, + "CompleteLayer5d71a60e81": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MyAwesomeLayer", + "Description": "Starter Lambda Layer", + "CompatibleRuntimes": [ + "python3.6", + "python2.7" + ], + "LicenseInfo": "License information" + } + }, + "LayerWithContentUriObjectac002ba767": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Delete", + "Properties": { + "LayerName": "LayerWithContentUriObject", + "Content": { + "S3Bucket": "somebucket", + "S3Key": "somekey", + "S3ObjectVersion": 1 + } + } + } + } +} diff --git a/tests/translator/output/aws-cn/function_with_global_layers.json b/tests/translator/output/aws-cn/function_with_global_layers.json new file mode 100644 index 000000000..a958c11d8 --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_global_layers.json @@ -0,0 +1,58 @@ +{ + "Resources": { + "ManyLayersFuncRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ManyLayersFunc": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ManyLayersFuncRole", + "Arn" + ] + }, + "Runtime": "python3.6", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:layer1:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer2:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer3:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer4:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer5:1" + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/function_with_layers.json b/tests/translator/output/aws-cn/function_with_layers.json new file mode 100644 index 000000000..5d26c87e0 --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_layers.json @@ -0,0 +1,216 @@ +{ + "Resources": { + "MinimalLayerFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "MinimalLayerFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalLayerFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:CorpXLayer:1" + ] + } + }, + "FunctionNoLayerVersionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "FunctionNoLayerVersion": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionNoLayerVersionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:CorpXLayer:1" + ] + } + }, + "FunctionLayerWithSubIntrinsicRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "FunctionLayerWithSubIntrinsic": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionLayerWithSubIntrinsicRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + {"Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:CorpXLayer:1"}, + {"Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:CorpYLayer:1"} + ] + } + }, + "MyLayera5167acaba": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "LayerName": "MyLayer", + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + } + } + }, + "FunctionReferencesLayerRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "FunctionReferencesLayer": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionReferencesLayerRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + { "Ref": "MyLayera5167acaba" } + ] + } + } + } +} diff --git a/tests/translator/output/aws-cn/function_with_many_layers.json b/tests/translator/output/aws-cn/function_with_many_layers.json new file mode 100644 index 000000000..a839dd5af --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_many_layers.json @@ -0,0 +1,69 @@ +{ + "Resources": { + "ManyLayersFuncRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ManyLayersFunc": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ManyLayersFuncRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:z:1", + { "Fn::Sub": "arn:aws:lambda:${AWS:Region}:123456789101:layer:a:1" }, + "arn:aws:lambda:us-east-1:123456789101:layer:d12345678:1", + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:123456789101:layer:c:1" }, + { "Ref": "MyLayera5167acaba" } + ] + } + }, + "MyLayera5167acaba": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "LayerName": "MyLayer", + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/globals_for_function.json b/tests/translator/output/aws-cn/globals_for_function.json index 4a5c5cd0f..82f9da5a5 100644 --- a/tests/translator/output/aws-cn/globals_for_function.json +++ b/tests/translator/output/aws-cn/globals_for_function.json @@ -71,7 +71,11 @@ ] }, "Timeout": 100, - "Runtime": "nodejs4.3" + "Runtime": "nodejs4.3", + "Layers": [ + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer:1" }, + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer2:2" } + ] } }, "MinimalFunctionRole": { @@ -154,7 +158,10 @@ ] }, "Timeout": 30, - "Runtime": "python2.7" + "Runtime": "python2.7", + "Layers": [ + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer:1" } + ] } }, "MinimalFunctionAliaslive": { diff --git a/tests/translator/output/aws-cn/layers_all_properties.json b/tests/translator/output/aws-cn/layers_all_properties.json new file mode 100644 index 000000000..72b43d607 --- /dev/null +++ b/tests/translator/output/aws-cn/layers_all_properties.json @@ -0,0 +1,122 @@ +{ + "Parameters": { + "LayerDeleteParam": { + "Default": "Delete", + "Type": "String" + } + }, + "Resources": { + "MyFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "Handler": "app.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.6", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ], + "Layers": [ + { + "Ref": "MyLayerd04062b365" + } + ] + } + }, + "MyFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + } + }, + "MyLayerd04062b365": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Delete", + "Properties": { + "Content": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "LayerName": "MyLayer" + } + }, + "MyLayerWithANamefda8c9ec8c": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "LayerName": "DifferentLayerName" + } + } + }, + "Outputs": { + "LayerName": { + "Value": { + "Ref": "MyLayerd04062b365" + } + }, + "FunctionName": { + "Value": { + "Ref": "MyFunction" + } + }, + "LayerAtt": { + "Value": { + "Fn::GetAtt": [ + "MyLayerd04062b365", + "Arn" + ] + } + }, + "FunctionAtt": { + "Value": { + "Fn::GetAtt": [ + "MyFunction", + "Arn" + ] + } + }, + "LayerSub": { + "Value": { + "Fn::Sub": "${MyLayerd04062b365}" + } + }, + "FunctionSub": { + "Value": { + "Fn::Sub": "${MyFunction}" + } + } + } +} diff --git a/tests/translator/output/aws-cn/layers_with_intrinsics.json b/tests/translator/output/aws-cn/layers_with_intrinsics.json new file mode 100644 index 000000000..a82cc4355 --- /dev/null +++ b/tests/translator/output/aws-cn/layers_with_intrinsics.json @@ -0,0 +1,41 @@ +{ + "Parameters": { + "LayerLicenseInfo": { + "Type": "String", + "Default": "MIT-0 License" + }, + "LayerRuntimeList": { + "Type": "CommaDelimitedList" + } + }, + "Resources": { + "LayerWithLicenseIntrinsic16287c50c8": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "LayerWithLicenseIntrinsic", + "LicenseInfo": { + "Ref": "LayerLicenseInfo" + } + } + }, + "LayerWithRuntimesIntrinsic1a006faa85": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "LayerWithRuntimesIntrinsic", + "CompatibleRuntimes": { + "Ref": "LayerRuntimeList" + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/application_preparing_state.json b/tests/translator/output/aws-us-gov/application_preparing_state.json new file mode 100644 index 000000000..92fe7cc8d --- /dev/null +++ b/tests/translator/output/aws-us-gov/application_preparing_state.json @@ -0,0 +1,24 @@ +{ + "Resources": { + "PreparingApplication": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://awsserverlessrepo-changesets-xxx.s3.amazonaws.com/signed-url", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "preparing", + "Key": "serverlessrepo:applicationId" + }, + { + "Value": "1.0.2", + "Key": "serverlessrepo:semanticVersion" + } + ] + } + } + } +} diff --git a/tests/translator/output/aws-us-gov/basic_application.json b/tests/translator/output/aws-us-gov/basic_application.json new file mode 100644 index 000000000..bbdb760ad --- /dev/null +++ b/tests/translator/output/aws-us-gov/basic_application.json @@ -0,0 +1,73 @@ +{ + "Resources": { + "BasicApplication": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://awsserverlessrepo-changesets-xxx.s3.amazonaws.com/signed-url", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "arn:aws:serverlessrepo:us-east-1:123456789012:applications/hello-world", + "Key": "serverlessrepo:applicationId" + }, + { + "Value": "1.0.2", + "Key": "serverlessrepo:semanticVersion" + } + ] + } + }, + "NormalApplication": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://awsserverlessrepo-changesets-xxx.s3.amazonaws.com/signed-url", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "arn:aws:serverlessrepo:us-east-1:123456789012:applications/hello-world", + "Key": "serverlessrepo:applicationId" + }, + { + "Value": "1.0.2", + "Key": "serverlessrepo:semanticVersion" + }, + { + "Value": "TagValue", + "Key": "TagName" + } + ], + "Parameters": + { + "IdentityNameParameter": "IdentityName" + }, + "NotificationArns": + [ + "arn:aws:sns:us-east-1:123456789012:sns-arn" + ], + "TimeoutInMinutes": 15 + } + }, + "ApplicationWithLocationUrl": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://s3-us-east-1.amazonaws.com/demo-bucket/template.yaml", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "TagValue2", + "Key": "TagName2" + } + ] + } + } + } +} diff --git a/tests/translator/output/aws-us-gov/basic_layer.json b/tests/translator/output/aws-us-gov/basic_layer.json new file mode 100644 index 000000000..af2794614 --- /dev/null +++ b/tests/translator/output/aws-us-gov/basic_layer.json @@ -0,0 +1,44 @@ +{ + "Resources": { + "MinimalLayer0c7f96cce7": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "LayerName": "MinimalLayer", + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + } + } + }, + "CompleteLayer5d71a60e81": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MyAwesomeLayer", + "Description": "Starter Lambda Layer", + "CompatibleRuntimes": [ + "python3.6", + "python2.7" + ], + "LicenseInfo": "License information" + } + }, + "LayerWithContentUriObjectac002ba767": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Delete", + "Properties": { + "LayerName": "LayerWithContentUriObject", + "Content": { + "S3Bucket": "somebucket", + "S3Key": "somekey", + "S3ObjectVersion": 1 + } + } + } + } +} diff --git a/tests/translator/output/aws-us-gov/function_with_global_layers.json b/tests/translator/output/aws-us-gov/function_with_global_layers.json new file mode 100644 index 000000000..7f9fea7e8 --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_global_layers.json @@ -0,0 +1,58 @@ +{ + "Resources": { + "ManyLayersFuncRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ManyLayersFunc": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ManyLayersFuncRole", + "Arn" + ] + }, + "Runtime": "python3.6", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:layer1:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer2:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer3:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer4:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer5:1" + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/function_with_layers.json b/tests/translator/output/aws-us-gov/function_with_layers.json new file mode 100644 index 000000000..763b6670a --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_layers.json @@ -0,0 +1,216 @@ +{ + "Resources": { + "MinimalLayerFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "MinimalLayerFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalLayerFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:CorpXLayer:1" + ] + } + }, + "FunctionNoLayerVersionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "FunctionNoLayerVersion": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionNoLayerVersionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:CorpXLayer:1" + ] + } + }, + "FunctionLayerWithSubIntrinsicRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "FunctionLayerWithSubIntrinsic": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionLayerWithSubIntrinsicRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + {"Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:CorpXLayer:1"}, + {"Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:CorpYLayer:1"} + ] + } + }, + "MyLayera5167acaba": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "LayerName": "MyLayer", + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + } + } + }, + "FunctionReferencesLayerRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "FunctionReferencesLayer": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionReferencesLayerRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + { "Ref": "MyLayera5167acaba" } + ] + } + } + } +} diff --git a/tests/translator/output/aws-us-gov/function_with_many_layers.json b/tests/translator/output/aws-us-gov/function_with_many_layers.json new file mode 100644 index 000000000..4e2fbf402 --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_many_layers.json @@ -0,0 +1,69 @@ +{ + "Resources": { + "ManyLayersFuncRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ManyLayersFunc": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ManyLayersFuncRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:z:1", + { "Fn::Sub": "arn:aws:lambda:${AWS:Region}:123456789101:layer:a:1" }, + "arn:aws:lambda:us-east-1:123456789101:layer:d12345678:1", + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:123456789101:layer:c:1" }, + { "Ref": "MyLayera5167acaba" } + ] + } + }, + "MyLayera5167acaba": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "LayerName": "MyLayer", + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + } + } + } + } +} \ No newline at end of file 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 a66321936..ad68dde1b 100644 --- a/tests/translator/output/aws-us-gov/globals_for_function.json +++ b/tests/translator/output/aws-us-gov/globals_for_function.json @@ -71,7 +71,11 @@ ] }, "Timeout": 100, - "Runtime": "nodejs4.3" + "Runtime": "nodejs4.3", + "Layers": [ + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer:1" }, + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer2:2" } + ] } }, "MinimalFunctionRole": { @@ -154,7 +158,10 @@ ] }, "Timeout": 30, - "Runtime": "python2.7" + "Runtime": "python2.7", + "Layers": [ + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer:1" } + ] } }, "MinimalFunctionAliaslive": { diff --git a/tests/translator/output/aws-us-gov/layers_all_properties.json b/tests/translator/output/aws-us-gov/layers_all_properties.json new file mode 100644 index 000000000..a6455d6cb --- /dev/null +++ b/tests/translator/output/aws-us-gov/layers_all_properties.json @@ -0,0 +1,122 @@ +{ + "Parameters": { + "LayerDeleteParam": { + "Default": "Delete", + "Type": "String" + } + }, + "Resources": { + "MyFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "Handler": "app.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.6", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ], + "Layers": [ + { + "Ref": "MyLayerd04062b365" + } + ] + } + }, + "MyFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + } + }, + "MyLayerd04062b365": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Delete", + "Properties": { + "Content": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "LayerName": "MyLayer" + } + }, + "MyLayerWithANamefda8c9ec8c": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "LayerName": "DifferentLayerName" + } + } + }, + "Outputs": { + "LayerName": { + "Value": { + "Ref": "MyLayerd04062b365" + } + }, + "FunctionName": { + "Value": { + "Ref": "MyFunction" + } + }, + "LayerAtt": { + "Value": { + "Fn::GetAtt": [ + "MyLayerd04062b365", + "Arn" + ] + } + }, + "FunctionAtt": { + "Value": { + "Fn::GetAtt": [ + "MyFunction", + "Arn" + ] + } + }, + "LayerSub": { + "Value": { + "Fn::Sub": "${MyLayerd04062b365}" + } + }, + "FunctionSub": { + "Value": { + "Fn::Sub": "${MyFunction}" + } + } + } +} diff --git a/tests/translator/output/aws-us-gov/layers_with_intrinsics.json b/tests/translator/output/aws-us-gov/layers_with_intrinsics.json new file mode 100644 index 000000000..a82cc4355 --- /dev/null +++ b/tests/translator/output/aws-us-gov/layers_with_intrinsics.json @@ -0,0 +1,41 @@ +{ + "Parameters": { + "LayerLicenseInfo": { + "Type": "String", + "Default": "MIT-0 License" + }, + "LayerRuntimeList": { + "Type": "CommaDelimitedList" + } + }, + "Resources": { + "LayerWithLicenseIntrinsic16287c50c8": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "LayerWithLicenseIntrinsic", + "LicenseInfo": { + "Ref": "LayerLicenseInfo" + } + } + }, + "LayerWithRuntimesIntrinsic1a006faa85": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "LayerWithRuntimesIntrinsic", + "CompatibleRuntimes": { + "Ref": "LayerRuntimeList" + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/basic_application.json b/tests/translator/output/basic_application.json new file mode 100644 index 000000000..bbdb760ad --- /dev/null +++ b/tests/translator/output/basic_application.json @@ -0,0 +1,73 @@ +{ + "Resources": { + "BasicApplication": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://awsserverlessrepo-changesets-xxx.s3.amazonaws.com/signed-url", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "arn:aws:serverlessrepo:us-east-1:123456789012:applications/hello-world", + "Key": "serverlessrepo:applicationId" + }, + { + "Value": "1.0.2", + "Key": "serverlessrepo:semanticVersion" + } + ] + } + }, + "NormalApplication": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://awsserverlessrepo-changesets-xxx.s3.amazonaws.com/signed-url", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "arn:aws:serverlessrepo:us-east-1:123456789012:applications/hello-world", + "Key": "serverlessrepo:applicationId" + }, + { + "Value": "1.0.2", + "Key": "serverlessrepo:semanticVersion" + }, + { + "Value": "TagValue", + "Key": "TagName" + } + ], + "Parameters": + { + "IdentityNameParameter": "IdentityName" + }, + "NotificationArns": + [ + "arn:aws:sns:us-east-1:123456789012:sns-arn" + ], + "TimeoutInMinutes": 15 + } + }, + "ApplicationWithLocationUrl": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://s3-us-east-1.amazonaws.com/demo-bucket/template.yaml", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + }, + { + "Value": "TagValue2", + "Key": "TagName2" + } + ] + } + } + } +} diff --git a/tests/translator/output/basic_layer.json b/tests/translator/output/basic_layer.json new file mode 100644 index 000000000..af2794614 --- /dev/null +++ b/tests/translator/output/basic_layer.json @@ -0,0 +1,44 @@ +{ + "Resources": { + "MinimalLayer0c7f96cce7": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "LayerName": "MinimalLayer", + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + } + } + }, + "CompleteLayer5d71a60e81": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MyAwesomeLayer", + "Description": "Starter Lambda Layer", + "CompatibleRuntimes": [ + "python3.6", + "python2.7" + ], + "LicenseInfo": "License information" + } + }, + "LayerWithContentUriObjectac002ba767": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Delete", + "Properties": { + "LayerName": "LayerWithContentUriObject", + "Content": { + "S3Bucket": "somebucket", + "S3Key": "somekey", + "S3ObjectVersion": 1 + } + } + } + } +} diff --git a/tests/translator/output/error_application_does_not_exist.json b/tests/translator/output/error_application_does_not_exist.json new file mode 100644 index 000000000..9c5be4471 --- /dev/null +++ b/tests/translator/output/error_application_does_not_exist.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [MyApplication] is invalid. Cannot access application: non-existent." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyApplication] is invalid. Cannot access application: non-existent." +} \ No newline at end of file diff --git a/tests/translator/output/error_application_no_access.json b/tests/translator/output/error_application_no_access.json new file mode 100644 index 000000000..7900c2731 --- /dev/null +++ b/tests/translator/output/error_application_no_access.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [InvalidSemver] is invalid. Cannot access application: invalid-semver. Resource with id [NoAccess] is invalid. Cannot access application: no-access. Resource with id [NonExistent] is invalid. Cannot access application: non-existent." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 3. Resource with id [InvalidSemver] is invalid. Cannot access application: invalid-semver. Resource with id [NoAccess] is invalid. Cannot access application: no-access. Resource with id [NonExistent] is invalid. Cannot access application: non-existent." +} \ No newline at end of file diff --git a/tests/translator/output/error_application_preparing_timeout.json b/tests/translator/output/error_application_preparing_timeout.json new file mode 100644 index 000000000..58ac8e821 --- /dev/null +++ b/tests/translator/output/error_application_preparing_timeout.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [['preparing-never-ready']] is invalid. Timed out waiting for nested stack templates to reach ACTIVE status." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [['preparing-never-ready']] is invalid. Timed out waiting for nested stack templates to reach ACTIVE status." +} diff --git a/tests/translator/output/error_application_properties.json b/tests/translator/output/error_application_properties.json new file mode 100644 index 000000000..4bd94227b --- /dev/null +++ b/tests/translator/output/error_application_properties.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [BlankProperties] is invalid. Property 'ApplicationId' cannot be blank. Resource with id [MissingApplicationId] is invalid. Resource is missing the required [ApplicationId] property. Resource with id [MissingLocation] is invalid. Resource is missing the required [Location] property. Resource with id [MissingSemanticVersion] is invalid. Resource is missing the required [SemanticVersion] property. Resource with id [NormalApplication] is invalid. Type of property 'ApplicationId' is invalid. Resource with id [UnsupportedProperty] is invalid. Resource is missing the required [Location] property." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 6. Resource with id [BlankProperties] is invalid. Property 'ApplicationId' cannot be blank. Resource with id [MissingApplicationId] is invalid. Resource is missing the required [ApplicationId] property. Resource with id [MissingLocation] is invalid. Resource is missing the required [Location] property. Resource with id [MissingSemanticVersion] is invalid. Resource is missing the required [SemanticVersion] property. Resource with id [NormalApplication] is invalid. Type of property 'ApplicationId' is invalid. Resource with id [UnsupportedProperty] is invalid. Resource is missing the required [Location] property." +} \ No newline at end of file diff --git a/tests/translator/output/error_function_invalid_layer.json b/tests/translator/output/error_function_invalid_layer.json new file mode 100644 index 000000000..94d1a11a9 --- /dev/null +++ b/tests/translator/output/error_function_invalid_layer.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [FunctionWithLayersString] is invalid. Type of property 'Layers' is invalid." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [FunctionWithLayersString] is invalid. Type of property 'Layers' is invalid." +} \ No newline at end of file diff --git a/tests/translator/output/error_globals_unsupported_property.json b/tests/translator/output/error_globals_unsupported_property.json index e1e48a1be..87a48e517 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', 'DeploymentPreference']" + "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']" } ], - "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', 'DeploymentPreference']" + "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']" } \ No newline at end of file diff --git a/tests/translator/output/error_layer_invalid_properties.json b/tests/translator/output/error_layer_invalid_properties.json new file mode 100644 index 000000000..696ca48b0 --- /dev/null +++ b/tests/translator/output/error_layer_invalid_properties.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [LayerWithLicenseInfoList] is invalid. Type of property 'LicenseInfo' is invalid. Resource with id [LayerWithNoContentUri] is invalid. Missing required property 'ContentUri'. Resource with id [LayerWithRetentionPolicy] is invalid. 'RetentionPolicy' must be one of the following options: ['Retain', 'Delete']. Resource with id [LayerWithRetentionPolicyListParam] is invalid. Could not resolve parameter for 'RetentionPolicy' or parameter is not a String. Resource with id [LayerWithRetentionPolicyParam] is invalid. 'RetentionPolicy' must be one of the following options: ['Retain', 'Delete']. Resource with id [LayerWithRuntimesString] is invalid. Type of property 'CompatibleRuntimes' is invalid." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 6. Resource with id [LayerWithLicenseInfoList] is invalid. Type of property 'LicenseInfo' is invalid. Resource with id [LayerWithNoContentUri] is invalid. Missing required property 'ContentUri'. Resource with id [LayerWithRetentionPolicy] is invalid. 'RetentionPolicy' must be one of the following options: ['Retain', 'Delete']. Resource with id [LayerWithRetentionPolicyListParam] is invalid. Could not resolve parameter for 'RetentionPolicy' or parameter is not a String. Resource with id [LayerWithRetentionPolicyParam] is invalid. 'RetentionPolicy' must be one of the following options: ['Retain', 'Delete']. Resource with id [LayerWithRuntimesString] is invalid. Type of property 'CompatibleRuntimes' is invalid." + } \ No newline at end of file diff --git a/tests/translator/output/error_reserved_sam_tag.json b/tests/translator/output/error_reserved_sam_tag.json index 2e7fdc8fb..4ff8d06bb 100644 --- a/tests/translator/output/error_reserved_sam_tag.json +++ b/tests/translator/output/error_reserved_sam_tag.json @@ -1,8 +1,8 @@ { "errors": [ { - "errorMessage": "Resource with id [AlexaSkillFunc] is invalid. lambda:createdBy is a reserved Tag key name and cannot be set on your function. Please change they tag key in the input." + "errorMessage": "Resource with id [AlexaSkillFunc] is invalid. lambda:createdBy is a reserved Tag key name and cannot be set on your resource. Please change the tag key in the input. Resource with id [SomeApplication] is invalid. stack:createdBy is a reserved Tag key name and cannot be set on your resource. Please change the tag key in the input." } ], - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [AlexaSkillFunc] is invalid. lambda:createdBy is a reserved Tag key name and cannot be set on your function. Please change they tag key in the input." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 2. Resource with id [AlexaSkillFunc] is invalid. lambda:createdBy is a reserved Tag key name and cannot be set on your resource. Please change the tag key in the input. Resource with id [SomeApplication] is invalid. lambda:createdBy is a reserved Tag key name and cannot be set on your resource. Please change the tag key in the input." } diff --git a/tests/translator/output/function_with_global_layers.json b/tests/translator/output/function_with_global_layers.json new file mode 100644 index 000000000..745ddd0cc --- /dev/null +++ b/tests/translator/output/function_with_global_layers.json @@ -0,0 +1,58 @@ +{ + "Resources": { + "ManyLayersFuncRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ManyLayersFunc": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ManyLayersFuncRole", + "Arn" + ] + }, + "Runtime": "python3.6", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:layer1:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer2:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer3:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer4:1", + "arn:aws:lambda:us-east-1:123456789101:layer:layer5:1" + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/function_with_layers.json b/tests/translator/output/function_with_layers.json new file mode 100644 index 000000000..07c058dff --- /dev/null +++ b/tests/translator/output/function_with_layers.json @@ -0,0 +1,216 @@ +{ + "Resources": { + "MinimalLayerFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "MinimalLayerFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalLayerFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:CorpXLayer:1" + ] + } + }, + "FunctionNoLayerVersionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "FunctionNoLayerVersion": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionNoLayerVersionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:CorpXLayer:1" + ] + } + }, + "FunctionLayerWithSubIntrinsicRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "FunctionLayerWithSubIntrinsic": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionLayerWithSubIntrinsicRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + {"Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:CorpXLayer:1"}, + {"Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:CorpYLayer:1"} + ] + } + }, + "MyLayera5167acaba": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "LayerName": "MyLayer", + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + } + } + }, + "FunctionReferencesLayerRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "FunctionReferencesLayer": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionReferencesLayerRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + { "Ref": "MyLayera5167acaba" } + ] + } + } + } +} diff --git a/tests/translator/output/function_with_many_layers.json b/tests/translator/output/function_with_many_layers.json new file mode 100644 index 000000000..c760894e2 --- /dev/null +++ b/tests/translator/output/function_with_many_layers.json @@ -0,0 +1,69 @@ +{ + "Resources": { + "ManyLayersFuncRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ManyLayersFunc": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ManyLayersFuncRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "Layers": [ + "arn:aws:lambda:us-east-1:123456789101:layer:z:1", + { "Fn::Sub": "arn:aws:lambda:${AWS:Region}:123456789101:layer:a:1" }, + "arn:aws:lambda:us-east-1:123456789101:layer:d12345678:1", + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:123456789101:layer:c:1" }, + { "Ref": "MyLayera5167acaba" } + ] + } + }, + "MyLayera5167acaba": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "LayerName": "MyLayer", + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/globals_for_function.json b/tests/translator/output/globals_for_function.json index 10f4487c8..051c63fbe 100644 --- a/tests/translator/output/globals_for_function.json +++ b/tests/translator/output/globals_for_function.json @@ -71,7 +71,11 @@ ] }, "Timeout": 100, - "Runtime": "nodejs4.3" + "Runtime": "nodejs4.3", + "Layers": [ + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer:1" }, + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer2:2" } + ] } }, "MinimalFunctionRole": { @@ -154,7 +158,10 @@ ] }, "Timeout": 30, - "Runtime": "python2.7" + "Runtime": "python2.7", + "Layers": [ + { "Fn::Sub": "arn:${AWS:Partition}:lambda:${AWS:Region}:${AWS:AccountId}:layer:MyLayer:1" } + ] } }, "MinimalFunctionAliaslive": { diff --git a/tests/translator/output/layers_all_properties.json b/tests/translator/output/layers_all_properties.json new file mode 100644 index 000000000..bc67f3d9f --- /dev/null +++ b/tests/translator/output/layers_all_properties.json @@ -0,0 +1,122 @@ +{ + "Parameters": { + "LayerDeleteParam": { + "Default": "Delete", + "Type": "String" + } + }, + "Resources": { + "MyFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "Handler": "app.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.6", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ], + "Layers": [ + { + "Ref": "MyLayerd04062b365" + } + ] + } + }, + "MyFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + } + }, + "MyLayerd04062b365": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Delete", + "Properties": { + "Content": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "LayerName": "MyLayer" + } + }, + "MyLayerWithANamefda8c9ec8c": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "LayerName": "DifferentLayerName" + } + } + }, + "Outputs": { + "LayerName": { + "Value": { + "Ref": "MyLayerd04062b365" + } + }, + "FunctionName": { + "Value": { + "Ref": "MyFunction" + } + }, + "LayerAtt": { + "Value": { + "Fn::GetAtt": [ + "MyLayerd04062b365", + "Arn" + ] + } + }, + "FunctionAtt": { + "Value": { + "Fn::GetAtt": [ + "MyFunction", + "Arn" + ] + } + }, + "LayerSub": { + "Value": { + "Fn::Sub": "${MyLayerd04062b365}" + } + }, + "FunctionSub": { + "Value": { + "Fn::Sub": "${MyFunction}" + } + } + } +} diff --git a/tests/translator/output/layers_with_intrinsics.json b/tests/translator/output/layers_with_intrinsics.json new file mode 100644 index 000000000..a82cc4355 --- /dev/null +++ b/tests/translator/output/layers_with_intrinsics.json @@ -0,0 +1,41 @@ +{ + "Parameters": { + "LayerLicenseInfo": { + "Type": "String", + "Default": "MIT-0 License" + }, + "LayerRuntimeList": { + "Type": "CommaDelimitedList" + } + }, + "Resources": { + "LayerWithLicenseIntrinsic16287c50c8": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "LayerWithLicenseIntrinsic", + "LicenseInfo": { + "Ref": "LayerLicenseInfo" + } + } + }, + "LayerWithRuntimesIntrinsic1a006faa85": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "LayerWithRuntimesIntrinsic", + "CompatibleRuntimes": { + "Ref": "LayerRuntimeList" + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/test_api_resource.py b/tests/translator/test_api_resource.py index febed7dd7..e25c94d2b 100644 --- a/tests/translator/test_api_resource.py +++ b/tests/translator/test_api_resource.py @@ -1,10 +1,12 @@ import json +import os from unittest import TestCase from mock import MagicMock, patch from tests.translator.helpers import get_template_parameter_values from samtranslator.translator.transform import transform from samtranslator.model.apigateway import ApiGatewayDeployment +from tests.plugins.application.test_serverless_app_plugin import mock_get_region mock_policy_loader = MagicMock() mock_policy_loader.load.return_value = { @@ -13,7 +15,7 @@ 'AWSLambdaRole': 'arn:aws:iam::aws:policy/service-role/AWSLambdaRole', } - +@patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_redeploy_explicit_api(): """ Test to verify that we will redeploy an API when Swagger document changes @@ -45,6 +47,7 @@ def test_redeploy_explicit_api(): assert updated_deployment_ids == translate_and_find_deployment_ids(manifest) +@patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_redeploy_implicit_api(): manifest = { 'Transform': 'AWS::Serverless-2016-10-31', diff --git a/tests/translator/test_function_resources.py b/tests/translator/test_function_resources.py index 2cdfae8ee..68075c4ff 100644 --- a/tests/translator/test_function_resources.py +++ b/tests/translator/test_function_resources.py @@ -1,5 +1,6 @@ from unittest import TestCase from mock import patch, Mock +import os from samtranslator.model.sam_resources import SamFunction from samtranslator.model.lambda_ import LambdaAlias, LambdaVersion, LambdaFunction from samtranslator.model.exceptions import InvalidResourceException diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 514566945..feb515a2c 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -7,12 +7,13 @@ from samtranslator.translator.translator import Translator, prepare_plugins, make_policy_template_for_function_plugin from samtranslator.parser.parser import Parser -from samtranslator.model.exceptions import InvalidDocumentException +from samtranslator.model.exceptions import InvalidDocumentException, InvalidResourceException from samtranslator.model import Resource from samtranslator.model.sam_resources import SamSimpleTable from samtranslator.public.plugins import BasePlugin from tests.translator.helpers import get_template_parameter_values +from tests.plugins.application.test_serverless_app_plugin import mock_get_region from samtranslator.yaml_helper import yaml_parse from parameterized import parameterized, param @@ -22,6 +23,12 @@ from samtranslator.translator.transform import transform from mock import Mock, MagicMock, patch +BASE_PATH = os.path.dirname(__file__) +INPUT_FOLDER = BASE_PATH + '/input' +OUTPUT_FOLDER = BASE_PATH + '/output' +# Do not sort AWS::Serverless::Function Layers Property. +# Order of Layers is an important attribute and shouldn't be changed. +DO_NOT_SORT = ['Layers'] BASE_PATH = os.path.dirname(__file__) INPUT_FOLDER = os.path.join(BASE_PATH, 'input') @@ -85,6 +92,43 @@ def custom_list_data_comparator(obj1, obj2): s1, s2 = type(obj1).__name__, type(obj2).__name__ return (s1 > s2) - (s1 < s2) +def mock_sar_service_call(self, service_call_function, logical_id, *args): + """ + Current implementation: args[0] is always the application_id + """ + application_id = args[0] + status = 'ACTIVE' + if application_id == "no-access": + raise InvalidResourceException(logical_id, "Cannot access application: {}.".format(application_id)) + elif application_id == "non-existent": + raise InvalidResourceException(logical_id, "Cannot access application: {}.".format(application_id)) + elif application_id == "invalid-semver": + raise InvalidResourceException(logical_id, "Cannot access application: {}.".format(application_id)) + elif application_id == 1: + raise InvalidResourceException(logical_id, "Type of property 'ApplicationId' is invalid.".format(application_id)) + elif application_id == "preparing" and self._wait_for_template_active_status < 2: + self._wait_for_template_active_status += 1 + self.SLEEP_TIME_SECONDS = 0 + self.TEMPLATE_WAIT_TIMEOUT_SECONDS = 2 + status = "PREPARING" + elif application_id == "preparing-never-ready": + self._wait_for_template_active_status = True + self.SLEEP_TIME_SECONDS = 0 + self.TEMPLATE_WAIT_TIMEOUT_SECONDS = 0 + status = "PREPARING" + elif application_id == "expired": + status = "EXPIRED" + message = { + 'ApplicationId': args[0], + 'CreationTime': 'x', + 'ExpirationTime': 'x', + 'SemanticVersion': '1.1.1', + 'Status': status, + 'TemplateId': 'id-xx-xx', + 'TemplateUrl': 'https://awsserverlessrepo-changesets-xxx.s3.amazonaws.com/signed-url' + } + return message + # implicit_api, explicit_api, explicit_api_ref, api_cache tests currently have deployment IDs hardcoded in output file. # These ids are generated using sha1 hash of the swagger body for implicit # api and s3 location for explicit api. @@ -94,6 +138,9 @@ class TestTranslatorEndToEnd(TestCase): @parameterized.expand( itertools.product([ 'basic_function', + 'basic_application', + 'application_preparing_state', + 'basic_layer', 'cloudwatchevent', 'cloudwatch_logs_with_ref', 'cloudwatchlog', @@ -131,6 +178,8 @@ class TestTranslatorEndToEnd(TestCase): 'alexa_skill', 'alexa_skill_with_skill_id', 'iot_rule', + 'layers_with_intrinsics', + 'layers_all_properties', 'function_managed_inline_policy', 'unsupported_resources', 'intrinsic_functions', @@ -148,6 +197,9 @@ class TestTranslatorEndToEnd(TestCase): 'function_with_resource_refs', 'function_with_deployment_and_custom_role', 'function_with_deployment_no_service_role', + 'function_with_global_layers', + 'function_with_layers', + 'function_with_many_layers', 'function_with_policy_templates', 'globals_for_function', 'globals_for_api', @@ -168,6 +220,8 @@ class TestTranslatorEndToEnd(TestCase): ] # Run all the above tests against each of the list of partitions to test against ) ) + @patch('samtranslator.plugins.application.serverless_app_plugin.ServerlessAppPlugin._sar_service_call', mock_sar_service_call) + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_transform_success(self, testcase, partition_with_region): partition = partition_with_region[0] region = partition_with_region[1] @@ -269,22 +323,27 @@ def _generate_new_deployment_hash(self, logical_id, dict_to_hash, rest_api_to_sw data_hash = hashlib.sha1(data_bytes).hexdigest() rest_api_to_swagger_hash[logical_id] = data_hash - @pytest.mark.parametrize('testcase', [ 'error_api_duplicate_methods_same_path', 'error_api_invalid_auth', 'error_api_invalid_definitionuri', 'error_api_invalid_definitionbody', 'error_api_invalid_restapiid', + 'error_application_properties', + 'error_application_does_not_exist', + 'error_application_no_access', + 'error_application_preparing_timeout', 'error_cors_on_external_swagger', 'error_invalid_cors_dict', 'error_function_invalid_codeuri', + 'error_function_invalid_layer', 'error_function_no_codeuri', 'error_function_no_handler', 'error_function_no_runtime', 'error_function_with_deployment_preference_missing_alias', 'error_function_with_invalid_deployment_preference_hook_property', 'error_invalid_logical_id', + 'error_layer_invalid_properties', 'error_missing_queue', 'error_missing_startingposition', 'error_missing_stream', @@ -307,6 +366,8 @@ def _generate_new_deployment_hash(self, logical_id, dict_to_hash, rest_api_to_sw 'error_function_with_invalid_policy_statement' ]) @patch('boto3.session.Session.region_name', 'ap-southeast-1') +@patch('samtranslator.plugins.application.serverless_app_plugin.ServerlessAppPlugin._sar_service_call', mock_sar_service_call) +@patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_transform_invalid_document(testcase): manifest = yaml_parse(open(os.path.join(INPUT_FOLDER, testcase + '.yaml'), 'r')) expected = json.load(open(os.path.join(OUTPUT_FOLDER, testcase + '.json'), 'r')) @@ -322,6 +383,7 @@ def test_transform_invalid_document(testcase): assert error_message == expected.get('errorMessage') @patch('boto3.session.Session.region_name', 'ap-southeast-1') +@patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_transform_unhandled_failure_empty_managed_policy_map(): document = { 'Transform': 'AWS::Serverless-2016-10-31', @@ -378,6 +440,7 @@ def assert_metric_call(mock, transform, transform_failure=0, invalid_document=0) @patch('boto3.session.Session.region_name', 'ap-southeast-1') +@patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_swagger_body_sha_gets_recomputed(): document = { @@ -420,6 +483,7 @@ def test_swagger_body_sha_gets_recomputed(): @patch('boto3.session.Session.region_name', 'ap-southeast-1') +@patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_swagger_definitionuri_sha_gets_recomputed(): document = { @@ -482,6 +546,7 @@ def setUp(self): } @patch('boto3.session.Session.region_name', 'ap-southeast-1') + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_logical_id_change_with_parameters(self): parameter_values = { 'CodeKeyParam': 'value1' @@ -497,6 +562,7 @@ def test_logical_id_change_with_parameters(self): assert first_version_id != second_version_id @patch('boto3.session.Session.region_name', 'ap-southeast-1') + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_logical_id_remains_same_without_parameter_change(self): parameter_values = { 'CodeKeyParam': 'value1' @@ -511,6 +577,7 @@ def test_logical_id_remains_same_without_parameter_change(self): assert first_version_id == second_version_id @patch('boto3.session.Session.region_name', 'ap-southeast-1') + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_logical_id_without_resolving_reference(self): # Now value of `CodeKeyParam` is not present in document @@ -646,6 +713,7 @@ def test_add_default_parameter_values_must_ignore_invalid_template_parameters(se class TestTemplateValidation(TestCase): + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_throws_when_resource_not_found(self): template = { "foo": "bar" @@ -656,6 +724,7 @@ def test_throws_when_resource_not_found(self): translator = Translator({}, sam_parser) translator.translate(template, {}) + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_throws_when_resource_is_empty(self): template = { "Resources": {} @@ -667,6 +736,7 @@ def test_throws_when_resource_is_empty(self): translator.translate(template, {}) + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_throws_when_resource_is_not_dict(self): template = { "Resources": [1,2,3] @@ -681,6 +751,7 @@ class TestPluginsUsage(TestCase): # Tests if plugins are properly injected into the translator @patch("samtranslator.translator.translator.make_policy_template_for_function_plugin") + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_prepare_plugins_must_add_required_plugins(self, make_policy_template_for_function_plugin_mock): # This is currently the only required plugin @@ -688,9 +759,10 @@ def test_prepare_plugins_must_add_required_plugins(self, make_policy_template_fo make_policy_template_for_function_plugin_mock.return_value = plugin_instance sam_plugins = prepare_plugins([]) - self.assertEquals(4, len(sam_plugins)) + self.assertEquals(5, len(sam_plugins)) @patch("samtranslator.translator.translator.make_policy_template_for_function_plugin") + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_prepare_plugins_must_merge_input_plugins(self, make_policy_template_for_function_plugin_mock): required_plugin = BasePlugin("something") @@ -698,12 +770,13 @@ def test_prepare_plugins_must_merge_input_plugins(self, make_policy_template_for custom_plugin = BasePlugin("someplugin") sam_plugins = prepare_plugins([custom_plugin]) - self.assertEquals(5, len(sam_plugins)) + self.assertEquals(6, len(sam_plugins)) + @patch('botocore.client.ClientEndpointBridge._check_default_region', mock_get_region) def test_prepare_plugins_must_handle_empty_input(self): sam_plugins = prepare_plugins(None) - self.assertEquals(4, len(sam_plugins)) + self.assertEquals(5, len(sam_plugins)) @patch("samtranslator.translator.translator.PolicyTemplatesProcessor") @patch("samtranslator.translator.translator.PolicyTemplatesForFunctionPlugin") @@ -786,3 +859,4 @@ def get_resource_by_type(template, type): def get_exception_error_message(e): return reduce(lambda message, error: message + ' ' + error.message, e.value.causes, e.value.message) + diff --git a/versions/2016-10-31.md b/versions/2016-10-31.md index 9dc883a00..3512d746b 100644 --- a/versions/2016-10-31.md +++ b/versions/2016-10-31.md @@ -91,7 +91,9 @@ Globals: ### Resource types - [AWS::Serverless::Function](#awsserverlessfunction) - [AWS::Serverless::Api](#awsserverlessapi) + - [AWS::Serverless::Application](#awsserverlessapplication) - [AWS::Serverless::SimpleTable](#awsserverlesssimpletable) + - [AWS::Serverless::LayerVersion](#awsserverlesslayerversion) #### AWS::Serverless::Function @@ -119,6 +121,7 @@ Tracing | `string` | String that specifies the function's [X-Ray tracing mode](h KmsKeyArn | `string` | The Amazon Resource Name (ARN) of an AWS Key Management Service (AWS KMS) key that Lambda uses to encrypt and decrypt your function's environment variables. DeadLetterQueue | `map` | [DeadLetterQueue Object](#deadletterqueue-object) | Configures SNS topic or SQS queue where Lambda sends events that it can't process. DeploymentPreference | [DeploymentPreference Object](#deploymentpreference-object) | Settings to enable Safe Lambda Deployments. Read the [usage guide](../docs/safe_lambda_deployments.rst) for detailed information. +Layers | list of `string` | List of LayerVersion ARNs that should be used by this function. The order specified here is the order that they will be imported when running the Lambda function. AutoPublishAlias | `string` | Name of the Alias. Read [AutoPublishAlias Guide](../docs/safe_lambda_deployments.rst#instant-traffic-shifting-using-lambda-aliases) for how it works ReservedConcurrentExecutions | `integer` | The maximum of concurrent executions you want to reserve for the function. For more information see [AWS Documentation on managing concurrency](https://docs.aws.amazon.com/lambda/latest/dg/concurrent-executions.html) @@ -195,6 +198,8 @@ Events: Tags: AppNameTag: ThumbnailApp DepartmentNameTag: ThumbnailDepartment +Layers: + - !Sub arn:aws:lambda:${AWS:Region}:123456789012:layer:MyLayer:1 ``` #### AWS::Serverless::Api @@ -238,6 +243,58 @@ DefinitionUri: swagger.yml SAM will generate an API Gateway Stage and API Gateway Deployment for every `AWS::Serverless::Api` resource. If you want to refer to these properties in an intrinsic function such as Ref or Fn::GetAtt, you can append `.Stage` and `.Deployment` suffix to the API's Logical ID. SAM will convert it to the correct Logical ID of the auto-generated Stage or Deployment resource respectively. +#### AWS::Serverless::Application + +Embeds a serverless application from the [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/) or from an Amazon S3 bucket as a nested application. Nested applications are deployed as nested stacks, which can contain multiple other resources, including other `AWS::Serverless::Application` resources. + +##### Properties + +Property Name | Type | Description +---|:---:|--- +Location | `string` or [Application Location Object](#application-location-object) | **Required** Template URL or location of nested application. If a template URL is given, it must follow the format specified in the [CloudFormation TemplateUrl documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stack.html#cfn-cloudformation-stack-templateurl) and contain a valid CloudFormation or SAM template. +Parameters | Map of `string` to `string` | Application parameter values. +NotificationArns | List of `string` | A list of existing Amazon SNS topics where notifications about stack events are sent. +Tags | Map of `string` to `string` | A map (string to string) that specifies the [tags](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) to be added to this application. When the stack is created, SAM will automatically add the following tags: lambda:createdBy:SAM, serverlessrepo:applicationId:\, serverlessrepo:semanticVersion:\. +TimeoutInMinutes | `integer` | The length of time, in minutes, that AWS CloudFormation waits for the nested stack to reach the CREATE_COMPLETE state. The default is no timeout. When AWS CloudFormation detects that the nested stack has reached the CREATE_COMPLETE state, it marks the nested stack resource as CREATE_COMPLETE in the parent stack and resumes creating the parent stack. If the timeout period expires before the nested stack reaches CREATE_COMPLETE, AWS CloudFormation marks the nested stack as failed and rolls back both the nested stack and parent stack. + +Other provided top-level resource attributes, e.g., Condition, DependsOn, etc, are automatically passed through to the underlying AWS::CloudFormation::Stack resource. + + +##### Return values + +###### Ref + +When the logical ID of this resource is provided to the [Ref intrinsic function](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html), it returns the resource name of the underlying CloudFormation nested stack. + +###### Fn::GetAtt + +When the logical ID of this resource is provided to the [Fn::GetAtt intrinsic function](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html), it returns a value for a specified attribute of this type. This section lists the available attributes. + +Attribute Name | Description +---|--- +Outputs.*ApplicationOutputName* | The value of the stack output with name *ApplicationOutputName*. + +##### Example: AWS::Serverless::Application + +```yaml +Resources: + MyApplication: + Properties: + Location: + ApplicationId: 'arn:aws:serverlessrepo:us-east-1:012345678901:applications/my-application' + SemanticVersion: 1.0.0 + Parameters: + StringParameter: parameter-value + IntegerParameter: 2 + MyOtherApplication: + Properties: + Location: https://s3-us-east-1.amazonaws.com/demo-bucket/template.yaml +Outputs: + MyNestedApplicationOutput: + Value: !GetAtt MyApplication.Outputs.ApplicationOutputName + Description: Example nested application output +``` + #### AWS::Serverless::SimpleTable The `AWS::Serverless::SimpleTable` resource creates a DynamoDB table with a single attribute primary key. It is useful when data only needs to be accessed via a primary key. To use the more advanced functionality of DynamoDB, use an [AWS::DynamoDB::Table](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html) resource instead. @@ -276,6 +333,40 @@ Properties: SSEEnabled: true ``` +#### AWS::Serverless::LayerVersion + +Creates a Lambda LayerVersion that contains library or runtime code needed by a Lambda Function. When a Serverless LayerVersion is transformed, SAM also transforms the logical id of the resource so that old LayerVersions are not automatically deleted by CloudFormation when the resource is updated. + +Property Name | Type | Description +---|:---:|--- +LayerName | `string` | Name of this layer. If you don't specify a name, the logical id of the resource will be used as the name. +Description | `string` | Description of this layer. +ContentUri | `string` | [S3 Location Object](#s3-location-object) | **Required.** S3 Uri or location for the layer code. +CompatibleRuntimes | List of `string`| List of runtimes compatible with this LayerVersion. +LicenseInfo | `string` | Information about the license for this LayerVersion. +RetentionPolicy | `string` | Options are `Retain` and `Delete`. Defaults to `Retain`. When `Retain` is set, SAM adds `DeletionPolicy: Retain` to the transformed resource so CloudFormation does not delete old versions after an update. + +##### Return values + +###### Ref + +When the logical ID of this resource is provided to the [Ref](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) intrinsic function, it returns the resource ARN of the underlying Lambda LayerVersion. + +##### Example: AWS::Serverless::LayerVersion + +```yaml +Properties: + LayerName: MyLayer + Description: Layer description + ContentUri: 's3://my-bucket/my-layer.zip' + CompatibleRuntimes: + - nodejs6.10 + - nodejs8.10 + LicenseInfo: 'Available under the MIT-0 license.' + RetentionPolicy: Retain +``` + + ### Event source types - [S3](#s3) - [SNS](#sns) @@ -574,6 +665,7 @@ Properties: ### Data Types - [S3 Location Object](#s3-location-object) +- [Application Location Object](#application-id-object) - [DeadLetterQueue Object](#deadletterqueue-object) - [Cors Configuration](#cors-configuration) - [API Auth Object](#api-auth-object) @@ -592,6 +684,18 @@ CodeUri: Version: 121212 ``` +#### Application Location Object + +Specifies the location of an application hosted in the [AWS Serverless Application Repository](https://aws.amazon.com/serverless/serverlessrepo/) as a dictionary containing ApplicationId and SemanticVersion properties. + +Example: + +```yaml +Location: # Both parameters are required + ApplicationId: 'arn:aws:serverlessrepo:us-east-1:012345678901:applications/my-application' + SemanticVersion: 1.0.0 +``` + #### DeadLetterQueue Object Specifies an SQS queue or SNS topic that AWS Lambda (Lambda) sends events to when it can't process them. For more information about DLQ functionality, refer to the officiall documentation at http://docs.aws.amazon.com/lambda/latest/dg/dlq.html. SAM will automatically add appropriate permission to your Lambda function execution role to give Lambda service access to the resource. `sqs:SendMessage` will be added for SQS queues and `sns:Publish` for SNS topics. @@ -688,4 +792,4 @@ Configure Auth for a specific Api+Path+Method. ```yaml Auth: Authorizer: MyCognitoAuth # OPTIONAL -``` \ No newline at end of file +```