Skip to content

Commit

Permalink
feat: add AWS::Serverless::LayerVersion and AWS::Serverless::Applicat…
Browse files Browse the repository at this point in the history
…ion (#688)
  • Loading branch information
keetonian committed Nov 29, 2018
1 parent 75cb2e8 commit 7b4c7b0
Show file tree
Hide file tree
Showing 81 changed files with 3,984 additions and 98 deletions.
15 changes: 15 additions & 0 deletions docs/cloudformation_compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <FunctionLogicalId+AliasName>. So SAM either needs a string for alias name, or a Ref to template Parameter that SAM can resolve into a string.
ReservedConcurrentExecutions All
============================ ================================== ========================
Expand Down Expand Up @@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
1 change: 1 addition & 0 deletions docs/globals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Currently, the following resources and properties are being supported:
Tags:
Tracing:
KmsKeyArn:
Layers:
AutoPublishAlias:
DeploymentPreference:
Expand Down
8 changes: 8 additions & 0 deletions examples/2016-10-31/nested_app/README.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions examples/2016-10-31/nested_app/src/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import logging

LOGGER = logging.getLogger()
LOGGER.setLevel(logging.INFO)

def process_tweets(tweets, context):
LOGGER.info("Received tweets: {}".format(tweets))
56 changes: 56 additions & 0 deletions examples/2016-10-31/nested_app/template.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion samtranslator/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.8.0'
__version__ = '1.9.0'
185 changes: 170 additions & 15 deletions samtranslator/intrinsics/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand All @@ -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):
"""
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Loading

0 comments on commit 7b4c7b0

Please sign in to comment.